commit 1c2137347818f007bec8818e20920342562034cf Author: Foti Dim Date: Wed Jan 24 14:23:31 2024 +0100 Initial commit diff --git a/.github/workflows/cr.yml b/.github/workflows/cr.yml new file mode 100644 index 0000000..5f747fe --- /dev/null +++ b/.github/workflows/cr.yml @@ -0,0 +1,27 @@ +name: Code Review + +permissions: + contents: read + pull-requests: write + +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: anc95/ChatGPT-CodeReview@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # Optional + OPENAI_API_ENDPOINT: https://api.openai.com/v1 + MODEL: gpt-3.5-turbo-1106 # https://platform.openai.com/docs/models + PROMPT: "Please check if there are any bugs, confusions, or irregularities in the following code diff: " + top_p: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-top_p + temperature: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature + max_tokens: 4096 + MAX_PATCH_LENGTH: 10000 # if the patch/diff length is large than MAX_PATCH_LENGTH, will be ignored and won't review. By default, with no MAX_PATCH_LENGTH set, there is also no limit for the patch/diff length. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..e4874b0 --- /dev/null +++ b/.metadata @@ -0,0 +1,42 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d211f42860350d914a5ad8102f9ec32764dc6d06" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: android + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: ios + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: linux + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: macos + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + - platform: windows + create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..a33045a --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "windowsSdkVersion": "10.0.20348.0", + "compilerPath": "cl.exe", + "cStandard": "c17", + "cppStandard": "c++20", + "intelliSenseMode": "windows-msvc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ac15806 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "example", + "cwd": "example", + "request": "launch", + "type": "dart" + }, + { + "name": "example_auto_connect", + "cwd": "example", + "request": "launch", + "type": "dart", + "args": [ + "--dart-define", + "AUTO_CONNECT=true", + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9d2857f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,106 @@ +{ + "cSpell.words": [ + "connnection", + "DSIMPLEBLE", + "Inspectable", + "microtask", + "modalias", + "NSUUID", + "pairable", + "rssi", + "simpleble", + "unawaited", + "Unpair", + "unwatch", + "uuidstr" + ], + "cmake.configureOnOpen": false, + "files.associations": { + "xstring": "cpp", + "chrono": "cpp", + "xlocale": "cpp", + "optional": "cpp", + "memory": "cpp", + "algorithm": "cpp", + "any": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "coroutine": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "exception": "cpp", + "format": "cpp", + "forward_list": "cpp", + "functional": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "iterator": "cpp", + "limits": "cpp", + "list": "cpp", + "locale": "cpp", + "map": "cpp", + "mutex": "cpp", + "new": "cpp", + "ostream": "cpp", + "ratio": "cpp", + "set": "cpp", + "shared_mutex": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "string": "cpp", + "system_error": "cpp", + "thread": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "typeinfo": "cpp", + "unordered_map": "cpp", + "utility": "cpp", + "variant": "cpp", + "vector": "cpp", + "xfacet": "cpp", + "xhash": "cpp", + "xiosbase": "cpp", + "xlocbuf": "cpp", + "xlocinfo": "cpp", + "xlocmes": "cpp", + "xlocmon": "cpp", + "xlocnum": "cpp", + "xloctime": "cpp", + "xmemory": "cpp", + "xtr1common": "cpp", + "xtree": "cpp", + "xutility": "cpp", + "deque": "cpp", + "fstream": "cpp", + "unordered_set": "cpp", + "complex": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "filesystem": "cpp", + "ranges": "cpp", + "span": "cpp", + "stack": "cpp", + "typeindex": "cpp", + "future": "cpp", + "regex": "cpp" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e551ca3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.8.0 + +* Initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77b395a --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2024, Navideck Labs OÜ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +Copyright (c) 2020, Woodemi Co,Ltd. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d99b7e --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# Universal Ble + +A cross-platform (Android/iOS/MacOS/Windows/Linux/Web) BluetoothLE plugin for Flutter + +# Usage + +- [Receive BLE availability changes](#receive-ble-availability-changes) +- [Scan BLE peripheral](#scan-ble-peripheral) +- [Connect BLE peripheral](#connect-ble-peripheral) +- [Discover services of BLE peripheral](#discover-services-of-ble-peripheral) +- [Transfer data between BLE central & peripheral](#transfer-data-between-ble-central--peripheral) +- [Pair BLE peripheral](#pair-ble-central--peripheral) + +| API | Android | iOS | macOS | Windows (beta) | Linux (beta) | Web | +| :------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: | +| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| getConnectedDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | +| discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| readValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| writeValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| setNotifiable | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| pair/unPair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | +| onPairStateChange | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | +| enableBluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | +| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | + +## Getting Started + +### Android + +You need to add the following permissions to your AndroidManifest.xml file: + +```xml + + + + +``` + +If you use `BLUETOOTH_SCAN` to determine location, modify your AndroidManifest.xml file to include the following entry: + +```xml + +``` + +If you use location services in your app, remove `android:maxSdkVersion="30"` from the location permission tags + +### iOS / MacOS + +Add `NSBluetoothPeripheralUsageDescription` and `NSBluetoothAlwaysUsageDescription` in Info.plist of your iOS and MacOS app, + +Add `Bluetooth` capability to MacOS app from Xcode + +## Scan BLE peripheral + +Android/iOS/macOS/Windows/Linux + +```dart +UniversalBle.onScanResult = (scanResult) { + // Handle scan result +} + +UniversalBle.startScan(); + +// To scan on web, you can optionally add filters, and `optionalServices` required to discover those services after connection, for more info check [this](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice) +UniversalBle.startScan( + webRequestOptions: WebRequestOptionsBuilder.acceptAllDevices( + optionalServices: [ + "SERVICE_UUID", + ], + ) +); + +UniversalBle.stopScan(); +``` + +## Connect to BLE peripheral + +Connect to `deviceId`, received from `UniversalBle.onScanResult` + +```dart +UniversalBle.onConnectionChanged = (String deviceId, BleConnectionState state) { + print('onConnectionChanged $deviceId, $state'); +} + +UniversalBle.connect(deviceId); + +UniversalBle.disconnect(deviceId); +``` + +Get connected devices ( These devices might be connected through another applications, connect using `connect` method to use in your application ), use `withServices` to filter devices + +```dart +await UniversalBle.getConnectedDevices(withServices: []); +``` + +## Discover services of BLE peripheral + +Discover services of `deviceId` + +```dart +UniversalBle.discoverServices(deviceId); +``` + +## Transfer data between BLE central & peripheral + +- Pull data from peripheral of `deviceId` + +```dart +UniversalBle.readValue(deviceId, serviceId, characteristicId); +``` + +- Send data to peripheral of `deviceId` + +```dart +UniversalBle.writeValue(deviceId, serviceId, characteristicId, value); +``` + +- Receive data from peripheral of `deviceId` + +```dart +UniversalBle.onValueChanged = (String deviceId, String characteristicId, Uint8List value) { + print('onValueChanged $deviceId, $characteristicId, ${hex.encode(value)}'); +} + +// To get characteristic updates in `onValueChanged` +UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProperty.notification); + +// To stop updates +UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProperty.disabled); +``` + +## Pair BLE central & peripheral + +- Pair apis + +```dart +UniversalBle.onPairStateChange = (String deviceId, bool isPaired, String? error) { + // Handle Pairing state change +} + +// Get result in [onPairStateChange] +UniversalBle.pair(deviceId); + +UniversalBle.unPair(deviceId); + +UniversalBle.isPaired(deviceId); +``` + +## Enable bluetooth programatically + +```dart +UniversalBle.enableBluetooth(); +``` + +## Receive BLE availability changes + +```dart +UniversalBle.onAvailabilityChange = (state) { + // Handle ble availability states + // e.g. poweredOff or poweredOn, +}; +``` + +## To set your own implementation of UniversalBle for any specific platform + +```dart +// Create a class which extends `UniversalBlePlatform` +class UniversalBleMock extends UniversalBlePlatform { + // Implement all methods +} + +UniversalBle.setInstance(UniversalBleMock()); +``` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..086e44c --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,68 @@ +group 'com.navideck.universal_ble' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.navideck.universal_ble' + } + + compileSdkVersion 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } + + defaultConfig { + minSdkVersion 21 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..2f6c0d8 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'universal_ble' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..067f1cb --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + 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 new file mode 100644 index 0000000..877506c --- /dev/null +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt @@ -0,0 +1,598 @@ +// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package com.navideck.universal_ble + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + if (exception is FlutterError) { + return listOf( + exception.code, + exception.message, + exception.details + ) + } else { + return listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalBleScanResult ( + val deviceId: String, + val name: String? = null, + val isPaired: Boolean? = null, + val rssi: Long? = null, + val manufacturerData: ByteArray? = null, + val manufacturerDataHead: ByteArray? = null + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): UniversalBleScanResult { + val deviceId = list[0] as String + val name = list[1] as String? + val isPaired = list[2] as Boolean? + val rssi = list[3].let { if (it is Int) it.toLong() else it as Long? } + val manufacturerData = list[4] as ByteArray? + val manufacturerDataHead = list[5] as ByteArray? + return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerData, manufacturerDataHead) + } + } + fun toList(): List { + return listOf( + deviceId, + name, + isPaired, + rssi, + manufacturerData, + manufacturerDataHead, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalBleService ( + val uuid: String, + val characteristics: List? = null + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): UniversalBleService { + val uuid = list[0] as String + val characteristics = list[1] as List? + return UniversalBleService(uuid, characteristics) + } + } + fun toList(): List { + return listOf( + uuid, + characteristics, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class UniversalBleCharacteristic ( + val uuid: String, + val properties: List + +) { + companion object { + @Suppress("UNCHECKED_CAST") + fun fromList(list: List): UniversalBleCharacteristic { + val uuid = list[0] as String + val properties = list[1] as List + return UniversalBleCharacteristic(uuid, properties) + } + } + fun toList(): List { + return listOf( + uuid, + properties, + ) + } +} + +@Suppress("UNCHECKED_CAST") +private object UniversalBlePlatformChannelCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 128.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalBleCharacteristic.fromList(it) + } + } + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalBleScanResult.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalBleService.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is UniversalBleCharacteristic -> { + stream.write(128) + writeValue(stream, value.toList()) + } + is UniversalBleScanResult -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is UniversalBleService -> { + stream.write(130) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** + * Flutter -> Native + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ +interface UniversalBlePlatformChannel { + fun getBluetoothAvailabilityState(callback: (Result) -> Unit) + fun enableBluetooth(callback: (Result) -> Unit) + fun startScan() + fun stopScan() + fun connect(deviceId: String) + fun disconnect(deviceId: String) + fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long) + fun discoverServices(deviceId: String, callback: (Result>) -> Unit) + fun readValue(deviceId: String, service: String, characteristic: String, callback: (Result) -> Unit) + fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) + fun writeValue(deviceId: String, service: String, characteristic: String, value: ByteArray, bleOutputProperty: Long, callback: (Result) -> Unit) + fun isPaired(deviceId: String, callback: (Result) -> Unit) + fun pair(deviceId: String) + fun unPair(deviceId: String) + fun getConnectedDevices(withServices: List, callback: (Result>) -> Unit) + + companion object { + /** The codec used by UniversalBlePlatformChannel. */ + val codec: MessageCodec by lazy { + UniversalBlePlatformChannelCodec + } + /** Sets up an instance of `UniversalBlePlatformChannel` to handle messages through the `binaryMessenger`. */ + @Suppress("UNCHECKED_CAST") + fun setUp(binaryMessenger: BinaryMessenger, api: UniversalBlePlatformChannel?) { + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.getBluetoothAvailabilityState() { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.enableBluetooth() { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + var wrapped: List + try { + api.startScan() + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + var wrapped: List + try { + api.stopScan() + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + var wrapped: List + try { + api.connect(deviceIdArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + var wrapped: List + try { + api.disconnect(deviceIdArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val serviceArg = args[1] as String + val characteristicArg = args[2] as String + val bleInputPropertyArg = args[3].let { if (it is Int) it.toLong() else it as Long } + var wrapped: List + try { + api.setNotifiable(deviceIdArg, serviceArg, characteristicArg, bleInputPropertyArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + api.discoverServices(deviceIdArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val serviceArg = args[1] as String + val characteristicArg = args[2] as String + api.readValue(deviceIdArg, serviceArg, characteristicArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val expectedMtuArg = args[1].let { if (it is Int) it.toLong() else it as Long } + api.requestMtu(deviceIdArg, expectedMtuArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + val serviceArg = args[1] as String + val characteristicArg = args[2] as String + val valueArg = args[3] as ByteArray + val bleOutputPropertyArg = args[4].let { if (it is Int) it.toLong() else it as Long } + api.writeValue(deviceIdArg, serviceArg, characteristicArg, valueArg, bleOutputPropertyArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + api.isPaired(deviceIdArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + var wrapped: List + try { + api.pair(deviceIdArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val deviceIdArg = args[0] as String + var wrapped: List + try { + api.unPair(deviceIdArg) + wrapped = listOf(null) + } catch (exception: Throwable) { + wrapped = wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val withServicesArg = args[0] as List + api.getConnectedDevices(withServicesArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +@Suppress("UNCHECKED_CAST") +private object UniversalBleCallbackChannelCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 128.toByte() -> { + return (readValue(buffer) as? List)?.let { + UniversalBleScanResult.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is UniversalBleScanResult -> { + stream.write(128) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** + * Native -> Flutter + * + * Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. + */ +@Suppress("UNCHECKED_CAST") +class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger) { + companion object { + /** The codec used by UniversalBleCallbackChannel. */ + val codec: MessageCodec by lazy { + UniversalBleCallbackChannelCodec + } + } + fun onAvailabilityChanged(stateArg: Long, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged", codec) + channel.send(listOf(stateArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } + fun onPairStateChange(deviceIdArg: String, isPairedArg: Boolean, errorArg: String?, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange", codec) + channel.send(listOf(deviceIdArg, isPairedArg, errorArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } + fun onScanResult(resultArg: UniversalBleScanResult, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult", codec) + channel.send(listOf(resultArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } + fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged", codec) + channel.send(listOf(deviceIdArg, characteristicIdArg, valueArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } + fun onConnectionChanged(deviceIdArg: String, stateArg: Long, callback: (Result) -> Unit) { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged", codec) + channel.send(listOf(deviceIdArg, stateArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))); + } else { + callback(Result.success(Unit)); + } + } else { + callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", ""))); + } + } + } +} diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt new file mode 100644 index 0000000..f9ad05e --- /dev/null +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -0,0 +1,212 @@ +package com.navideck.universal_ble + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothStatusCodes +import android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED +import android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED +import android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED +import android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR +import android.bluetooth.le.ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES +import android.bluetooth.le.ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY +import android.bluetooth.le.ScanResult +import android.util.Log +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.UUID + +private const val TAG = "UniversalBlePlugin" + +val knownGatts = mutableListOf() + + +enum class BleConnectionState(val value: Long) { + Connected(0), + Disconnected(1) +} + +enum class AvailabilityState(val value: Long) { + Unknown(0), + Resetting(1), + Unsupported(2), + Unauthorized(3), + PoweredOff(4), + PoweredOn(5); +} + +enum class BleInputProperty(val value: Long) { + Disabled(0), + Notification(1), + Indication(2); +} + +enum class BleOutputProperty(val value: Long) { + withResponse(0), + withoutResponse(1); +} + + +enum class CharacteristicProperty(val value: Long) { + Broadcast(0), + Read(1), + WriteWithoutResponse(2), + Write(3), + Notify(4), + Indicate(5), + AuthenticatedSignedWrites(6), + ExtendedProperties(7) +} + +fun String.toBluetoothGatt(): BluetoothGatt { + return knownGatts.find { it.device.address == this } + ?: throw FlutterError("IllegalArgument", "Unknown deviceId: $this", null) +} + +fun Int.toAvailabilityState(): Long { + return when (this) { + BluetoothAdapter.STATE_OFF -> AvailabilityState.PoweredOff.value + BluetoothAdapter.STATE_ON -> AvailabilityState.PoweredOn.value + BluetoothAdapter.STATE_TURNING_ON -> AvailabilityState.Resetting.value + BluetoothAdapter.STATE_TURNING_OFF -> AvailabilityState.Resetting.value + else -> AvailabilityState.Unknown.value + } +} + +fun Int.parseScanErrorMessage(): String { + return when (this) { + SCAN_FAILED_ALREADY_STARTED -> "SCAN_FAILED_ALREADY_STARTED" + SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED" + SCAN_FAILED_FEATURE_UNSUPPORTED -> "SCAN_FAILED_FEATURE_UNSUPPORTED" + SCAN_FAILED_INTERNAL_ERROR -> "SCAN_FAILED_INTERNAL_ERROR" + SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES -> "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES" + SCAN_FAILED_SCANNING_TOO_FREQUENTLY -> "SCAN_FAILED_SCANNING_TOO_FREQUENTLY" + else -> "ErrorCode: $this" + } +} + +fun Int.parseBluetoothStatusCodeError(): String? { + if (this == BluetoothStatusCodes.SUCCESS) return null + return when (this) { + BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED -> "ERROR_BLUETOOTH_NOT_ENABLED" + BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED -> "ERROR_BLUETOOTH_NOT_ALLOWED" + BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED -> "ERROR_DEVICE_NOT_BONDED" + BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED -> "ERROR_GATT_WRITE_NOT_ALLOWED" + BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY -> "ERROR_GATT_WRITE_REQUEST_BUSY" + BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION -> "ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION" + BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND -> "ERROR_PROFILE_SERVICE_NOT_BOUND" + BluetoothStatusCodes.ERROR_UNKNOWN -> "ERROR_UNKNOWN" + BluetoothStatusCodes.FEATURE_NOT_CONFIGURED -> "FEATURE_NOT_CONFIGURED" + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED -> "FEATURE_NOT_SUPPORTED" + BluetoothStatusCodes.FEATURE_SUPPORTED -> "FEATURE_SUPPORTED" + else -> "ErrorCode: $this" + } +} + +val ScanResult.manufacturerDataHead: ByteArray? + get() { + val sparseArray = scanRecord?.manufacturerSpecificData ?: return null + if (sparseArray.size() == 0) return null + + return sparseArray.keyAt(0).toShort().toByteArray() + sparseArray.valueAt(0) + } + +fun BluetoothGatt.getCharacteristic( + service: String, + characteristic: String, +): BluetoothGattCharacteristic? = + getService(UUID.fromString(service)).getCharacteristic(UUID.fromString(characteristic)) + + +@SuppressLint("MissingPermission") +fun BluetoothGatt.setNotifiable( + gattCharacteristic: BluetoothGattCharacteristic, + bleInputProperty: Long, +) { + val descClientCharConfirmation = + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") + val descriptor = gattCharacteristic.getDescriptor(descClientCharConfirmation) + val bleInputPropertyEnum: BleInputProperty = + BleInputProperty.values().first { it.value == bleInputProperty } + val (value, enable) = when (bleInputPropertyEnum) { + BleInputProperty.Notification -> BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE to true + BleInputProperty.Indication -> BluetoothGattDescriptor.ENABLE_INDICATION_VALUE to true + else -> BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE to false + } + descriptor.value = value + setCharacteristicNotification(descriptor.characteristic, enable) && writeDescriptor(descriptor) +} + +fun BluetoothDevice.removeBond() { + try { + javaClass.getMethod("removeBond").invoke(this) + } catch (e: Exception) { + Log.e(TAG, "Removing bond has been failed. ${e.message}") + } +} + + +fun BluetoothGattCharacteristic.getPropertiesList(): ArrayList { + val propertiesList = arrayListOf() + if (properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0) { + propertiesList.add(CharacteristicProperty.Broadcast.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_READ > 0) { + propertiesList.add(CharacteristicProperty.Read.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0) { + propertiesList.add(CharacteristicProperty.WriteWithoutResponse.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0) { + propertiesList.add(CharacteristicProperty.Write.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) { + propertiesList.add(CharacteristicProperty.Notify.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0) { + propertiesList.add(CharacteristicProperty.Indicate.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0) { + propertiesList.add(CharacteristicProperty.AuthenticatedSignedWrites.value) + } + if (properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0) { + propertiesList.add(CharacteristicProperty.ExtendedProperties.value) + } + return propertiesList +} + + +fun Short.toByteArray(byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): ByteArray = + ByteBuffer.allocate(2 /*Short.SIZE_BYTES*/).order(byteOrder).putShort(this).array() + +// Errors +fun unknownCharacteristicError(char: String) = + FlutterError("IllegalArgument", "Unknown error", null) + +// Future result classes +class BleCharacteristicFuture( + val deviceId: String, + val characteristicId: String, + val serviceId: String, + val result: (Result) -> Unit, +) + +class DiscoverServicesFuture( + val deviceId: String, + val result: (Result>) -> Unit, +) + +class MtuResultFuture( + val deviceId: String, + val result: (Result) -> Unit, +) + +class WriteResultFuture( + val deviceId: String, + val characteristicId: String, + val serviceId: String, + val result: (Result) -> Unit, +) \ No newline at end of file diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt new file mode 100644 index 0000000..5bc7b42 --- /dev/null +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -0,0 +1,743 @@ +package com.navideck.universal_ble + +import android.annotation.SuppressLint +import android.app.Activity +import android.bluetooth.* +import android.bluetooth.BluetoothDevice.BOND_BONDED +import android.bluetooth.BluetoothDevice.BOND_NONE +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Context.RECEIVER_EXPORTED +import android.content.Intent +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 +import io.flutter.plugin.common.* +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +private const val TAG = "UniversalBlePlugin" + +@SuppressLint("MissingPermission") +class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), FlutterPlugin, + ActivityAware, PluginRegistry.ActivityResultListener { + private val bluetoothEnableRequestCode = 2342313 + private var callbackChannel: UniversalBleCallbackChannel? = null + private var mainThreadHandler: Handler? = null + private lateinit var context: Context + private var activity: Activity? = null + private lateinit var bluetoothManager: BluetoothManager + private val mtuResultFutureList = mutableListOf() + private val bleCharacteristicFutureList = mutableListOf() + private val discoverServicesFutureList = mutableListOf() + private val writeResultFutureList = mutableListOf() + private val cachedServicesMap = mutableMapOf>() + private val devicesStateMap = mutableMapOf() + private var bluetoothEnableRequestFuture: ((Result) -> Unit)? = null + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this) + callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger) + context = flutterPluginBinding.applicationContext + mainThreadHandler = Handler(Looper.getMainLooper()) + bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + + val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.registerReceiver(broadcastReceiver, intentFilter, RECEIVER_EXPORTED) + } else { + context.registerReceiver(broadcastReceiver, intentFilter) + } + cachedServicesMap.putAll(getCachedServicesMap()) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback) + context.unregisterReceiver(broadcastReceiver) + callbackChannel = null + mainThreadHandler = null + } + + + override fun getBluetoothAvailabilityState(callback: (Result) -> Unit) { + callback( + Result.success( + bluetoothManager.adapter?.state?.toAvailabilityState() + ?: AvailabilityState.Unknown.value + ) + ) + } + + override fun enableBluetooth(callback: (Result) -> Unit) { + if (bluetoothManager.adapter.isEnabled) { + callback(Result.success(true)) + return + } + if (bluetoothEnableRequestFuture != null) { + callback( + Result.failure( + FlutterError("Failed", "Bluetooth enable request in progress", null) + ) + ) + return + } + val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + activity?.startActivityForResult(enableBtIntent, bluetoothEnableRequestCode) + bluetoothEnableRequestFuture = callback + } + + override fun startScan() { + if (!isBluetoothAvailable()) throw FlutterError( + "BluetoothNotEnabled", + "Bluetooth not enabled", + ) + bluetoothManager.adapter.bluetoothLeScanner?.startScan(scanCallback) + } + + override fun stopScan() { + if (!isBluetoothAvailable()) throw FlutterError( + "BluetoothNotEnabled", + "Bluetooth not enabled", + ) + // check if already scanning + bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback) + } + + override fun connect(deviceId: String) { + val currentState = devicesStateMap[deviceId] + + // If already connected, send connected message, + // if connecting, do nothing + knownGatts.find { it.device.address == deviceId }?.let { + if (currentState == BluetoothGatt.STATE_CONNECTED) { + Log.e(TAG, "$deviceId Already connected") + mainThreadHandler?.post { + callbackChannel?.onConnectionChanged( + deviceId, + BleConnectionState.Connected.value + ) {} + } + return + } else if (currentState == BluetoothGatt.STATE_CONNECTING) { + throw FlutterError("Connecting", "Connection already in progress", null) + } + } + + + val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) + val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + remoteDevice.connectGatt( + context, + false, + this, + BluetoothDevice.TRANSPORT_LE + ) + } else { + remoteDevice.connectGatt(context, false, this) + } + knownGatts.add(gatt) + } + + override fun disconnect(deviceId: String) { + // val currentState = devicesStateMap[deviceId] + // If `disconnect` is called before BluetoothGatt.STATE_CONNECTED or BluetoothGatt.STATE_CONNECTING + // there will be no `disconnected` message any more from callback + // if (currentState != null && currentState != BluetoothGatt.STATE_CONNECTED) { + // Log.e(TAG, "$deviceId Already disconnected or disconnecting") + // mainThreadHandler?.post { + // callbackChannel?.onConnectionChanged( + // deviceId, + // BleConnectionState.Disconnected.value + // ) {} + // } + // return + //} + cleanConnection(deviceId.toBluetoothGatt()) + } + + override fun discoverServices( + deviceId: String, + callback: (Result>) -> Unit, + ) { + if (!deviceId.toBluetoothGatt().discoverServices()) { + callback(Result.failure(FlutterError("Failed", "Failed to discover services", null))) + return + } + discoverServicesFutureList.add(DiscoverServicesFuture(deviceId, callback)) + } + + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + if (status != BluetoothGatt.GATT_SUCCESS) { + discoverServicesFutureList.filter { it.deviceId == gatt.device.address }.forEach { + discoverServicesFutureList.remove(it) + it.result( + Result.failure(FlutterError("Failed", "Failed to discover services", null)) + ) + } + return + } + setCachedServices(gatt.device.address, gatt.services.map { it.uuid.toString() }) + val universalBleServices = gatt.services.map { service -> + UniversalBleService( + uuid = service.uuid.toString(), + characteristics = service.characteristics.map { + UniversalBleCharacteristic( + uuid = it.uuid.toString(), + properties = it.getPropertiesList() + ) + } + ) + } + discoverServicesFutureList.filter { it.deviceId == gatt.device.address }.forEach { + discoverServicesFutureList.remove(it) + it.result(Result.success(universalBleServices)) + } + } + + + override fun setNotifiable( + deviceId: String, + service: String, + characteristic: String, + bleInputProperty: Long, + ) { + val gatt = deviceId.toBluetoothGatt() + val gattCharacteristic = gatt.getCharacteristic(service, characteristic) + ?: throw FlutterError( + "IllegalArgument", + "Unknown characteristic: $characteristic", + null + ) + gatt.setNotifiable(gattCharacteristic, bleInputProperty) + } + + override fun readValue( + deviceId: String, + service: String, + characteristic: String, + callback: (Result) -> Unit, + ) { + try { + val gatt = deviceId.toBluetoothGatt() + val gattCharacteristic = gatt.getCharacteristic(service, characteristic) + if (gattCharacteristic == null) { + callback( + Result.failure(FlutterError("IllegalArgument", "Unknown characteristic", null)) + ) + return + } + if (!gatt.readCharacteristic(gattCharacteristic)) { + callback( + Result.failure(unknownCharacteristicError(characteristic)) + ) + return + } + + bleCharacteristicFutureList.add( + BleCharacteristicFuture( + gatt.device.address, + gattCharacteristic.uuid.toString(), + gattCharacteristic.service.uuid.toString(), + callback + ) + ) + } catch (e: FlutterError) { + callback(Result.failure(e)) + } + } + + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + status: Int, + ) { + bleCharacteristicFutureList.filter { + it.deviceId == gatt.device.address && + it.characteristicId == characteristic.uuid.toString() && + it.serviceId == characteristic.service.uuid.toString() + }.forEach { + bleCharacteristicFutureList.remove(it) + it.result(Result.success(value)) + } + } + + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int, + ) { + // Pass data to new api + onCharacteristicRead(gatt, characteristic, characteristic.value, status) + } + + + override fun writeValue( + deviceId: String, + service: String, + characteristic: String, + value: ByteArray, + bleOutputProperty: Long, + callback: (Result) -> Unit, + ) { + try { + val gatt = deviceId.toBluetoothGatt() + val gattCharacteristic = gatt.getCharacteristic(service, characteristic) + if (gattCharacteristic == null) { + callback(Result.failure(unknownCharacteristicError(characteristic))) + return + } + + var writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + if (bleOutputProperty == BleOutputProperty.withResponse.value) { + if (gattCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE == 0) { + callback( + Result.failure( + FlutterError( + "IllegalArgument", + "Characteristic does not support write withResponse", + null + ) + ) + ) + return + } + writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + } else if (bleOutputProperty == BleOutputProperty.withoutResponse.value) { + if (gattCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE == 0) { + callback( + Result.failure( + FlutterError( + "IllegalArgument", + "Characteristic does not support write withoutResponse", + null + ) + ) + ) + return + } + writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE + } +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +// val result = gatt.writeCharacteristic(gattCharacteristic, value, writeType) +// Log.v(TAG,"writeResult $characteristic: $result => ${result.parseBluetoothStatusCodeError()}") +// return +// } + gattCharacteristic.value = value + gattCharacteristic.writeType = writeType + val result = gatt.writeCharacteristic(gattCharacteristic) + if (!result) { + callback(Result.failure(FlutterError("Failed", "Failed to write", null))) + return + } + if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) { + // wait for the result + writeResultFutureList.add( + WriteResultFuture( + gatt.device.address, + gattCharacteristic.uuid.toString(), + gattCharacteristic.service.uuid.toString(), + callback + ) + ) + } else { + callback(Result.success(Unit)) + } + } catch (e: FlutterError) { + callback(Result.failure(e)) + } + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic, + status: Int, + ) { + writeResultFutureList.filter { + it.deviceId == gatt?.device?.address && + it.characteristicId == characteristic.uuid.toString() && + it.serviceId == characteristic.service.uuid.toString() + }.forEach { + writeResultFutureList.remove(it) + if (status == BluetoothGatt.GATT_SUCCESS) { + it.result(Result.success(Unit)) + } else { + it.result( + Result.failure( + FlutterError( + "Failed", + "Failed to write ($status)", + "status $status" + ) + ) + ) + } + } + } + + + override fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result) -> Unit) { + val gatt = deviceId.toBluetoothGatt() + gatt.requestMtu(expectedMtu.toInt()) + mtuResultFutureList.add(MtuResultFuture(deviceId, callback)) + } + + override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { + val deviceId = gatt?.device?.address ?: return + mtuResultFutureList.filter { it.deviceId == deviceId }.forEach { + mtuResultFutureList.remove(it) + if (status == BluetoothGatt.GATT_SUCCESS) { + it.result(Result.success(mtu.toLong())) + } else { + it.result(Result.failure(FlutterError("Failed to change MTU", null, null))) + } + } + } + + override fun isPaired(deviceId: String, callback: (Result) -> Unit) { + val remoteDevice: BluetoothDevice = + bluetoothManager.adapter.getRemoteDevice(deviceId) + callback(Result.success(remoteDevice.bondState == BOND_BONDED)) + } + + override fun pair(deviceId: String) { + val remoteDevice = + bluetoothManager.adapter.getRemoteDevice(deviceId) + if (remoteDevice.bondState == BOND_NONE) { + if (!remoteDevice.createBond()) { + throw FlutterError("Failed", "Failed to pair", null) + } + } else { + throw FlutterError("AlreadyPair", "Already paired", null) + } + } + + override fun unPair(deviceId: String) { + val remoteDevice: BluetoothDevice = + bluetoothManager.adapter.getRemoteDevice(deviceId) + if (remoteDevice.bondState == BOND_BONDED) { + remoteDevice.removeBond() + } + } + + override fun getConnectedDevices( + withServices: List, + callback: (Result>) -> Unit, + ) { + var devices: List = + bluetoothManager.getConnectedDevices(BluetoothProfile.GATT) + if (withServices.isNotEmpty()) { + devices = filterDevicesByServices(devices, withServices) + } + callback( + Result.success( + devices.map { + UniversalBleScanResult( + name = it.name, + deviceId = it.address, + isPaired = it.bondState == BOND_BONDED, + manufacturerDataHead = null, + manufacturerData = null, + rssi = null, + ) + } + ) + ) + } + + private fun filterDevicesByServices( + devices: List, + withServices: List, + ): List { + // If all devices have cached services + if (devices.all { cachedServicesMap[it.address] != null }) { + return devices.filter { device -> + cachedServicesMap[device.address]?.any { uuid -> withServices.contains(uuid) } == true + } + } + + // Else discover services off already connected devices + val latch = CountDownLatch(devices.size) + val resultMap = mutableMapOf() + + devices.forEach { device -> + discoverServicesOffAlreadyConnectedDevice(device) { uuids -> + resultMap[device.address] = + uuids?.any { uuid -> withServices.contains(uuid) } == true + latch.countDown() + } + } + + try { + val timeout = (devices.size * 2).toLong() + latch.await(timeout, TimeUnit.SECONDS) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } + + return devices.filter { resultMap[it.address] == true } + } + + private fun discoverServicesOffAlreadyConnectedDevice( + device: BluetoothDevice, + callback: (List?) -> Unit, + ) { + // Check if already cached + cachedServicesMap[device.address]?.let { + callback(it) + return + } + + // To avoid duplicate callback + var isUpdated = false + fun updateCallback(uuids: List?) { + if (isUpdated) return + callback(uuids) + isUpdated = true + } + + // If its a known gatt, just discover services + knownGatts.find { it.device.address == device.address }?.let { + it.services?.let { services -> + updateCallback(services.map { service -> service.uuid.toString() }) + return + } + + if (it.discoverServices()) { + discoverServicesFutureList.add( + DiscoverServicesFuture(device.address) { uuids: Result> -> + if (uuids.isSuccess) { + updateCallback(uuids.getOrNull()?.map { it.uuid }) + } else { + updateCallback(null) + } + } + ) + return + } + } + + // Else connect to Gatt, discover services, then disconnect + val callbackHandler = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { + if (gatt?.discoverServices() != true) { + updateCallback(null) + if (!knownGatts.any { it.device.address == device.address }) { + gatt?.disconnect() + } + } + } else { + updateCallback(null) + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + if (status == BluetoothGatt.GATT_SUCCESS) { + val uuids = gatt?.services?.map { it.uuid.toString() } + if (uuids != null) { + setCachedServices(device.address, uuids) + } + updateCallback(uuids) + } + if (!knownGatts.any { it.device.address == device.address }) { + gatt?.disconnect() + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + device.connectGatt(context, false, callbackHandler, BluetoothDevice.TRANSPORT_LE) + } else { + device.connectGatt(context, false, callbackHandler) + } + } + + private fun cleanConnection(gatt: BluetoothGatt) { + knownGatts.remove(gatt) + gatt.disconnect() + bleCharacteristicFutureList.removeAll { + it.deviceId == gatt.device.address + } + mtuResultFutureList.removeAll { + it.deviceId == gatt.device.address + } + discoverServicesFutureList.removeAll { + it.deviceId == gatt.device.address + } + } + + private val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) { + mainThreadHandler?.post { + callbackChannel?.onAvailabilityChanged( + bluetoothManager.adapter?.state?.toAvailabilityState() + ?: AvailabilityState.Unknown.value + ) {} + } + } else if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) { + val device: BluetoothDevice = + intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) ?: return + // get pairing failed error + when (intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)) { + BluetoothDevice.BOND_BONDING -> { + Log.v(TAG, "${device.address} BOND_BONDING") + } + + BluetoothDevice.ERROR -> { + mainThreadHandler?.post { + callbackChannel?.onPairStateChange( + device.address, + false, + "No pairing state received" + ) {} + } + } + + BOND_BONDED -> { + mainThreadHandler?.post { + callbackChannel?.onPairStateChange( + device.address, + true, + null + ) {} + } + } + + + BOND_NONE -> { + mainThreadHandler?.post { + callbackChannel?.onPairStateChange( + device.address, + false, + null + ) {} + } + } + } + } + } + } + + private val scanCallback = object : ScanCallback() { + override fun onScanFailed(errorCode: Int) { + Log.e(TAG, "OnScanFailed: ${errorCode.parseScanErrorMessage()}") + } + + override fun onScanResult(callbackType: Int, result: ScanResult) { + // Log.v(TAG, "onScanResult: $result") + mainThreadHandler?.post { + callbackChannel?.onScanResult( + UniversalBleScanResult( + name = result.device.name, + deviceId = result.device.address, + isPaired = result.device.bondState == BOND_BONDED, + manufacturerDataHead = result.manufacturerDataHead, + rssi = result.rssi.toLong() + ) + ) {} + } + } + + override fun onBatchScanResults(results: MutableList?) { + Log.v(TAG, "onBatchScanResults: $results") + } + } + + + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + devicesStateMap[gatt.device.address] = newState + if (newState == BluetoothGatt.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { + mainThreadHandler?.post { + callbackChannel?.onConnectionChanged( + gatt.device.address, + BleConnectionState.Connected.value + ) {} + } + } else { + cleanConnection(gatt) + mainThreadHandler?.post { + callbackChannel?.onConnectionChanged( + gatt.device.address, + BleConnectionState.Disconnected.value + ) {} + } + } + } + + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + ) { + mainThreadHandler?.post { + callbackChannel?.onValueChanged( + deviceIdArg = gatt.device.address, + characteristicIdArg = characteristic.uuid.toString(), + valueArg = characteristic.value + ) {} + } + } + + private fun isBluetoothAvailable(): Boolean { + return bluetoothManager.adapter.isEnabled + } + + private fun setCachedServices(deviceId: String, services: List) { + val cachedServicesSharedPref = context.getSharedPreferences( + "com.navideck.universal_ble.services", + Context.MODE_PRIVATE + ) + cachedServicesSharedPref.edit().putStringSet(deviceId, services.toSet()).apply() + cachedServicesMap[deviceId] = services + } + + private fun getCachedServicesMap(): Map> { + val cachedServicesSharedPref = context.getSharedPreferences( + "com.navideck.universal_ble.services", + Context.MODE_PRIVATE + ) + return cachedServicesSharedPref.all.mapValues { (_, value) -> + (value as? Set<*>)?.map { it.toString() } ?: emptyList() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (requestCode == bluetoothEnableRequestCode) { + val future = bluetoothEnableRequestFuture ?: return false + future(Result.success(resultCode == Activity.RESULT_OK)) + bluetoothEnableRequestFuture = null + return true + } + return false + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + binding.addActivityResultListener(this) + } + + override fun onDetachedFromActivity() { + activity = null + } + + override fun onDetachedFromActivityForConfigChanges() {} + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} +} + + + + + + + + diff --git a/android/src/test/kotlin/com/navideck/universal_ble/UniversalBlePluginTest.kt b/android/src/test/kotlin/com/navideck/universal_ble/UniversalBlePluginTest.kt new file mode 100644 index 0000000..2040143 --- /dev/null +++ b/android/src/test/kotlin/com/navideck/universal_ble/UniversalBlePluginTest.kt @@ -0,0 +1,27 @@ +package com.navideck.universal_ble + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlin.test.Test +import org.mockito.Mockito + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class UniversalBlePluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = UniversalBlePlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/darwin/.gitignore b/darwin/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/darwin/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/darwin/Assets/.gitkeep b/darwin/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/darwin/Classes/UniversalBle.g.swift b/darwin/Classes/UniversalBle.g.swift new file mode 100644 index 0000000..e005102 --- /dev/null +++ b/darwin/Classes/UniversalBle.g.swift @@ -0,0 +1,525 @@ +// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)" + ] +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalBleScanResult { + var deviceId: String + var name: String? = nil + var isPaired: Bool? = nil + var rssi: Int64? = nil + var manufacturerData: FlutterStandardTypedData? = nil + var manufacturerDataHead: FlutterStandardTypedData? = nil + + static func fromList(_ list: [Any?]) -> UniversalBleScanResult? { + let deviceId = list[0] as! String + let name: String? = nilOrValue(list[1]) + let isPaired: Bool? = nilOrValue(list[2]) + let rssi: Int64? = isNullish(list[3]) ? nil : (list[3] is Int64? ? list[3] as! Int64? : Int64(list[3] as! Int32)) + let manufacturerData: FlutterStandardTypedData? = nilOrValue(list[4]) + let manufacturerDataHead: FlutterStandardTypedData? = nilOrValue(list[5]) + + return UniversalBleScanResult( + deviceId: deviceId, + name: name, + isPaired: isPaired, + rssi: rssi, + manufacturerData: manufacturerData, + manufacturerDataHead: manufacturerDataHead + ) + } + func toList() -> [Any?] { + return [ + deviceId, + name, + isPaired, + rssi, + manufacturerData, + manufacturerDataHead, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalBleService { + var uuid: String + var characteristics: [UniversalBleCharacteristic?]? = nil + + static func fromList(_ list: [Any?]) -> UniversalBleService? { + let uuid = list[0] as! String + let characteristics: [UniversalBleCharacteristic?]? = nilOrValue(list[1]) + + return UniversalBleService( + uuid: uuid, + characteristics: characteristics + ) + } + func toList() -> [Any?] { + return [ + uuid, + characteristics, + ] + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct UniversalBleCharacteristic { + var uuid: String + var properties: [Int64?] + + static func fromList(_ list: [Any?]) -> UniversalBleCharacteristic? { + let uuid = list[0] as! String + let properties = list[1] as! [Int64?] + + return UniversalBleCharacteristic( + uuid: uuid, + properties: properties + ) + } + func toList() -> [Any?] { + return [ + uuid, + properties, + ] + } +} + +private class UniversalBlePlatformChannelCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?]) + case 129: + return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) + case 130: + return UniversalBleService.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class UniversalBlePlatformChannelCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? UniversalBleCharacteristic { + super.writeByte(128) + super.writeValue(value.toList()) + } else if let value = value as? UniversalBleScanResult { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? UniversalBleService { + super.writeByte(130) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class UniversalBlePlatformChannelCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return UniversalBlePlatformChannelCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return UniversalBlePlatformChannelCodecWriter(data: data) + } +} + +class UniversalBlePlatformChannelCodec: FlutterStandardMessageCodec { + static let shared = UniversalBlePlatformChannelCodec(readerWriter: UniversalBlePlatformChannelCodecReaderWriter()) +} + +/// Flutter -> Native +/// +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol UniversalBlePlatformChannel { + func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) + func enableBluetooth(completion: @escaping (Result) -> Void) + func startScan() throws + func stopScan() throws + func connect(deviceId: String) throws + func disconnect(deviceId: String) throws + func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64) throws + func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) + func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) + func requestMtu(deviceId: String, expectedMtu: Int64, completion: @escaping (Result) -> Void) + func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result) -> Void) + func isPaired(deviceId: String, completion: @escaping (Result) -> Void) + func pair(deviceId: String) throws + func unPair(deviceId: String) throws + func getConnectedDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class UniversalBlePlatformChannelSetup { + /// The codec used by UniversalBlePlatformChannel. + static var codec: FlutterStandardMessageCodec { UniversalBlePlatformChannelCodec.shared } + /// Sets up an instance of `UniversalBlePlatformChannel` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UniversalBlePlatformChannel?) { + let getBluetoothAvailabilityStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getBluetoothAvailabilityStateChannel.setMessageHandler { _, reply in + api.getBluetoothAvailabilityState() { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getBluetoothAvailabilityStateChannel.setMessageHandler(nil) + } + let enableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + enableBluetoothChannel.setMessageHandler { _, reply in + api.enableBluetooth() { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + enableBluetoothChannel.setMessageHandler(nil) + } + let startScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + startScanChannel.setMessageHandler { _, reply in + do { + try api.startScan() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + startScanChannel.setMessageHandler(nil) + } + let stopScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + stopScanChannel.setMessageHandler { _, reply in + do { + try api.stopScan() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + stopScanChannel.setMessageHandler(nil) + } + let connectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + connectChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + do { + try api.connect(deviceId: deviceIdArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + connectChannel.setMessageHandler(nil) + } + let disconnectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + disconnectChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + do { + try api.disconnect(deviceId: deviceIdArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + disconnectChannel.setMessageHandler(nil) + } + let setNotifiableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setNotifiableChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + let serviceArg = args[1] as! String + let characteristicArg = args[2] as! String + let bleInputPropertyArg = args[3] is Int64 ? args[3] as! Int64 : Int64(args[3] as! Int32) + do { + try api.setNotifiable(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, bleInputProperty: bleInputPropertyArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + setNotifiableChannel.setMessageHandler(nil) + } + let discoverServicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + discoverServicesChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + api.discoverServices(deviceId: deviceIdArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + discoverServicesChannel.setMessageHandler(nil) + } + let readValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + readValueChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + let serviceArg = args[1] as! String + let characteristicArg = args[2] as! String + api.readValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + readValueChannel.setMessageHandler(nil) + } + let requestMtuChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + requestMtuChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + let expectedMtuArg = args[1] is Int64 ? args[1] as! Int64 : Int64(args[1] as! Int32) + api.requestMtu(deviceId: deviceIdArg, expectedMtu: expectedMtuArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + requestMtuChannel.setMessageHandler(nil) + } + let writeValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + writeValueChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + let serviceArg = args[1] as! String + let characteristicArg = args[2] as! String + let valueArg = args[3] as! FlutterStandardTypedData + let bleOutputPropertyArg = args[4] is Int64 ? args[4] as! Int64 : Int64(args[4] as! Int32) + api.writeValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, value: valueArg, bleOutputProperty: bleOutputPropertyArg) { result in + switch result { + case .success: + reply(wrapResult(nil)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + writeValueChannel.setMessageHandler(nil) + } + let isPairedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + isPairedChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + api.isPaired(deviceId: deviceIdArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + isPairedChannel.setMessageHandler(nil) + } + let pairChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + pairChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + do { + try api.pair(deviceId: deviceIdArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + pairChannel.setMessageHandler(nil) + } + let unPairChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + unPairChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let deviceIdArg = args[0] as! String + do { + try api.unPair(deviceId: deviceIdArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + unPairChannel.setMessageHandler(nil) + } + let getConnectedDevicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getConnectedDevicesChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let withServicesArg = args[0] as! [String] + api.getConnectedDevices(withServices: withServicesArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getConnectedDevicesChannel.setMessageHandler(nil) + } + } +} +private class UniversalBleCallbackChannelCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return UniversalBleScanResult.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class UniversalBleCallbackChannelCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? UniversalBleScanResult { + super.writeByte(128) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class UniversalBleCallbackChannelCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return UniversalBleCallbackChannelCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return UniversalBleCallbackChannelCodecWriter(data: data) + } +} + +class UniversalBleCallbackChannelCodec: FlutterStandardMessageCodec { + static let shared = UniversalBleCallbackChannelCodec(readerWriter: UniversalBleCallbackChannelCodecReaderWriter()) +} + +/// Native -> Flutter +/// +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +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 onConnectionChanged(deviceId deviceIdArg: String, state stateArg: Int64, completion: @escaping (Result) -> Void) +} +class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol { + private let binaryMessenger: FlutterBinaryMessenger + init(binaryMessenger: FlutterBinaryMessenger){ + self.binaryMessenger = binaryMessenger + } + var codec: FlutterStandardMessageCodec { + return UniversalBleCallbackChannelCodec.shared + } + func onAvailabilityChanged(state stateArg: Int64, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([stateArg] as [Any?]) { _ in + completion(.success(Void())) + } + } + func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, error errorArg: String?, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, isPairedArg, errorArg] as [Any?]) { _ in + completion(.success(Void())) + } + } + func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([resultArg] as [Any?]) { _ in + completion(.success(Void())) + } + } + func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, characteristicIdArg, valueArg] as [Any?]) { _ in + completion(.success(Void())) + } + } + func onConnectionChanged(deviceId deviceIdArg: String, state stateArg: Int64, completion: @escaping (Result) -> Void) { + let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged", binaryMessenger: binaryMessenger, codec: codec) + channel.sendMessage([deviceIdArg, stateArg] as [Any?]) { _ in + completion(.success(Void())) + } + } +} diff --git a/darwin/Classes/UniversalBleHelper.swift b/darwin/Classes/UniversalBleHelper.swift new file mode 100644 index 0000000..a834751 --- /dev/null +++ b/darwin/Classes/UniversalBleHelper.swift @@ -0,0 +1,172 @@ +// +// UniversalBleHelper.swift +// universal_ble +// +// Created by Rohit Sangwan on 25/10/23. +// + +import CoreBluetooth +import Foundation +#if os(iOS) + import Flutter +#elseif os(OSX) + import FlutterMacOS +#endif + +enum BleInputProperty: Int { + case disabled = 0 + case notification = 1 + case indication = 2 +} + +enum BleOutputProperty: Int { + case withResponse = 0 + case withoutResponse = 1 +} + +enum BlueConnectionState: Int64 { + case connected = 0 + case disconnected = 1 +} + +enum AvailabilityState: Int64 { + case unknown = 0 + case resetting = 1 + case unsupported = 2 + case unauthorized = 3 + case poweredOff = 4 + case poweredOn = 5 +} + +enum CharacteristicProperty: Int64 { + case broadcast = 0 + case read = 1 + case writeWithoutResponse = 2 + case write = 3 + case notify = 4 + case indicate = 5 + case authenticatedSignedWrites = 6 + case extendedProperties = 7 +} + +extension CBCharacteristicProperties { + var toCharacteristicProperty: [Int64] { + var properties = [Int64]() + if contains(.broadcast) { + properties.append(CharacteristicProperty.broadcast.rawValue) + } + if contains(.read) { + properties.append(CharacteristicProperty.read.rawValue) + } + if contains(.writeWithoutResponse) { + properties.append(CharacteristicProperty.writeWithoutResponse.rawValue) + } + if contains(.write) { + properties.append(CharacteristicProperty.write.rawValue) + } + if contains(.notify) { + properties.append(CharacteristicProperty.notify.rawValue) + } + if contains(.indicate) { + properties.append(CharacteristicProperty.indicate.rawValue) + } + if contains(.authenticatedSignedWrites) { + properties.append(CharacteristicProperty.authenticatedSignedWrites.rawValue) + } + if contains(.extendedProperties) { + properties.append(CharacteristicProperty.extendedProperties.rawValue) + } + return properties + } +} + +extension CBManagerState { + func toAvailabilityState() -> AvailabilityState { + switch self { + case .unknown: + return AvailabilityState.unknown + case .resetting: + return AvailabilityState.resetting + case .unsupported: + return AvailabilityState.unsupported + case .unauthorized: + return AvailabilityState.unauthorized + case .poweredOff: + return AvailabilityState.poweredOff + case .poweredOn: + return AvailabilityState.poweredOn + @unknown default: + return AvailabilityState.unknown + } + } +} + +public extension CBUUID { + var uuidStr: String { + uuidString.lowercased() + } +} + +public extension CBPeripheral { + // FIXME: https://forums.developer.apple.com/thread/84375 + var uuid: UUID { + value(forKey: "identifier") as! NSUUID as UUID + } + + func getCharacteristic(_ characteristic: String, of service: String) -> CBCharacteristic? { + let GSS_SUFFIX = "0000-1000-8000-00805f9b34fb" + let s = services?.first { + $0.uuid.uuidStr.lowercased() == service.lowercased() || service.lowercased() == "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)".lowercased() + } + let c = s?.characteristics?.first { + $0.uuid.uuidStr.lowercased() == characteristic.lowercased() || characteristic.lowercased() == "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)".lowercased() + } + return c + } + + func setNotifiable(_ bleInputProperty: String, for characteristic: String, of service: String) { + guard let characteristic = getCharacteristic(characteristic, of: service) else { + return + } + setNotifyValue(bleInputProperty != "disabled", for: characteristic) + } +} + +// Future classes +class CharacteristicReadFuture { + let deviceId: String + let characteristicId: String + let serviceId: String? + let result: (Result) -> Void + + init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + self.deviceId = deviceId + self.characteristicId = characteristicId + self.serviceId = serviceId + self.result = result + } +} + +class CharacteristicWriteFuture { + let deviceId: String + let characteristicId: String + let serviceId: String? + let result: (Result) -> Void + + init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result) -> Void) { + self.deviceId = deviceId + self.characteristicId = characteristicId + self.serviceId = serviceId + self.result = result + } +} + +class DiscoverServicesFuture { + let deviceId: String + let result: (Result<[UniversalBleService], Error>) -> Void + + init(deviceId: String, result: @escaping (Result<[UniversalBleService], Error>) -> Void) { + self.deviceId = deviceId + self.result = result + } +} diff --git a/darwin/Classes/UniversalBlePlugin.swift b/darwin/Classes/UniversalBlePlugin.swift new file mode 100644 index 0000000..bd9d5b0 --- /dev/null +++ b/darwin/Classes/UniversalBlePlugin.swift @@ -0,0 +1,334 @@ +import CoreBluetooth + +#if os(iOS) + import Flutter + import UIKit +#elseif os(OSX) + import Cocoa + import FlutterMacOS +#endif + +public class UniversalBlePlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + var messenger: FlutterBinaryMessenger + #if os(iOS) + messenger = registrar.messenger() + #elseif os(macOS) + messenger = registrar.messenger + #endif + let callbackChannel = UniversalBleCallbackChannel(binaryMessenger: messenger) + let api = BleCentralDarwin(callbackChannel: callbackChannel) + UniversalBlePlatformChannelSetup.setUp(binaryMessenger: messenger, api: api) + } +} + +private var discoveredPeripherals = [String: CBPeripheral]() + +private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate { + var callbackChannel: UniversalBleCallbackChannel + private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil) + private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:] + private var characteristicReadFutures = [CharacteristicReadFuture]() + private var characteristicWriteFutures = [CharacteristicWriteFuture]() + private var discoverServicesFutures = [DiscoverServicesFuture]() + private var bluetoothAvailabilityStateCallback: ((Result) -> Void)? { + didSet { + oldValue?(Result.success(AvailabilityState.unknown.rawValue)) + } + } + + init(callbackChannel: UniversalBleCallbackChannel) { + self.callbackChannel = callbackChannel + super.init() + } + + func getBluetoothAvailabilityState(completion: @escaping (Result) -> Void) { + let managerState = manager.state + + if managerState == .unknown { + bluetoothAvailabilityStateCallback = completion + } else { + completion(.success(managerState.toAvailabilityState().rawValue)) + } + } + + func enableBluetooth(completion: @escaping (Result) -> Void) { + completion(Result.failure(FlutterError(code: "NotSupported", message: nil, details: nil))) + } + + func startScan() throws { + manager.scanForPeripherals(withServices: nil) + } + + func stopScan() throws { + manager.stopScan() + } + + func connect(deviceId: String) throws { + let peripheral = try deviceId.getPeripheral() + peripheral.delegate = self + manager.connect(peripheral) + } + + func disconnect(deviceId: String) throws { + let peripheral = try deviceId.getPeripheral() + if peripheral.state != CBPeripheralState.disconnected { + manager.cancelPeripheralConnection(peripheral) + } + cleanUpConnection(deviceId: deviceId) + } + + func cleanUpConnection(deviceId: String) { + characteristicReadFutures.removeAll { future in + future.deviceId == deviceId + } + discoverServicesFutures.removeAll { future in + future.deviceId == deviceId + } + discoveredServicesProgressMap[deviceId] = nil + } + + func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) { + guard let peripheral = discoveredPeripherals[deviceId] else { + completion(Result.failure( + FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + return + } + if discoveredServicesProgressMap[deviceId] != nil { + completion(Result.failure(FlutterError(code: "AlreadyInProgress", message: "Services discovery already in progress for :\(deviceId)", details: nil))) + return + } + peripheral.discoverServices(nil) + discoverServicesFutures.append(DiscoverServicesFuture(deviceId: deviceId, result: completion)) + } + + private func onServicesDiscovered(deviceId: String, services: [UniversalBleService]) { + discoverServicesFutures.removeAll { future in + if future.deviceId == deviceId { + future.result(Result.success(services)) + return true + } + return false + } + } + + func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64) throws { + let peripheral = try deviceId.getPeripheral() + + guard let c = peripheral.getCharacteristic(characteristic, of: service) else { + throw FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil) + } + + if bleInputProperty == BleInputProperty.notification.rawValue && !c.properties.contains(.notify) { + throw FlutterError(code: "InvalidAction", message: "Characteristic does not support notify", details: nil) + } + + if bleInputProperty == BleInputProperty.indication.rawValue && !c.properties.contains(.indicate) { + throw FlutterError(code: "InvalidAction", message: "Characteristic does not support indicate", details: nil) + } + + let shouldNotify = bleInputProperty != BleInputProperty.disabled.rawValue + peripheral.setNotifyValue(shouldNotify, for: c) + } + + func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result) -> Void) { + guard let peripheral = discoveredPeripherals[deviceId] else { + completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + return + } + guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { + completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil))) + return + } + if !gattCharacteristic.properties.contains(.read) { + completion(Result.failure(FlutterError(code: "InvalidAction", message: "Characteristic does not support read", details: nil))) + return + } + peripheral.readValue(for: gattCharacteristic) + characteristicReadFutures.append(CharacteristicReadFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) + } + + func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result) -> Void) { + guard let peripheral = discoveredPeripherals[deviceId] else { + completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + return + } + guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else { + completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil))) + return + } + + let type = bleOutputProperty == BleOutputProperty.withoutResponse.rawValue ? CBCharacteristicWriteType.withoutResponse : CBCharacteristicWriteType.withResponse + + if type == CBCharacteristicWriteType.withResponse { + if !gattCharacteristic.properties.contains(.write) { + completion(Result.failure(FlutterError(code: "InvalidAction", message: "Characteristic does not support write withResponse", details: nil))) + return + } + } else if type == CBCharacteristicWriteType.withoutResponse { + if !gattCharacteristic.properties.contains(.writeWithoutResponse) { + completion(Result.failure(FlutterError(code: "InvalidAction", message: "Characteristic does not support write withoutResponse", details: nil))) + return + } + } + peripheral.writeValue(value.data, for: gattCharacteristic, type: type) + + if type == CBCharacteristicWriteType.withResponse { + // Wait for future response + characteristicWriteFutures.append(CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion)) + } else { + completion(Result.success({}())) + } + } + + func requestMtu(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result) -> Void) { + guard let peripheral = discoveredPeripherals[deviceId] else { + completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil))) + return + } + let mtu = peripheral.maximumWriteValueLength(for: CBCharacteristicWriteType.withoutResponse) + let GATT_HEADER_LENGTH = 3 + let mtuResult = Int64(mtu + GATT_HEADER_LENGTH) + completion(Result.success(mtuResult)) + } + + func isPaired(deviceId _: String, completion: @escaping (Result) -> Void) { + completion(Result.failure(FlutterError(code: "NotSupported", message: nil, details: nil))) + } + + func pair(deviceId _: String) throws { + throw FlutterError(code: "NotSupported", message: nil, details: nil) + } + + func unPair(deviceId _: String) throws { + throw FlutterError(code: "NotSupported", message: nil, details: nil) + } + + func getConnectedDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) { + var filterCBUUID = withServices.map { CBUUID(string: $0) } + // We can't keep this filter empty, so adding a default filter + if filterCBUUID.isEmpty { filterCBUUID.append(CBUUID(string: "1800")) } + let bleDevices = manager.retrieveConnectedPeripherals(withServices: filterCBUUID) + bleDevices.forEach { discoveredPeripherals[$0.uuid.uuidString] = $0 } + completion(Result.success(bleDevices.map { + UniversalBleScanResult( + deviceId: $0.uuid.uuidString, + name: $0.name ?? "" + ) + })) + } + + func centralManagerDidUpdateState(_ central: CBCentralManager) { + let state = central.state.toAvailabilityState().rawValue + callbackChannel.onAvailabilityChanged(state: state) { _ in } + + bluetoothAvailabilityStateCallback?(Result.success(state)) + } + + public func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + discoveredPeripherals[peripheral.uuid.uuidString] = peripheral + let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data + callbackChannel.onScanResult(result: UniversalBleScanResult( + deviceId: peripheral.uuid.uuidString, + name: peripheral.name, + isPaired: nil, + rssi: RSSI as? Int64, + manufacturerData: FlutterStandardTypedData(bytes: manufacturerData ?? Data()) + )) { _ in } + } + + public func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { + callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, state: BlueConnectionState.connected.rawValue) { _ in } + } + + public func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error _: Error?) { + callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, state: BlueConnectionState.disconnected.rawValue) { _ in } + } + + public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) { + let deviceId = peripheral.identifier.uuidString + guard let services = peripheral.services else { + onServicesDiscovered(deviceId: deviceId, services: []) + return + } + discoveredServicesProgressMap[deviceId] = services.map { UniversalBleService(uuid: $0.uuid.uuidString, characteristics: nil) } + for service in services { + peripheral.discoverCharacteristics(nil, for: service) + } + } + + public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?) { + let deviceId = peripheral.identifier.uuidString + var universalBleCharacteristicsList: [UniversalBleCharacteristic] = [] + for characteristic in service.characteristics ?? [] { + universalBleCharacteristicsList.append( + UniversalBleCharacteristic(uuid: characteristic.uuid.uuidString, properties: characteristic.properties.toCharacteristicProperty)) + } + // Update discoveredServicesProgressMap + if let index = discoveredServicesProgressMap[deviceId]?.firstIndex(where: { $0.uuid == service.uuid.uuidString }) { + discoveredServicesProgressMap[deviceId]?[index] = UniversalBleService(uuid: service.uuid.uuidString, characteristics: universalBleCharacteristicsList) + } + // Check if all services and their characteristics have been discovered + if discoveredServicesProgressMap[deviceId]?.allSatisfy({ $0.characteristics != nil }) ?? false { + onServicesDiscovered(deviceId: deviceId, services: discoveredServicesProgressMap[deviceId] ?? []) + discoveredServicesProgressMap[deviceId] = nil + } + } + + public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { + print("peripheral:didWriteValueForCharacteristic \(characteristic.uuid.uuidStr) error: \(String(describing: error))") + // Update futures for writeValue + characteristicWriteFutures.removeAll { future in + if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { + if error != nil { + future.result(Result.failure(FlutterError(code: "WriteFailed", message: String(describing: error), details: nil))) + } else { + future.result(Result.success({}())) + } + return true + } + return false + } + } + + public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + // 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 } + } + } + + if characteristicReadFutures.count == 0 { + return + } + + // Update futures for readValue + characteristicReadFutures.removeAll { future in + if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr { + if error != nil { + future.result(Result.failure(FlutterError(code: "ReadFailed", message: String(describing: error), details: nil))) + } else { + if let characteristicValue = characteristic.value { + future.result(Result.success(FlutterStandardTypedData(bytes: characteristicValue))) + } else { + future.result(Result.failure(FlutterError(code: "ReadFailed", message: "No value", details: nil))) + } + } + return true + } + return false + } + } +} + +extension String { + func getPeripheral() throws -> CBPeripheral { + guard let peripheral = discoveredPeripherals[self] else { + throw FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil) + } + return peripheral + } +} + +extension FlutterError: Error {} diff --git a/darwin/universal_ble.podspec b/darwin/universal_ble.podspec new file mode 100644 index 0000000..bbae355 --- /dev/null +++ b/darwin/universal_ble.podspec @@ -0,0 +1,25 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint universal_ble.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'universal_ble' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.ios.dependency 'Flutter' + s.osx.dependency 'FlutterMacOS' + s.ios.deployment_target = '9.0' + s.osx.deployment_target = '10.12' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..56b8ab2 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ead455963c12b453cdb2358cad34969c76daf180" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + - platform: web + create_revision: ead455963c12b453cdb2358cad34969c76daf180 + base_revision: ead455963c12b453cdb2358cad34969c76daf180 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..ab71c46 --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# universal_ble_example + +Demonstrates how to use the universal_ble plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..88dd507 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "com.navideck.universal_ble_example" + compileSdkVersion 34 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.navideck.universal_ble_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 21 + targetSdkVersion 34 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..725af88 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/navideck/universal_ble_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/navideck/universal_ble_example/MainActivity.kt new file mode 100644 index 0000000..cf1d443 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/navideck/universal_ble_example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.navideck.universal_ble_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..f7eb7f6 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..55c4ca8 --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + plugins { + id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + } +} + +include ":app" + +apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..6397454 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - device_info_plus (0.0.1): + - Flutter + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - permission_handler_apple (9.1.1): + - Flutter + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) + +EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + universal_ble: + :path: ".symlinks/plugins/universal_ble/darwin" + +SPEC CHECKSUMS: + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + integration_test: 13825b8a9334a850581300559b8839134b124670 + permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + +PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 + +COCOAPODS: 1.14.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c0163fd --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,724 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3C020DDC565FF8D37F3235C5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E92536821956EA00892DD639 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + B2742FE9E6C1C51A78D26865 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 30B9BF32CD25D1ECB8BB5DCA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 4414252E2351EBDB7761F83A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A11EED44BE0324AD23055698 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + E92536821956EA00892DD639 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8EE4584064C335A1F66BB6D9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B2742FE9E6C1C51A78D26865 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C020DDC565FF8D37F3235C5 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 68033A1F47BF85C19A1ED513 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E92536821956EA00892DD639 /* Pods_Runner.framework */, + 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + FAF8A3758B0770B81319B3AE /* Pods */, + 68033A1F47BF85C19A1ED513 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + FAF8A3758B0770B81319B3AE /* Pods */ = { + isa = PBXGroup; + children = ( + 30B9BF32CD25D1ECB8BB5DCA /* Pods-Runner.debug.xcconfig */, + 4414252E2351EBDB7761F83A /* Pods-Runner.release.xcconfig */, + A11EED44BE0324AD23055698 /* Pods-Runner.profile.xcconfig */, + 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */, + 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */, + 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 8EE4584064C335A1F66BB6D9 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 3CE2E408302E9BF272A3BE07 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 3CE2E408302E9BF272A3BE07 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QQAZR43YM7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QQAZR43YM7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = QQAZR43YM7; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..87131a0 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..69d7afc --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Universal Ble + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + universal_ble_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSBluetoothAlwaysUsageDescription + example + NSBluetoothPeripheralUsageDescription + example + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..344ed3e --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,26 @@ +import Flutter +import UIKit +import XCTest + +@testable import universal_ble + +// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. +// +// See https://developer.apple.com/documentation/xctest for more information about using XCTest. + +class RunnerTests: XCTestCase { + + func testGetPlatformVersion() { + let plugin = UniversalBlePlugin() + + let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) + resultExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..34951eb --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,303 @@ +// ignore_for_file: use_build_context_synchronously, avoid_print + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:universal_ble_example/peripheral_detail_page.dart'; +import 'package:universal_ble_example/permission_validator.dart'; + +// Run with auto connect to : auto Scan , FindDevice, Connect, DiscoverServices +const autoConnect = bool.fromEnvironment('AUTO_CONNECT', defaultValue: false); +const deviceIdToConnect = "50:e9:f1:c2:5e:2e"; +const deviceNameFilterForAutoConnect = "MyDevice"; +const primaryColor = Colors.blue; + +void main() { + runApp( + const MaterialApp( + debugShowCheckedModeBanner: false, + home: MyApp(), + ), + ); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + StreamSubscription? _subscription; + final _scanResults = []; + AvailabilityState? bleAvailabilityState; + + List services = [ + "00001800-0000-1000-8000-00805f9b34fb", + "00002a00-0000-1000-8000-00805f9b34fb", + "00002a01-0000-1000-8000-00805f9b34fb", + "00002a19-0000-1000-8000-00805f9b34fb", + "8000cc00-cc00-ffff-ffff-ffffffffffff", + "8000dd00-dd00-ffff-ffff-ffffffffffff", + ]; + + // Set the web request options with the required services + get requestOptions => + WebRequestOptionsBuilder.acceptAllDevices(optionalServices: services); + + void initialize() { + UniversalBle.getBluetoothAvailabilityState().then((value) { + print("GetBluetoothAvailabilityState: ${value.name}"); + setState(() { + bleAvailabilityState = value; + }); + }); + + UniversalBle.onAvailabilityChange = (state) { + print("OnAvailabilityChange: ${state.name}"); + setState(() { + bleAvailabilityState = state; + }); + }; + + if (autoConnect) { + UniversalBle.startScan(webRequestOptions: requestOptions); + } + + UniversalBle.onScanResult = (result) { + if (!_scanResults.any((r) => r.deviceId == result.deviceId)) { + setState(() => _scanResults.add(result)); + } else { + setState(() { + int index = _scanResults + .indexWhere((element) => element.deviceId == result.deviceId); + if (result.name == null && _scanResults[index].name != null) { + result.name = _scanResults[index].name; + } + _scanResults[index] = result; + }); + } + if (autoConnect && + (result.deviceId == deviceIdToConnect || + (result.name?.contains(deviceNameFilterForAutoConnect) == + true))) { + print("Auto connecting to ${result.deviceId}"); + UniversalBle.stopScan(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PeripheralDetailPage(result.deviceId, true), + )); + } + }; + } + + @override + void initState() { + super.initState(); + + initialize(); + } + + @override + void dispose() { + super.dispose(); + _subscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Universal Ble'), + ), + body: Column( + children: [ + _buildButtons(), + const Divider( + color: Colors.blue, + ), + _scanResults.isEmpty + ? const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.bluetooth, + color: Colors.grey, + size: 100, + ), + ), + Text( + 'Scan For Devices', + style: TextStyle(color: Colors.grey, fontSize: 22), + ) + ], + ), + ) + : _buildListView(), + ], + ), + ); + } + + Widget _buildButtons() { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: LayoutBuilder(builder: (context, constraints) { + const tileWidth = 150; + const tileHeight = 500; + final count = constraints.maxWidth ~/ tileWidth; + return GridView.count( + crossAxisCount: count, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: tileHeight / tileWidth, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + ElevatedButton( + child: const Text('Enable Bluetooth'), + onPressed: () async { + bool isEnabled = await UniversalBle.enableBluetooth(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("BluetoothEnabled: $isEnabled"), + ), + ); + }, + ), + ElevatedButton( + child: const Text('Permissions'), + onPressed: () async { + bool hasPermissions = + await Validator.arePermissionsGranted(); + if (hasPermissions) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Permissions already granted"), + ), + ); + } + }, + ), + ElevatedButton( + child: const Text('Start Scan'), + onPressed: () { + setState(() { + _scanResults.clear(); + }); + UniversalBle.startScan(webRequestOptions: requestOptions); + }, + ), + ElevatedButton( + child: const Text('Stop Scan'), + onPressed: () { + UniversalBle.stopScan(); + }, + ), + ElevatedButton( + child: const Text('Connected Devices'), + onPressed: () async { + var devices = await UniversalBle.getConnectedDevices( + withServices: services, + ); + if (devices.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("No Connected Devices Found"), + ), + ); + } + // print(devices.map((e) => e.toJson())); + setState(() { + _scanResults.clear(); + _scanResults.addAll(devices); + }); + }, + ), + ], + ); + }), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Ble Availability : ${bleAvailabilityState?.name}', + ), + ), + IconButton( + onPressed: () { + setState(() { + _scanResults.clear(); + }); + }, + icon: const Icon(Icons.close_rounded)), + ], + ), + ], + ); + } + + Widget _buildListView() { + return Expanded( + child: ListView.separated( + itemBuilder: (context, index) { + var scanResult = _scanResults[index]; + String? name = scanResult.name; + if (name == null || name.isEmpty) { + name = 'NA'; + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Card( + child: ListTile( + title: Text( + '$name (${scanResult.rssi})', + ), + subtitle: scanResult.isPaired != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${scanResult.deviceId} "), + scanResult.isPaired == true + ? const Text( + "Paired", + style: TextStyle(color: Colors.green), + ) + : const Text( + "Not Paired", + style: TextStyle(color: Colors.red), + ), + ], + ) + : Text(scanResult.deviceId), + trailing: const Icon(Icons.arrow_forward_ios), + onTap: () { + print("$name: ${scanResult.deviceId}"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + PeripheralDetailPage(scanResult.deviceId, false), + )); + }, + ), + ), + ); + }, + separatorBuilder: (context, index) => const Divider(), + itemCount: _scanResults.length, + ), + ); + } +} diff --git a/example/lib/peripheral_detail_page.dart b/example/lib/peripheral_detail_page.dart new file mode 100644 index 0000000..c258749 --- /dev/null +++ b/example/lib/peripheral_detail_page.dart @@ -0,0 +1,662 @@ +// ignore_for_file: avoid_print, depend_on_referenced_packages + +import 'dart:convert'; + +import 'package:convert/convert.dart'; +import 'package:expandable/expandable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:universal_ble_example/main.dart'; + +String gssUuid(String code) => '0000$code-0000-1000-8000-00805f9b34fb'; + +class PeripheralDetailPage extends StatefulWidget { + final String deviceId; + final bool autoConnect; + const PeripheralDetailPage(this.deviceId, this.autoConnect, {Key? key}) + : super(key: key); + + @override + State createState() { + return _PeripheralDetailPageState(); + } +} + +class _PeripheralDetailPageState extends State { + bool isConnected = false; + bool parseValue = true; + GlobalKey uuidFormKey = GlobalKey(); + GlobalKey valueFormKey = GlobalKey(); + + List discoveredServices = []; + ExpandableController expandableController = + ExpandableController(initialExpanded: autoConnect); + List results = []; + + @override + void initState() { + super.initState(); + UniversalBle.onConnectionChanged = _handleConnectionChange; + UniversalBle.onValueChanged = _handleValueChange; + UniversalBle.onPairStateChange = + (String deviceId, bool isPaired, String? error) { + print('OnPairStateChange $deviceId, $isPaired'); + setState(() { + if (error != null) { + results.add("PairStateChangeError (Paired: $isPaired): $error "); + } else { + results.add('PairStateChange: $isPaired'); + } + }); + }; + + if (autoConnect) { + UniversalBle.connect(widget.deviceId); + } + } + + @override + void dispose() { + super.dispose(); + UniversalBle.onConnectionChanged = null; + UniversalBle.onValueChanged = null; + + // Disconnect when leaving the page + if (isConnected) UniversalBle.disconnect(widget.deviceId); + } + + void _handleConnectionChange(String deviceId, BleConnectionState state) { + print('_handleConnectionChange $deviceId, ${state.name}'); + setState(() { + if (deviceId == widget.deviceId) { + isConnected = (state == BleConnectionState.connected); + } + results.add('Connection: ${state.name.toUpperCase()}'); + }); + + // Auto Discover Services + if (isConnected) { + discoverServices(); + } else if (autoConnect && !isConnected) { + print("Trying to reconnect"); + UniversalBle.connect(widget.deviceId); + } + } + + void discoverServices() async { + var services = await UniversalBle.discoverServices(widget.deviceId); + print('${services.length} services discovered'); + discoveredServices.clear(); + setState(() { + discoveredServices = services; + }); + } + + void _handleValueChange( + String deviceId, String characteristicId, Uint8List value) { + String s = String.fromCharCodes(value); + String data = + parseValue ? '$s\nraw : ${value.toString()}' : value.toString(); + print('_handleValueChange $deviceId, $characteristicId, $s'); + setState(() { + results.add("Value: $data"); + }); + } + + Uint8List? _getWriteValue() { + if (!uuidFormKey.currentState!.validate() || + !valueFormKey.currentState!.validate()) { + return null; + } + + if (binaryCode.text.isEmpty) { + print("Error: No value to write"); + return null; + } + List? value; + String text = binaryCode.text; + if (text.contains(",") || text.contains("[") || text.contains("]")) { + text = text.replaceAll("[", "").replaceAll("]", "").trim(); + value = text.split(',').map(int.parse).toList(); + } else { + try { + value = hex.decode(text); + } catch (e) { + print("Error parsing hex $e"); + print("Trying utf8 encoding"); + value = utf8.encode(text); + } + } + return Uint8List.fromList(value); + } + + final serviceUUID = TextEditingController(); + final characteristicUUID = TextEditingController(); + final binaryCode = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Peripheral Details'), + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.circle, + color: isConnected ? Colors.greenAccent : Colors.red, + size: 20, + ), + ) + ], + ), + body: LayoutBuilder(builder: (context, constraints) { + bool isDesktop = constraints.maxWidth > 1000; + return Row( + children: [ + if (isDesktop) + Expanded( + flex: 1, + child: Container( + color: Colors.grey[300], + child: discoveredServices.isEmpty + ? const Center( + child: Text('No Services Discovered'), + ) + : ServicesView( + discoveredServices: discoveredServices, + scrollable: true, + onTap: (BleService service, + BleCharacteristic characteristic) { + serviceUUID.text = service.uuid; + characteristicUUID.text = characteristic.uuid; + print( + characteristic.properties.map((e) => e.name), + ); + }, + ), + ), + ), + Expanded( + flex: 3, + child: Align( + alignment: Alignment.topCenter, + child: SingleChildScrollView( + child: Form( + key: uuidFormKey, + child: Column( + children: [ + // Top buttons + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + PlatformButton( + text: 'Connect', + enabled: !isConnected, + onPressed: () async { + try { + await UniversalBle.connect(widget.deviceId); + } catch (e) { + print(e); + setState(() { + results.add('ConnectError : $e'); + }); + } + }, + ), + PlatformButton( + text: 'Disconnect', + enabled: isConnected, + onPressed: () { + UniversalBle.disconnect(widget.deviceId); + }, + ), + ], + ), + ), + const Divider(), + + // Text Fields + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: serviceUUID, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a service uuid'; + } + return null; + }, + decoration: const InputDecoration( + labelText: 'ServiceUUID', + border: OutlineInputBorder(), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: characteristicUUID, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a Characteristic uuid'; + } + return null; + }, + decoration: const InputDecoration( + labelText: 'CharacteristicUUID', + border: OutlineInputBorder(), + ), + ), + ), + + Form( + key: valueFormKey, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + flex: 5, + child: TextFormField( + controller: binaryCode, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + decoration: const InputDecoration( + labelText: 'Enter Value ', + hintText: + "Enter Value (String, Hex or List)", + border: OutlineInputBorder(), + ), + ), + ), + Switch( + value: parseValue, + onChanged: (value) { + setState(() { + parseValue = value; + }); + }), + ], + ), + ), + ), + + const Divider(), + + // Buttons + Padding( + padding: const EdgeInsets.all(8.0), + child: LayoutBuilder(builder: (context, constraints) { + const tileWidth = 150; + const tileHeight = 500; + final count = constraints.maxWidth ~/ tileWidth; + return GridView.count( + crossAxisCount: count, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: tileHeight / tileWidth, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + PlatformButton( + enabled: isConnected, + onPressed: () { + Uint8List? value = _getWriteValue(); + if (value == null) return; + print("Writing $value"); + UniversalBle.writeValue( + widget.deviceId, + serviceUUID.text, + characteristicUUID.text, + value, + BleOutputProperty.withResponse); + }, + text: 'Write', + ), + PlatformButton( + enabled: isConnected, + onPressed: () async { + if (!uuidFormKey.currentState!.validate()) { + return; + } + try { + Uint8List value = + await UniversalBle.readValue( + widget.deviceId, + serviceUUID.text, + characteristicUUID.text); + String s = String.fromCharCodes(value); + String data = parseValue + ? '$s\nraw : ${value.toString()}' + : value.toString(); + setState(() { + results.add('Read : $data'); + }); + } catch (e) { + print(e); + setState(() { + results.add('ReadError : $e'); + }); + } + }, + text: 'Read', + ), + PlatformButton( + enabled: isConnected, + onPressed: () async { + int mtu = await UniversalBle.requestMtu( + widget.deviceId, 247); + setState(() { + results.add('MTU : $mtu'); + }); + }, + text: 'Request Mtu', + ), + PlatformButton( + enabled: isConnected, + onPressed: () { + if (!uuidFormKey.currentState!.validate()) { + return; + } + BleCharacteristic? characteristic; + + for (BleService service + in discoveredServices) { + for (BleCharacteristic c + in service.characteristics) { + if (c.uuid == characteristicUUID.text) { + characteristic = c; + break; + } + } + } + BleInputProperty inputProperty = + BleInputProperty.notification; + if (characteristic != null) { + if (characteristic.properties.contains( + CharacteristicProperty.indicate)) { + inputProperty = + BleInputProperty.indication; + } else if (characteristic.properties + .contains( + CharacteristicProperty.notify)) { + inputProperty = + BleInputProperty.notification; + } else { + print('No notify or indicate property'); + return; + } + } + print(inputProperty); + UniversalBle.setNotifiable( + widget.deviceId, + serviceUUID.text, + characteristicUUID.text, + inputProperty, + ); + }, + text: 'Notify/Indicate', + ), + PlatformButton( + enabled: isConnected, + onPressed: () { + if (!uuidFormKey.currentState!.validate()) { + return; + } + UniversalBle.setNotifiable( + widget.deviceId, + serviceUUID.text, + characteristicUUID.text, + BleInputProperty.disabled, + ); + }, + text: 'Cancel Notify', + ), + PlatformButton( + onPressed: () async { + discoverServices(); + }, + enabled: isConnected, + text: 'Discover Services', + ), + PlatformButton( + onPressed: () async { + await UniversalBle.pair(widget.deviceId); + }, + text: 'Pair', + ), + PlatformButton( + onPressed: () async { + bool isPaired = await UniversalBle.isPaired( + widget.deviceId); + setState(() { + results.add('IsPaired : $isPaired'); + }); + }, + text: 'IsPaired', + ), + PlatformButton( + onPressed: () async { + await UniversalBle.unPair(widget.deviceId); + }, + text: 'UnPair', + ), + PlatformButton( + onPressed: () async { + setState(() { + results.clear(); + discoveredServices.clear(); + }); + }, + text: 'Clear', + ), + ], + ); + }), + ), + + // Results , Services + if (!isDesktop) + Column( + children: [ + ResultView( + results: results, + onClearTap: (int? index) { + setState(() { + if (index != null) { + results.removeAt(index); + } else { + results.clear(); + } + }); + }), + const Divider(), + ServicesView( + discoveredServices: discoveredServices, + onTap: (BleService service, + BleCharacteristic characteristic) { + serviceUUID.text = service.uuid; + characteristicUUID.text = characteristic.uuid; + print( + characteristic.properties + .map((e) => e.name), + ); + }, + ), + ], + ) + ], + ), + ), + ), + ), + ), + + // For Desktop + if (isDesktop) + Expanded( + flex: 1, + child: Container( + color: Colors.grey[300], + child: results.isEmpty + ? const Center( + child: Text('No results'), + ) + : ResultView( + results: results, + scrollable: true, + onClearTap: (int? index) { + setState(() { + if (index != null) { + results.removeAt(index); + } else { + results.clear(); + } + }); + }), + )), + ], + ); + }), + ); + } +} + +class ResultView extends StatelessWidget { + final List results; + final bool scrollable; + final Function(int? index) onClearTap; + const ResultView({ + required this.results, + required this.onClearTap, + this.scrollable = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: !scrollable, + physics: scrollable ? null : const NeverScrollableScrollPhysics(), + itemCount: results.length, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + child: ListTile( + onTap: () { + onClearTap(index); + }, + title: Text(results[index]), + trailing: const Icon(Icons.clear), + ), + ), + ); + }, + ); + } +} + +class ServicesView extends StatelessWidget { + final List discoveredServices; + final bool scrollable; + + final Function(BleService service, BleCharacteristic characteristic)? onTap; + const ServicesView({ + super.key, + required this.discoveredServices, + this.onTap, + this.scrollable = false, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: !scrollable, + physics: scrollable ? null : const NeverScrollableScrollPhysics(), + itemCount: discoveredServices.length, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + child: ExpandablePanel( + header: Container( + color: scrollable ? Colors.lime : Colors.grey[300], + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + const Icon(Icons.arrow_forward_ios), + Expanded(child: Text(discoveredServices[index].uuid)), + ], + ), + ), + ), + collapsed: const SizedBox(), + expanded: Column( + children: discoveredServices[index] + .characteristics + .map((e) => Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + onTap?.call(discoveredServices[index], e); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.arrow_right_outlined), + Expanded(child: Text(e.uuid)), + ], + ), + Text( + "Properties: ${e.properties.map((e) => e.name)}", + ) + ], + ), + ), + ], + ), + )) + .toList(), + ), + ), + ), + ); + }, + ); + } +} + +class PlatformButton extends StatelessWidget { + final String text; + final Function()? onPressed; + final bool enabled; + const PlatformButton({ + required this.text, + required this.onPressed, + this.enabled = true, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Text(text), + ); + } +} diff --git a/example/lib/permission_validator.dart b/example/lib/permission_validator.dart new file mode 100644 index 0000000..99f5532 --- /dev/null +++ b/example/lib/permission_validator.dart @@ -0,0 +1,102 @@ +// ignore_for_file: avoid_print + +import 'dart:async'; +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class Validator { + /* + Required Permissions : + <-----------> + IOS : + - Bluetooth + <-----------> + Android : + if AndroidVersions < 12 + - Location + - Bluetooth + else + - Bluetooth Scan + - Bluetooth Connect + <-----------> + Macos : + <-----------> + Windows : None + <-----------> + Linux : None + <-----------> + Web : + Check if Browser Supports Bluetooth + */ + + static Future arePermissionsGranted() async { + if (!isMobilePlatform) return true; + + var status = await _permissionStatus; + bool blePermissionGranted = status[0]; + bool locationPermissionGranted = status[1]; + + if (locationPermissionGranted && blePermissionGranted) return true; + + if (!blePermissionGranted) { + PermissionStatus blePermissionCheck = + await Permission.bluetooth.request(); + if (blePermissionCheck.isPermanentlyDenied) { + print("Bluetooth Permission Permanently Denied"); + openAppSettings(); + } + return false; + } + + if (!locationPermissionGranted) { + PermissionStatus locationPermissionCheck = + await Permission.location.request(); + if (locationPermissionCheck.isPermanentlyDenied) { + print("Location Permission Permanently Denied"); + openAppSettings(); + } + return false; + } + + return false; + } + + static Future> get _permissionStatus async { + bool blePermissionGranted = false; + bool locationPermissionGranted = false; + + if (await requiresExplicitAndroidBluetoothPermissions) { + bool bleConnectPermission = + (await Permission.bluetoothConnect.request()).isGranted; + bool bleScanPermission = + (await Permission.bluetoothScan.request()).isGranted; + + blePermissionGranted = bleConnectPermission && bleScanPermission; + locationPermissionGranted = true; + } else { + PermissionStatus permissionStatus = await Permission.bluetooth.request(); + blePermissionGranted = permissionStatus.isGranted; + locationPermissionGranted = await requiresLocationPermission + ? (await Permission.locationWhenInUse.request()).isGranted + : true; + } + return [locationPermissionGranted, blePermissionGranted]; + } + + static bool get isMobilePlatform => + !kIsWeb && (Platform.isAndroid || Platform.isIOS); + + static Future get requiresLocationPermission async => + !kIsWeb && + Platform.isAndroid && + (!await requiresExplicitAndroidBluetoothPermissions); + + static Future get requiresExplicitAndroidBluetoothPermissions async { + if (kIsWeb || !Platform.isAndroid) return false; + AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo; + return androidInfo.version.sdkInt >= 31; + } +} diff --git a/example/linux/.gitignore b/example/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt new file mode 100644 index 0000000..b375099 --- /dev/null +++ b/example/linux/CMakeLists.txt @@ -0,0 +1,141 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "universal_ble_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.navideck.universal_ble") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Enable the test target. +set(include_universal_ble_tests TRUE) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc new file mode 100644 index 0000000..a3e4f92 --- /dev/null +++ b/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "universal_ble_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "universal_ble_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..b02da4c --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import device_info_plus +import universal_ble + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..c795730 --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,43 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..7cabaf3 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,29 @@ +PODS: + - device_info_plus (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`) + +EXTERNAL SOURCES: + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + FlutterMacOS: + :path: Flutter/ephemeral + universal_ble: + :path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin + +SPEC CHECKSUMS: + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 + +COCOAPODS: 1.14.3 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..57008be --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,791 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 54A7291550390DBA7F60B398 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 095AF621C9B1EE27B63D82D1 /* Pods_RunnerTests.framework */; }; + BFFAEE386BA8C5023F9983AE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F517363F4B1C144EC77DADCD /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 095AF621C9B1EE27B63D82D1 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 32ED472C70D6ED7C3CD8CE8B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* universal_ble_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = universal_ble_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 65CCC8CF23B15CA6818A0496 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BF9EC7C1208428926C8C045E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + C5F6162E162B944885A4C57E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D8A7E0EE9000B69937E2CAAF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F517363F4B1C144EC77DADCD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FF25EBD7FA998BE27A25C911 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 54A7291550390DBA7F60B398 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BFFAEE386BA8C5023F9983AE /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 918B2BA59DA01E445579435F /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* universal_ble_example.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 918B2BA59DA01E445579435F /* Pods */ = { + isa = PBXGroup; + children = ( + FF25EBD7FA998BE27A25C911 /* Pods-Runner.debug.xcconfig */, + 65CCC8CF23B15CA6818A0496 /* Pods-Runner.release.xcconfig */, + 32ED472C70D6ED7C3CD8CE8B /* Pods-Runner.profile.xcconfig */, + D8A7E0EE9000B69937E2CAAF /* Pods-RunnerTests.debug.xcconfig */, + BF9EC7C1208428926C8C045E /* Pods-RunnerTests.release.xcconfig */, + C5F6162E162B944885A4C57E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F517363F4B1C144EC77DADCD /* Pods_Runner.framework */, + 095AF621C9B1EE27B63D82D1 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 0B773D64B309FE8B1C43C096 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 4DACA89469123DBF706A6DEC /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + DB73DCCDE8FAA136C11B0BD8 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* universal_ble_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1430; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0B773D64B309FE8B1C43C096 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 4DACA89469123DBF706A6DEC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DB73DCCDE8FAA136C11B0BD8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D8A7E0EE9000B69937E2CAAF /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BF9EC7C1208428926C8C045E /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C5F6162E162B944885A4C57E /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/universal_ble_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/universal_ble_example"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c60a4dd --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..d550e14 --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = universal_ble_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.navideck. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..14a39b7 --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.device.bluetooth + + com.apple.security.network.server + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 0000000..a902218 --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + NSBluetoothAlwaysUsageDescription + example + NSBluetoothPeripheralUsageDescription + example + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..c63c951 --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.bluetooth + + com.apple.security.network.server + + + diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..e0c61cb --- /dev/null +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,27 @@ +import FlutterMacOS +import Cocoa +import XCTest + +@testable import universal_ble + +// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. +// +// See https://developer.apple.com/documentation/xctest for more information about using XCTest. + +class RunnerTests: XCTestCase { + + func testGetPlatformVersion() { + let plugin = UniversalBlePlugin() + + let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertEqual(result as! String, + "macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + resultExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..a3068f8 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,440 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: bfd004c81e3de0f06dce8580bc39a4600e4a6efe465a866b31d4d954c9f356aa + url: "https://pub.dev" + source: hosted + version: "0.8.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + expandable: + dependency: "direct main" + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: "0823eabf8643fb9adbf0d074b117a6b1357b312f6c0a34bd76cef0cd34b7df54" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + url: "https://pub.dev" + source: hosted + version: "1.10.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + url: "https://pub.dev" + source: hosted + version: "10.4.5" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + url: "https://pub.dev" + source: hosted + version: "10.3.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + universal_ble: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" + web: + dependency: transitive + description: + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" +sdks: + dart: ">=3.2.0-194.0.dev <4.0.0" + flutter: ">=3.3.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..2880541 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: universal_ble_example +description: Demonstrates how to use the universal_ble plugin. +version: 1.0.0+1 +publish_to: "none" + +environment: + sdk: ">=3.1.3 <4.0.0" + +dependencies: + flutter: + sdk: flutter + expandable: ^5.0.1 + cupertino_icons: ^1.0.2 + permission_handler: ^10.4.3 + device_info_plus: ^9.0.2 + universal_ble: + path: ../ + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..45cf2ca --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/example/windows/.gitignore b/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt new file mode 100644 index 0000000..0937d82 --- /dev/null +++ b/example/windows/CMakeLists.txt @@ -0,0 +1,104 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(universal_ble_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "universal_ble_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Enable the test target. +set(include_universal_ble_tests TRUE) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..3860860 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UniversalBlePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); +} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..3dcad9f --- /dev/null +++ b/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows + universal_ble +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc new file mode 100644 index 0000000..23c0d5c --- /dev/null +++ b/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.navideck" "\0" + VALUE "FileDescription", "universal_ble_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "universal_ble_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.navideck. All rights reserved." "\0" + VALUE "OriginalFilename", "universal_ble_example.exe" "\0" + VALUE "ProductName", "universal_ble_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp new file mode 100644 index 0000000..f2b1e45 --- /dev/null +++ b/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"universal_ble_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/example/windows/runner/resources/app_icon.ico differ diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp new file mode 100644 index 0000000..b2b0873 --- /dev/null +++ b/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/lib/src/models/availability_state.dart b/lib/src/models/availability_state.dart new file mode 100644 index 0000000..321fbbf --- /dev/null +++ b/lib/src/models/availability_state.dart @@ -0,0 +1,14 @@ +enum AvailabilityState { + unknown(0), + resetting(1), + unsupported(2), + unauthorized(3), + poweredOff(4), + poweredOn(5); + + final int value; + const AvailabilityState(this.value); + + factory AvailabilityState.parse(int value) => + AvailabilityState.values.firstWhere((element) => element.value == value); +} diff --git a/lib/src/models/ble_connection_state.dart b/lib/src/models/ble_connection_state.dart new file mode 100644 index 0000000..bfb52f4 --- /dev/null +++ b/lib/src/models/ble_connection_state.dart @@ -0,0 +1,10 @@ +enum BleConnectionState { + connected(0), + disconnected(1); + + final int value; + const BleConnectionState(this.value); + + factory BleConnectionState.parse(int value) => + BleConnectionState.values.firstWhere((element) => element.value == value); +} diff --git a/lib/src/models/ble_property.dart b/lib/src/models/ble_property.dart new file mode 100644 index 0000000..8d71192 --- /dev/null +++ b/lib/src/models/ble_property.dart @@ -0,0 +1,22 @@ +enum BleInputProperty { + disabled(0), + notification(1), + indication(2); + + final int value; + const BleInputProperty(this.value); + + factory BleInputProperty.parse(int value) => + BleInputProperty.values.firstWhere((element) => element.value == value); +} + +enum BleOutputProperty { + withResponse(0), + withoutResponse(1); + + final int value; + const BleOutputProperty(this.value); + + factory BleOutputProperty.parse(int value) => + BleOutputProperty.values.firstWhere((element) => element.value == value); +} diff --git a/lib/src/models/ble_scan_result.dart b/lib/src/models/ble_scan_result.dart new file mode 100644 index 0000000..1fe8e51 --- /dev/null +++ b/lib/src/models/ble_scan_result.dart @@ -0,0 +1,22 @@ +import 'dart:typed_data'; + +class BleScanResult { + String deviceId; + String? name; + bool? isPaired; + Uint8List? manufacturerDataHead; + Uint8List? manufacturerData; + int? rssi; + + BleScanResult({ + required this.name, + required this.deviceId, + this.rssi, + this.isPaired, + Uint8List? manufacturerData, + Uint8List? manufacturerDataHead, + }) { + this.manufacturerDataHead = manufacturerDataHead ?? Uint8List.fromList([]); + this.manufacturerData = manufacturerData ?? manufacturerDataHead; + } +} diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart new file mode 100644 index 0000000..a0a2fe8 --- /dev/null +++ b/lib/src/models/ble_service.dart @@ -0,0 +1,29 @@ +class BleService { + String uuid; + List characteristics; + BleService(this.uuid, this.characteristics); +} + +class BleCharacteristic { + String uuid; + List properties; + BleCharacteristic(this.uuid, this.properties); +} + +enum CharacteristicProperty { + broadcast(0), + read(1), + writeWithoutResponse(2), + write(3), + notify(4), + indicate(5), + authenticatedSignedWrites(6), + extendedProperties(7); + + final int value; + const CharacteristicProperty(this.value); + + factory CharacteristicProperty.parse(int value) => + CharacteristicProperty.values + .firstWhere((element) => element.value == value); +} diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart new file mode 100644 index 0000000..85ef3fc --- /dev/null +++ b/lib/src/models/model_exports.dart @@ -0,0 +1,6 @@ +export 'package:universal_ble/src/models/web_request_options_builder.dart'; +export 'package:universal_ble/src/models/ble_property.dart'; +export 'package:universal_ble/src/models/ble_service.dart'; +export 'package:universal_ble/src/models/availability_state.dart'; +export 'package:universal_ble/src/models/ble_connection_state.dart'; +export 'package:universal_ble/src/models/ble_scan_result.dart'; diff --git a/lib/src/models/web_request_options_builder.dart b/lib/src/models/web_request_options_builder.dart new file mode 100644 index 0000000..f017e7c --- /dev/null +++ b/lib/src/models/web_request_options_builder.dart @@ -0,0 +1,96 @@ +import 'dart:typed_data'; +import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; + +class WebRequestOptionsBuilder { + final bool _acceptAllDevices; + final List _requestFilters; + final List? _exclusionFilters; + final List? _optionalServices; + + WebRequestOptionsBuilder( + List requestFilters, { + List? exclusionFilters, + List? optionalServices, + }) : _requestFilters = requestFilters, + _exclusionFilters = exclusionFilters, + _acceptAllDevices = false, + _optionalServices = optionalServices { + if (_requestFilters.isEmpty) { + throw StateError( + 'No filters have been set, consider using ' + 'RequestOptionsBuilder.acceptAllDevices() instead.', + ); + } + } + + /// To accept all devices + WebRequestOptionsBuilder.acceptAllDevices({ + List? optionalServices, + }) : _acceptAllDevices = true, + _requestFilters = [], + _exclusionFilters = null, + _optionalServices = optionalServices; + + /// For internal use + /// To convert UniversalBleRequestOptions to FlutterWebBluetoothRequestOptions + RequestOptionsBuilder toRequestOptionsBuilder() { + if (_acceptAllDevices) { + return RequestOptionsBuilder.acceptAllDevices( + optionalServices: _optionalServices, + ); + } else { + return RequestOptionsBuilder( + _requestFilters.map((e) => e.getRequestFilterBuilder()).toList(), + exclusionFilters: + _exclusionFilters?.map((e) => e.getRequestFilterBuilder()).toList(), + optionalServices: _optionalServices, + ); + } + } +} + +/// To filter by name, namePrefix, services, manufacturerData +class WebRequestFilterBuilder { + String? name; + String? namePrefix; + List? services; + List? manufacturerData; + + WebRequestFilterBuilder({ + this.name, + this.namePrefix, + this.services, + this.manufacturerData, + }); + + RequestFilterBuilder getRequestFilterBuilder() { + return RequestFilterBuilder( + name: name, + namePrefix: namePrefix, + services: services, + manufacturerData: manufacturerData + ?.map((e) => e.getManufacturerDataFilterBuilder()) + .toList(), + ); + } +} + +/// To filter by manufacturer data +class WebManufacturerDataFilterBuilder { + int? companyIdentifier; + Uint8List? dataPrefix; + Uint8List? mask; + WebManufacturerDataFilterBuilder({ + this.companyIdentifier, + this.dataPrefix, + this.mask, + }); + + ManufacturerDataFilterBuilder getManufacturerDataFilterBuilder() { + return ManufacturerDataFilterBuilder( + companyIdentifier: companyIdentifier, + dataPrefix: dataPrefix, + mask: mask, + ); + } +} diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart new file mode 100644 index 0000000..67d7a81 --- /dev/null +++ b/lib/src/universal_ble.dart @@ -0,0 +1,179 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:universal_ble/src/universal_ble_linux/universal_ble_linux.dart'; +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart'; +import 'package:universal_ble/src/universal_ble_web/universal_ble_web.dart'; +import 'package:universal_ble/universal_ble.dart'; + +class UniversalBle { + /// Get platform specific implementation + static UniversalBlePlatform _platform = _defaultPlatform(); + + /// Set custom platform specific implementation (e.g. for testing) + static void setInstance(UniversalBlePlatform instance) => + _platform = instance; + + /// To get Bluetooth state availability + /// To get updates, set [onAvailabilityChange] listener + static Future getBluetoothAvailabilityState() async { + return await _platform.getBluetoothAvailabilityState(); + } + + /// To Start scan, get scan results in [onScanResult] listener + /// might throw errors if Bluetooth is not available + /// `webRequestOptions` supported on Web only + static Future startScan({ + WebRequestOptionsBuilder? webRequestOptions, + }) async { + await _platform.startScan(webRequestOptions: webRequestOptions); + } + + /// To Stop scan, set [onScanResult] listener to `null` if you don't need it anymore + /// might throw errors if Bluetooth is not available + static Future stopScan() async { + await _platform.stopScan(); + } + + /// To connect to a device, get connection state in [onConnectionChanged] listener + /// preferred to stop scan before connecting + /// might throw errors if device is not connectable + /// `connectionTimeout` supported on Web only + static Future connect( + String deviceId, { + Duration? connectionTimeout, + }) async { + await _platform.connect(deviceId, connectionTimeout: connectionTimeout); + } + + /// To disconnect from a device, get connection state in [onConnectionChanged] listener + static Future disconnect(String deviceId) async { + await _platform.disconnect(deviceId); + } + + /// To discover services of a device + static Future> discoverServices(String deviceId) async { + return await _platform.discoverServices(deviceId); + } + + /// To set a characteristic notifiable, set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication], get updates in [onValueChanged] listener + /// To stop listening to a characteristic, set `bleInputProperty` to [BleInputProperty.disabled] + static Future setNotifiable( + String deviceId, + String service, + String characteristic, + BleInputProperty bleInputProperty, + ) async { + await _platform.setNotifiable( + deviceId, + service, + characteristic, + bleInputProperty, + ); + } + + /// To read a characteristic value + /// on iOS and MacOS, this method will also trigger [onValueChanged] listener + static Future readValue( + String deviceId, + String service, + String characteristic, + ) async { + return await _platform.readValue(deviceId, service, characteristic); + } + + /// To write a characteristic value + /// To write a characteristic value with response, set `bleOutputProperty` to [BleOutputProperty.withResponse] + static Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty, + ) async { + await _platform.writeValue( + deviceId, + service, + characteristic, + value, + bleOutputProperty, + ); + } + + /// `requestMtu` not supported on `Linux` and `Web + static Future requestMtu(String deviceId, int expectedMtu) async { + return await _platform.requestMtu(deviceId, expectedMtu); + } + + /// Pair methods are not supported on `iOS`, `MacOS` and `Web` + static Future isPaired(String deviceId) async { + return await _platform.isPaired(deviceId); + } + + /// To trigger pair request, might throw errors if device is already paired + static Future pair(String deviceId) async { + await _platform.pair(deviceId); + } + + /// To trigger unPair request, might throw errors if device is not paired + static Future unPair(String deviceId) async { + await _platform.unPair(deviceId); + } + + /// To get connected devices to the system ( connected by any app ) + /// use [withServices] to filter devices by services + /// on `iOS`, `MacOS` [withServices] is required to get connected devices, else [1800] service will be used as default filter + /// on `Android`, `Linux` and `Windows`, if [withServices] is used, then internally all services will be discovered for each device first (either by connecting or by using cached services) + /// Not supported on `Web` + static Future> getConnectedDevices({ + List? withServices, + }) async { + return await _platform.getConnectedDevices(withServices); + } + + /// Enabling Bluetooth, might throw errors if Bluetooth is not available + /// Not supported on `Web` and `Apple` + static Future enableBluetooth() async { + return await _platform.enableBluetooth(); + } + + /// To get Bluetooth state availability + static set onAvailabilityChange(OnAvailabilityChange? onAvailabilityChange) => + _platform.onAvailabilityChange = onAvailabilityChange; + + /// To get scan results + static set onScanResult(OnScanResult? onScanResult) => + _platform.onScanResult = onScanResult; + + /// To get connection state changes + static set onConnectionChanged(OnConnectionChanged? onConnectionChanged) => + _platform.onConnectionChanged = onConnectionChanged; + + /// To get characteristic value updates, set `bleInputProperty` in [setNotifiable] to [BleInputProperty.notification] or [BleInputProperty.indication] + static set onValueChanged(OnValueChanged? onValueChanged) => + _platform.onValueChanged = onValueChanged; + + /// To get pair state changes, + static set onPairStateChange(OnPairStateChange pairStateChange) => + _platform.onPairStateChange = pairStateChange; + + static UniversalBlePlatform _defaultPlatform() { + if (kIsWeb) return UniversalBleWeb.instance; + if (Platform.isLinux) return UniversalBleLinux.instance; + return UniversalBlePigeonChannel.instance; + } +} + +// Callback types +typedef OnConnectionChanged = void Function( + String deviceId, BleConnectionState state); + +typedef OnValueChanged = void Function( + String deviceId, String characteristicId, Uint8List value); + +typedef OnScanResult = void Function(BleScanResult scanResult); + +typedef OnAvailabilityChange = void Function(AvailabilityState state); + +typedef OnPairStateChange = void Function( + String deviceId, bool isPaired, String? error); diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart new file mode 100644 index 0000000..71dc363 --- /dev/null +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -0,0 +1,396 @@ +// ignore_for_file: avoid_print + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:bluez/bluez.dart'; +import 'package:collection/collection.dart'; +import 'package:universal_ble/src/models/model_exports.dart'; +import 'package:universal_ble/src/universal_ble_platform_interface.dart'; + +class UniversalBleLinux extends UniversalBlePlatform { + UniversalBleLinux._(); + static UniversalBleLinux? _instance; + static UniversalBleLinux get instance => _instance ??= UniversalBleLinux._(); + + bool isInitialized = false; + + final BlueZClient _client = BlueZClient(); + + BlueZAdapter? _activeAdapter; + final Map _devices = {}; + final Map _deviceStreamSubscriptions = {}; + final Map _characteristicPropertiesSubscriptions = + {}; + + @override + Future getBluetoothAvailabilityState() async { + await _ensureInitialized(); + BlueZAdapter? adapter = _activeAdapter; + if (adapter == null) { + return AvailabilityState.unsupported; + } + return adapter.powered + ? AvailabilityState.poweredOn + : AvailabilityState.poweredOff; + } + + @override + Future enableBluetooth() async { + await _ensureInitialized(); + if (_activeAdapter?.powered == true) return true; + await _activeAdapter?.setPowered(true); + return _activeAdapter?.powered ?? false; + } + + @override + Future startScan({ + WebRequestOptionsBuilder? webRequestOptions, + }) async { + await _ensureInitialized(); + + if (!_activeAdapter!.discovering) { + _activeAdapter!.startDiscovery(); + _client.devices.forEach(_onDeviceAdd); + } + } + + @override + Future stopScan() async { + await _ensureInitialized(); + var adapter = _activeAdapter; + if (adapter != null && adapter.discovering) { + adapter.stopDiscovery(); + } + } + + @override + Future connect(String deviceId, {Duration? connectionTimeout}) async { + await _findDeviceById(deviceId).connect(); + } + + @override + Future disconnect(String deviceId) async { + await _findDeviceById(deviceId).disconnect(); + } + + @override + Future> discoverServices(String deviceId) async { + var device = _findDeviceById(deviceId); + if (!device.servicesResolved) { + await device.propertiesChanged + .firstWhere( + (element) => element.contains(BluezProperty.servicesResolved)) + .timeout(const Duration(seconds: 2), onTimeout: () => []); + } + + // while (!device.servicesResolved) { + // await Future.delayed(const Duration(seconds: 200)); + // } + + List services = []; + for (var service in device.gattServices) { + var characteristics = service.characteristics.map((e) { + var properties = List.from(e.flags + .map((e) => e.toCharacteristicProperty()) + .where((element) => element != null) + .toList()); + return BleCharacteristic(e.uuid.toString(), properties); + }).toList(); + services.add(BleService(service.uuid.toString(), characteristics)); + } + return services; + } + + BlueZGattCharacteristic _getCharacteristic( + String deviceId, String service, String characteristic) { + var device = _findDeviceById(deviceId); + var s = device.gattServices + .firstWhereOrNull((s) => s.uuid.toString() == service); + var c = s?.characteristics + .firstWhereOrNull((c) => c.uuid.toString() == characteristic); + + if (c == null) { + throw Exception('Unknown characteristic:$characteristic'); + } + return c; + } + + @override + Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty) async { + var char = _getCharacteristic(deviceId, service, characteristic); + if (bleInputProperty != BleInputProperty.disabled) { + char.startNotify(); + + if (_characteristicPropertiesSubscriptions[characteristic] != null) { + _characteristicPropertiesSubscriptions[characteristic]?.cancel(); + } + + _characteristicPropertiesSubscriptions[characteristic] = + char.propertiesChanged.listen((List properties) { + for (String property in properties) { + switch (property) { + case BluezProperty.value: + onValueChanged?.call( + deviceId, + characteristic, + Uint8List.fromList(char.value), + ); + break; + default: + print("UnhandledCharValuePropertyChange: $property"); + } + } + }); + } else { + char.stopNotify(); + _characteristicPropertiesSubscriptions.remove(characteristic)?.cancel(); + } + } + + @override + Future readValue( + String deviceId, String service, String characteristic) async { + var c = _getCharacteristic(deviceId, service, characteristic); + var data = await c.readValue(); + return Uint8List.fromList(data); + } + + @override + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty) async { + var c = _getCharacteristic(deviceId, service, characteristic); + + if (bleOutputProperty == BleOutputProperty.withResponse) { + await c.writeValue(value, type: BlueZGattCharacteristicWriteType.request); + } else { + await c.writeValue(value, type: BlueZGattCharacteristicWriteType.command); + } + } + + @override + Future requestMtu(String deviceId, int expectedMtu) async { + // var device = _findDeviceById(deviceId); + // if (!device.connected) return 0; + // for (BlueZGattService service in device.gattServices) { + // for (BlueZGattCharacteristic characteristic in service.characteristics) { + // // The value provided by Bluez includes an extra 3 bytes from the GATT header, which needs to be removed. + // // requires `int? get mtu => _object.getUint16Property(_gattCharacteristicInterfaceName, 'MTU');` to add in bluez.dart + // return characteristic.mtu - 3; + // } + // } + // return 0; + throw UnimplementedError(); + } + + @override + Future pair(String deviceId) async { + BlueZDevice device = _findDeviceById(deviceId); + await device.pair(); + } + + @override + Future unPair(String deviceId) async { + BlueZDevice device = _findDeviceById(deviceId); + if (device.paired) { + // await device.cancelPairing(); + await _activeAdapter?.removeDevice(device); + } + } + + @override + Future isPaired(String deviceId) async { + return _findDeviceById(deviceId).paired; + } + + @override + Future> getConnectedDevices( + List? withServices, + ) async { + List devices = _client.devices; + if (withServices != null && withServices.isNotEmpty) { + return devices + .where((device) { + if (device.servicesResolved) { + return device.gattServices + .map((e) => e.uuid.toString()) + .any((service) => withServices.contains(service)); + } + return true; + }) + .map((device) => device.toBleScanResult()) + .toList(); + } + return devices.map((device) => device.toBleScanResult()).toList(); + } + + AvailabilityState get _availabilityState { + return _activeAdapter!.powered + ? AvailabilityState.poweredOn + : AvailabilityState.poweredOff; + } + + BlueZDevice _findDeviceById(String deviceId) { + var device = _devices[deviceId] ?? + _client.devices + .firstWhereOrNull((device) => device.address == deviceId); + if (device == null) { + throw Exception('Unknown deviceId:$deviceId'); + } + return device; + } + + Future _ensureInitialized() async { + if (!isInitialized) { + await _client.connect(); + + _activeAdapter ??= + _client.adapters.firstWhereOrNull((adapter) => adapter.powered); + + if (_activeAdapter == null) { + if (_client.adapters.isEmpty) { + throw Exception('Bluetooth adapter unavailable'); + } + await _client.adapters.first.setPowered(true); + _activeAdapter = _client.adapters.first; + } + + _client.deviceAdded.listen(_onDeviceAdd); + _client.deviceRemoved.listen(_onDeviceRemoved); + + _activeAdapter?.propertiesChanged.listen((List properties) { + // Handle pairing state change + for (var property in properties) { + switch (property) { + case BluezProperty.powered: + onAvailabilityChange?.call(_availabilityState); + break; + case BluezProperty.discoverable: + case BluezProperty.discovering: + break; + default: + print("UnhandledPropertyChanged: $property"); + } + } + }); + + onAvailabilityChange?.call(_availabilityState); + isInitialized = true; + } + } + + void _onDeviceAdd(BlueZDevice device) { + // Update ScanResults + onScanResult?.call(device.toBleScanResult()); + + // Setup Cache + _devices[device.address] = device; + + // Setup update listener + if (_deviceStreamSubscriptions[device.address] != null) { + _deviceStreamSubscriptions[device.address]?.cancel(); + } + _deviceStreamSubscriptions[device.address] = + device.propertiesChanged.listen((properties) { + for (var property in properties) { + switch (property) { + case BluezProperty.rssi: + onScanResult?.call(device.toBleScanResult()); + break; + case BluezProperty.connected: + onConnectionChanged?.call( + device.address, + device.connected + ? BleConnectionState.connected + : BleConnectionState.disconnected, + ); + break; + case BluezProperty.manufacturerData: + onScanResult?.call(device.toBleScanResult()); + break; + case BluezProperty.paired: + onPairStateChange?.call(device.address, device.paired, null); + break; + case BluezProperty.legacyPairing: + case BluezProperty.servicesResolved: + case BluezProperty.uuids: + break; + default: + print("UnhandledDevicePropertyChanged: $property"); + break; + } + } + }); + } + + void _onDeviceRemoved(BlueZDevice device) { + _devices.remove(device.address); + + // Stop listener + _deviceStreamSubscriptions[device.address]?.cancel(); + _deviceStreamSubscriptions + .removeWhere((key, value) => key == device.address); + } +} + +class BluezProperty { + static const String rssi = 'RSSI'; + static const String connected = 'Connected'; + static const String manufacturerData = 'ManufacturerData'; + static const String legacyPairing = 'LegacyPairing'; + static const String servicesResolved = 'ServicesResolved'; + static const String paired = 'Paired'; + static const String address = 'Address'; + static const String addressType = 'AddressType'; + static const String modalias = 'Modalias'; + static const String uuids = 'UUIDs'; + static const String value = 'Value'; + static const String powered = 'Powered'; + static const String discoverable = 'Discoverable'; + static const String discovering = 'Discovering'; +} + +extension BlueZDeviceExtension on BlueZDevice { + Uint8List get manufacturerDataHead { + if (manufacturerData.isEmpty) return Uint8List(0); + + final sorted = manufacturerData.entries.toList() + ..sort((a, b) => a.key.id - b.key.id); + return Uint8List.fromList(sorted.first.value); + } + + BleScanResult toBleScanResult() { + return BleScanResult( + name: alias, + deviceId: address, + isPaired: paired, + manufacturerData: manufacturerDataHead, + manufacturerDataHead: manufacturerDataHead, + rssi: rssi, + ); + } +} + +extension on BlueZGattCharacteristicFlag { + CharacteristicProperty? toCharacteristicProperty() { + return switch (this) { + BlueZGattCharacteristicFlag.broadcast => CharacteristicProperty.broadcast, + BlueZGattCharacteristicFlag.read => CharacteristicProperty.read, + BlueZGattCharacteristicFlag.writeWithoutResponse => + CharacteristicProperty.writeWithoutResponse, + BlueZGattCharacteristicFlag.write => CharacteristicProperty.write, + BlueZGattCharacteristicFlag.notify => CharacteristicProperty.notify, + BlueZGattCharacteristicFlag.indicate => CharacteristicProperty.indicate, + BlueZGattCharacteristicFlag.authenticatedSignedWrites => + CharacteristicProperty.authenticatedSignedWrites, + BlueZGattCharacteristicFlag.extendedProperties => + CharacteristicProperty.extendedProperties, + _ => null, + }; + } +} diff --git a/lib/src/universal_ble_pigeon/universal_ble.g.dart b/lib/src/universal_ble_pigeon/universal_ble.g.dart new file mode 100644 index 0000000..c72a268 --- /dev/null +++ b/lib/src/universal_ble_pigeon/universal_ble.g.dart @@ -0,0 +1,705 @@ +// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +class UniversalBleScanResult { + UniversalBleScanResult({ + required this.deviceId, + this.name, + this.isPaired, + this.rssi, + this.manufacturerData, + this.manufacturerDataHead, + }); + + String deviceId; + + String? name; + + bool? isPaired; + + int? rssi; + + Uint8List? manufacturerData; + + Uint8List? manufacturerDataHead; + + Object encode() { + return [ + deviceId, + name, + isPaired, + rssi, + manufacturerData, + manufacturerDataHead, + ]; + } + + static UniversalBleScanResult decode(Object result) { + result as List; + return UniversalBleScanResult( + deviceId: result[0]! as String, + name: result[1] as String?, + isPaired: result[2] as bool?, + rssi: result[3] as int?, + manufacturerData: result[4] as Uint8List?, + manufacturerDataHead: result[5] as Uint8List?, + ); + } +} + +class UniversalBleService { + UniversalBleService({ + required this.uuid, + this.characteristics, + }); + + String uuid; + + List? characteristics; + + Object encode() { + return [ + uuid, + characteristics, + ]; + } + + static UniversalBleService decode(Object result) { + result as List; + return UniversalBleService( + uuid: result[0]! as String, + characteristics: (result[1] as List?)?.cast(), + ); + } +} + +class UniversalBleCharacteristic { + UniversalBleCharacteristic({ + required this.uuid, + required this.properties, + }); + + String uuid; + + List properties; + + Object encode() { + return [ + uuid, + properties, + ]; + } + + static UniversalBleCharacteristic decode(Object result) { + result as List; + return UniversalBleCharacteristic( + uuid: result[0]! as String, + properties: (result[1] as List?)!.cast(), + ); + } +} + +class _UniversalBlePlatformChannelCodec extends StandardMessageCodec { + const _UniversalBlePlatformChannelCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is UniversalBleCharacteristic) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is UniversalBleScanResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is UniversalBleService) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return UniversalBleCharacteristic.decode(readValue(buffer)!); + case 129: + return UniversalBleScanResult.decode(readValue(buffer)!); + case 130: + return UniversalBleService.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Flutter -> Native +class UniversalBlePlatformChannel { + /// Constructor for [UniversalBlePlatformChannel]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + UniversalBlePlatformChannel({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _UniversalBlePlatformChannelCodec(); + + Future getBluetoothAvailabilityState() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + + Future enableBluetooth() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future startScan() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future stopScan() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future connect(String arg_deviceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future disconnect(String arg_deviceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setNotifiable(String arg_deviceId, String arg_service, String arg_characteristic, int arg_bleInputProperty) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId, arg_service, arg_characteristic, arg_bleInputProperty]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future> discoverServices(String arg_deviceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + Future readValue(String arg_deviceId, String arg_service, String arg_characteristic) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId, arg_service, arg_characteristic]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as Uint8List?)!; + } + } + + Future requestMtu(String arg_deviceId, int arg_expectedMtu) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId, arg_expectedMtu]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as int?)!; + } + } + + Future writeValue(String arg_deviceId, String arg_service, String arg_characteristic, Uint8List arg_value, int arg_bleOutputProperty) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId, arg_service, arg_characteristic, arg_value, arg_bleOutputProperty]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future isPaired(String arg_deviceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future pair(String arg_deviceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future unPair(String arg_deviceId) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_deviceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future> getConnectedDevices(List arg_withServices) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_withServices]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } +} + +class _UniversalBleCallbackChannelCodec extends StandardMessageCodec { + const _UniversalBleCallbackChannelCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is UniversalBleScanResult) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return UniversalBleScanResult.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Native -> Flutter +abstract class UniversalBleCallbackChannel { + static const MessageCodec codec = _UniversalBleCallbackChannelCodec(); + + void onAvailabilityChanged(int state); + + void onPairStateChange(String deviceId, bool isPaired, String? error); + + void onScanResult(UniversalBleScanResult result); + + void onValueChanged(String deviceId, String characteristicId, Uint8List value); + + void onConnectionChanged(String deviceId, int state); + + static void setup(UniversalBleCallbackChannel? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged was null.'); + final List args = (message as List?)!; + final int? arg_state = (args[0] as int?); + assert(arg_state != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged was null, expected non-null int.'); + try { + api.onAvailabilityChanged(arg_state!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null.'); + final List args = (message as List?)!; + final String? arg_deviceId = (args[0] as String?); + assert(arg_deviceId != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null, expected non-null String.'); + final bool? arg_isPaired = (args[1] as bool?); + assert(arg_isPaired != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange was null, expected non-null bool.'); + final String? arg_error = (args[2] as String?); + try { + api.onPairStateChange(arg_deviceId!, arg_isPaired!, arg_error); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null.'); + final List args = (message as List?)!; + final UniversalBleScanResult? arg_result = (args[0] as UniversalBleScanResult?); + assert(arg_result != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult was null, expected non-null UniversalBleScanResult.'); + try { + api.onScanResult(arg_result!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null.'); + final List args = (message as List?)!; + final String? arg_deviceId = (args[0] as String?); + assert(arg_deviceId != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null, expected non-null String.'); + final String? arg_characteristicId = (args[1] as String?); + assert(arg_characteristicId != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged was null, expected non-null String.'); + 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.'); + try { + api.onValueChanged(arg_deviceId!, arg_characteristicId!, arg_value!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null.'); + final List args = (message as List?)!; + final String? arg_deviceId = (args[0] as String?); + assert(arg_deviceId != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null, expected non-null String.'); + final int? arg_state = (args[1] as int?); + assert(arg_state != null, + 'Argument for dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged was null, expected non-null int.'); + try { + api.onConnectionChanged(arg_deviceId!, arg_state!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart new file mode 100644 index 0000000..9343d68 --- /dev/null +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -0,0 +1,197 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:universal_ble/src/universal_ble_pigeon/universal_ble.g.dart'; +import 'package:universal_ble/universal_ble.dart'; + +class UniversalBlePigeonChannel extends UniversalBlePlatform { + static UniversalBlePigeonChannel? _instance; + static UniversalBlePigeonChannel get instance => + _instance ??= UniversalBlePigeonChannel._(); + + UniversalBlePigeonChannel._() { + _setupListeners(); + } + + final _channel = UniversalBlePlatformChannel(); + + @override + Future getBluetoothAvailabilityState() async { + int state = await _channel.getBluetoothAvailabilityState(); + return AvailabilityState.parse(state); + } + + @override + Future enableBluetooth() { + if (Platform.isIOS || Platform.isMacOS) throw UnimplementedError(); + return _channel.enableBluetooth(); + } + + @override + Future startScan({ + WebRequestOptionsBuilder? webRequestOptions, + }) => + _channel.startScan(); + + @override + Future stopScan() => _channel.stopScan(); + + @override + Future connect(String deviceId, {Duration? connectionTimeout}) => + _channel.connect(deviceId); + + @override + Future disconnect(String deviceId) => _channel.disconnect(deviceId); + + @override + Future> discoverServices(String deviceId) async { + List universalBleServices = + await _channel.discoverServices(deviceId); + return List.from(universalBleServices + .where((e) => e != null) + .map((e) => e!.toBleService()) + .toList()); + } + + @override + Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty) { + return _channel.setNotifiable( + deviceId, + service, + characteristic, + bleInputProperty.value, + ); + } + + @override + Future readValue( + String deviceId, String service, String characteristic) { + return _channel.readValue(deviceId, service, characteristic); + } + + @override + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty) { + return _channel.writeValue( + deviceId, + service, + characteristic, + value, + bleOutputProperty.value, + ); + } + + @override + Future requestMtu(String deviceId, int expectedMtu) => + _channel.requestMtu(deviceId, expectedMtu); + + @override + Future isPaired(String deviceId) => _channel.isPaired(deviceId); + + @override + Future pair(String deviceId) => _channel.pair(deviceId); + + @override + Future unPair(String deviceId) => _channel.unPair(deviceId); + + @override + Future> getConnectedDevices( + List? withServices, + ) async { + var devices = await _channel.getConnectedDevices(withServices ?? []); + return List.from(devices + .map((e) => e?.toBleScanResult()) + .where((e) => e != null) + .toList()); + } + + /// To set listeners + void _setupListeners() { + UniversalBleCallbackChannel.setup(_UniversalBleCallbackHandler( + scanResult: (BleScanResult scanResult) => onScanResult?.call(scanResult), + availabilityChange: (AvailabilityState state) => + onAvailabilityChange?.call(state), + connectionChanged: (String deviceId, BleConnectionState state) => + onConnectionChanged?.call(deviceId, state), + valueChanged: + (String deviceId, String characteristicId, Uint8List value) => + onValueChanged?.call(deviceId, characteristicId, value), + pairStateChange: (String deviceId, bool isPaired, String? error) => + onPairStateChange?.call(deviceId, isPaired, error), + )); + } +} + +extension _BleServiceExtension on UniversalBleService { + BleService toBleService() { + List bleCharacteristics = []; + for (UniversalBleCharacteristic? characteristic in characteristics ?? []) { + if (characteristic == null) continue; + List? properties = List.from(characteristic.properties); + bleCharacteristics.add(BleCharacteristic( + characteristic.uuid, + List.from( + properties.map((e) => CharacteristicProperty.parse(e ?? 1)), + ), + )); + } + return BleService(uuid, bleCharacteristics); + } +} + +class _UniversalBleCallbackHandler extends UniversalBleCallbackChannel { + OnAvailabilityChange availabilityChange; + OnScanResult scanResult; + OnConnectionChanged connectionChanged; + OnValueChanged valueChanged; + OnPairStateChange pairStateChange; + + _UniversalBleCallbackHandler({ + required this.availabilityChange, + required this.scanResult, + required this.connectionChanged, + required this.valueChanged, + required this.pairStateChange, + }); + + @override + void onAvailabilityChanged(int state) => + availabilityChange(AvailabilityState.parse(state)); + + @override + void onConnectionChanged(String deviceId, int state) => + connectionChanged(deviceId, BleConnectionState.parse(state)); + + @override + void onScanResult(UniversalBleScanResult result) => + scanResult(result.toBleScanResult()); + + @override + void onValueChanged( + String deviceId, String characteristicId, Uint8List value) => + valueChanged(deviceId, characteristicId, value); + + @override + void onPairStateChange(String deviceId, bool isPaired, String? error) => + pairStateChange(deviceId, isPaired, error); +} + +extension _UniversalBleScanResultExtension on UniversalBleScanResult { + BleScanResult toBleScanResult() { + var mnfDataHead = manufacturerDataHead ?? Uint8List.fromList([]); + var mnfData = manufacturerData ?? mnfDataHead; + return BleScanResult( + name: name, + deviceId: deviceId, + isPaired: isPaired, + manufacturerData: mnfData, + manufacturerDataHead: mnfDataHead, + rssi: rssi, + ); + } +} diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart new file mode 100644 index 0000000..e8542fc --- /dev/null +++ b/lib/src/universal_ble_platform_interface.dart @@ -0,0 +1,52 @@ +import 'dart:typed_data'; + +import 'package:universal_ble/universal_ble.dart'; + +abstract class UniversalBlePlatform { + Future getBluetoothAvailabilityState(); + + Future enableBluetooth(); + + Future startScan({ + WebRequestOptionsBuilder? webRequestOptions, + }); + + Future stopScan(); + + Future connect(String deviceId, {Duration? connectionTimeout}); + + Future disconnect(String deviceId); + + Future> discoverServices(String deviceId); + + Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty); + + Future readValue( + String deviceId, String service, String characteristic); + + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty); + + Future requestMtu(String deviceId, int expectedMtu); + + Future isPaired(String deviceId); + + Future pair(String deviceId); + + Future unPair(String deviceId); + + Future> getConnectedDevices( + List? withServices, + ); + + OnAvailabilityChange? onAvailabilityChange; + OnScanResult? onScanResult; + OnConnectionChanged? onConnectionChanged; + OnValueChanged? onValueChanged; + OnPairStateChange? onPairStateChange; +} diff --git a/lib/src/universal_ble_web/queue.dart b/lib/src/universal_ble_web/queue.dart new file mode 100644 index 0000000..24aa133 --- /dev/null +++ b/lib/src/universal_ble_web/queue.dart @@ -0,0 +1,139 @@ +import 'dart:async'; + +/// Original Author: Ryan Knell (https://github.com/rknell/dart_queue/commits/master/lib/src/dart_queue_base.dart) + +/// Queue to execute Futures in order. +/// It awaits each future before executing the next one. +class Queue { + final List<_QueuedFuture> _nextCycle = []; + final Duration? delay; + final Duration? timeout; + int parallel; + int _lastProcessId = 0; + bool _isCancelled = false; + bool get isCancelled => _isCancelled; + StreamController? _remainingItemsController; + + Stream? get remainingItems { + _remainingItemsController ??= StreamController(); + return _remainingItemsController?.stream.asBroadcastStream(); + } + + final List> _completeListeners = []; + Future get onComplete { + final completer = Completer(); + _completeListeners.add(completer); + return completer.future; + } + + Set activeItems = {}; + + void cancel() { + for (final item in _nextCycle) { + item.completer.completeError(QueueCancelledException()); + } + _nextCycle.removeWhere((item) => item.completer.isCompleted); + _isCancelled = true; + } + + void dispose() { + _remainingItemsController?.close(); + cancel(); + } + + Queue({this.delay, this.parallel = 1, this.timeout}); + + Future add(Future Function() closure) { + if (isCancelled) throw QueueCancelledException(); + final completer = Completer(); + _nextCycle.add(_QueuedFuture(closure, completer, timeout)); + _updateRemainingItems(); + unawaited(_process()); + return completer.future; + } + + Future _process() async { + if (activeItems.length < parallel) { + _queueUpNext(); + } + } + + void _updateRemainingItems() { + final remainingItemsController = _remainingItemsController; + if (remainingItemsController != null && + remainingItemsController.isClosed == false) { + remainingItemsController.sink.add(_nextCycle.length + activeItems.length); + } + } + + void _queueUpNext() { + if (_nextCycle.isNotEmpty && + !isCancelled && + activeItems.length <= parallel) { + final processId = _lastProcessId; + activeItems.add(processId); + final item = _nextCycle.first; + _lastProcessId++; + _nextCycle.remove(item); + item.onComplete = () async { + activeItems.remove(processId); + var completionDelay = delay; + if (completionDelay != null) { + await Future.delayed(completionDelay); + } + _updateRemainingItems(); + _queueUpNext(); + }; + unawaited(item.execute()); + } else if (activeItems.isEmpty && _nextCycle.isEmpty) { + for (final completer in _completeListeners) { + if (completer.isCompleted != true) { + completer.complete(); + } + } + _completeListeners.clear(); + } + } +} + +class QueueCancelledException implements Exception {} + +class _QueuedFuture { + final Completer completer; + final Future Function() closure; + final Duration? timeout; + Function? onComplete; + + _QueuedFuture(this.closure, this.completer, this.timeout, {this.onComplete}); + + bool _timedOut = false; + + Future execute() async { + try { + T result; + Timer? timeoutTimer; + + var executionTimeout = timeout; + if (executionTimeout != null) { + timeoutTimer = Timer(executionTimeout, () { + _timedOut = true; + if (onComplete != null) { + onComplete?.call(); + } + }); + } + result = await closure(); + if (result != null) { + completer.complete(result); + } else { + completer.complete(null); + } + timeoutTimer?.cancel(); + await Future.microtask(() {}); + } catch (e) { + completer.completeError(e); + } finally { + if (onComplete != null && !_timedOut) onComplete?.call(); + } + } +} diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart new file mode 100644 index 0000000..e265cd1 --- /dev/null +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -0,0 +1,381 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart'; +import 'package:universal_ble/src/models/model_exports.dart'; +import 'package:universal_ble/src/universal_ble_platform_interface.dart'; +import 'package:universal_ble/src/universal_ble_web/queue.dart'; + +class UniversalBleWeb extends UniversalBlePlatform { + static UniversalBleWeb? _instance; + static UniversalBleWeb get instance => _instance ??= UniversalBleWeb._(); + + UniversalBleWeb._() { + _setupListeners(); + } + + late final _queue = Queue(); + final Map _bluetoothDeviceList = {}; + final Map _deviceAdvertisementStreamList = {}; + final Map _connectedDeviceStreamList = {}; + final Map _characteristicStreamList = {}; + + @override + Future connect( + String deviceId, { + Duration? connectionTimeout = const Duration(seconds: 10), + }) async { + var device = _getDeviceById(deviceId); + if (device == null) throw "$deviceId Not Found"; + await device.connect(timeout: connectionTimeout); + + // Subscribe to Connection Stream + if (_connectedDeviceStreamList[deviceId] != null) { + _connectedDeviceStreamList[deviceId]?.cancel(); + } + + _connectedDeviceStreamList[deviceId] = device.connected.listen((event) { + if (!event) _cleanConnection(deviceId); + onConnectionChanged?.call( + deviceId, + event ? BleConnectionState.connected : BleConnectionState.disconnected, + ); + }); + } + + @override + Future disconnect(String deviceId) async { + _cleanConnection(deviceId); + onConnectionChanged?.call(deviceId, BleConnectionState.disconnected); + _getDeviceById(deviceId)?.disconnect(); + } + + @override + Future> discoverServices(String deviceId) async { + final device = _getDeviceById(deviceId); + final services = await device?.discoverServices(); + if (services == null) return []; + var discoveredServices = []; + for (var service in services) { + final characteristics = await service.getCharacteristics(); + List bleCharacteristics = []; + for (BluetoothCharacteristic characteristic in characteristics) { + List properties = []; + if (characteristic.properties.broadcast) { + properties.add(CharacteristicProperty.broadcast); + } + if (characteristic.properties.read) { + properties.add(CharacteristicProperty.read); + } + if (characteristic.properties.writeWithoutResponse) { + properties.add(CharacteristicProperty.writeWithoutResponse); + } + if (characteristic.properties.write) { + properties.add(CharacteristicProperty.write); + } + if (characteristic.properties.notify) { + properties.add(CharacteristicProperty.notify); + } + if (characteristic.properties.indicate) { + properties.add(CharacteristicProperty.indicate); + } + if (characteristic.properties.authenticatedSignedWrites) { + properties.add(CharacteristicProperty.authenticatedSignedWrites); + } + bleCharacteristics.add( + BleCharacteristic(characteristic.uuid.toString(), properties), + ); + } + discoveredServices.add( + BleService(service.uuid.toString(), bleCharacteristics), + ); + } + return discoveredServices; + } + + @override + Future getBluetoothAvailabilityState() async { + bool isSupported = FlutterWebBluetooth.instance.isBluetoothApiSupported; + if (!isSupported) { + return AvailabilityState.unsupported; + } + bool isAvailable = await FlutterWebBluetooth.instance.isAvailable.first; + if (isSupported && !isAvailable) { + return AvailabilityState.poweredOff; + } else if (isAvailable) { + return AvailabilityState.poweredOn; + } + return AvailabilityState.unknown; + } + + @override + Future startScan({ + WebRequestOptionsBuilder? webRequestOptions, + }) async { + BluetoothDevice device = await FlutterWebBluetooth.instance.requestDevice( + webRequestOptions?.toRequestOptionsBuilder() ?? + RequestOptionsBuilder.acceptAllDevices(), + ); + + // Update local device list + _bluetoothDeviceList[device.id] = device; + + // Update Scan Result + onScanResult?.call(device.toBleScanResult()); + + /// This will work only if `chrome://flags/#enable-experimental-web-platform-features` is enabled + if (FlutterWebBluetooth.instance.hasRequestLEScan) { + // Check if platform can watch advertisements + if (device.hasWatchAdvertisements()) { + if (_deviceAdvertisementStreamList[device.id] == null) { + _deviceAdvertisementStreamList[device.id]?.cancel(); + await device.unwatchAdvertisements(); + } + + _deviceAdvertisementStreamList[device.id] = + device.advertisements.listen((event) { + onScanResult?.call( + device.toBleScanResult( + rssi: event.rssi, + manufacturerDataMap: event.manufacturerData, + ), + ); + }); + + await device.watchAdvertisements(); + } + } + } + + @override + Future stopScan() async { + // Cancel advertisement streams + if (FlutterWebBluetooth.instance.hasRequestLEScan) { + _deviceAdvertisementStreamList.removeWhere((key, value) { + value.cancel(); + return true; + }); + + for (var element in _bluetoothDeviceList.entries) { + if (element.value.hasWatchAdvertisements()) { + element.value.unwatchAdvertisements(); + } + } + } + } + + @override + Future setNotifiable( + String deviceId, + String service, + String characteristic, + BleInputProperty bleInputProperty, + ) async { + final bleCharacteristic = await _getBleCharacteristic( + deviceId: deviceId, + serviceId: service, + characteristicId: characteristic, + ); + + if (bleCharacteristic == null) { + throw Exception( + 'Characteristic $characteristic for service $service not found', + ); + } + + String characteristicKey = "${deviceId}_${service}_$characteristic"; + + if (bleInputProperty == BleInputProperty.notification || + bleInputProperty == BleInputProperty.indication) { + if (bleCharacteristic.isNotifying) { + throw Exception("Already listening to this characteristic"); + } + + if (_characteristicStreamList[characteristicKey] != null) { + _characteristicStreamList[characteristicKey]?.cancel(); + } + + await bleCharacteristic.startNotifications(); + + _characteristicStreamList[characteristicKey] = bleCharacteristic.value + .map((event) => event.buffer.asUint8List()) + .listen((event) { + onValueChanged?.call(deviceId, characteristic, event); + }); + } + // Cancel Notification + else if (bleInputProperty == BleInputProperty.disabled) { + await bleCharacteristic.stopNotifications(); + _characteristicStreamList.removeWhere((key, value) { + if (key == characteristicKey) value.cancel(); + return key == characteristicKey; + }); + } + } + + @override + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty, + ) async { + await _queue.add(() async { + final bleCharacteristic = await _getBleCharacteristic( + deviceId: deviceId, + serviceId: service, + characteristicId: characteristic, + ); + + if (bleCharacteristic == null) { + throw Exception( + 'Characteristic $characteristic for service $service not found', + ); + } + + if (bleOutputProperty == BleOutputProperty.withResponse) { + await bleCharacteristic + .writeValueWithResponse(Uint8List.fromList(value)); + } else { + await bleCharacteristic + .writeValueWithoutResponse(Uint8List.fromList(value)); + } + }); + } + + @override + Future readValue( + String deviceId, + String service, + String characteristic, + ) async { + return _queue.add(() async { + var bleCharacteristic = await _getBleCharacteristic( + deviceId: deviceId, + serviceId: service, + characteristicId: characteristic, + ); + if (bleCharacteristic == null) { + throw Exception( + 'Characteristic $characteristic for service $service not found'); + } + var data = await bleCharacteristic.readValue(); + return data.buffer.asUint8List(); + }); + } + + /// `Unimplemented` + @override + Future requestMtu(String deviceId, int expectedMtu) { + throw UnimplementedError(); + } + + @override + Future isPaired(String deviceId) { + throw UnimplementedError(); + } + + @override + Future pair(String deviceId) { + throw UnimplementedError(); + } + + @override + Future unPair(String deviceId) { + throw UnimplementedError(); + } + + @override + Future> getConnectedDevices( + List? withServices, + ) { + throw UnimplementedError(); + } + + /// Helpers + void _setupListeners() { + FlutterWebBluetooth.instance.isAvailable.listen( + (bool isAvailable) { + AvailabilityState newState = AvailabilityState.unknown; + if (!FlutterWebBluetooth.instance.isBluetoothApiSupported) { + newState = AvailabilityState.unsupported; + } else if (FlutterWebBluetooth.instance.isBluetoothApiSupported && + !isAvailable) { + newState = AvailabilityState.poweredOff; + } else if (isAvailable) { + newState = AvailabilityState.poweredOn; + } + onAvailabilityChange?.call(newState); + }, + ); + } + + void _cleanConnection(String deviceId) { + _connectedDeviceStreamList.removeWhere((key, value) { + if (key == deviceId) value.cancel(); + return key == deviceId; + }); + _characteristicStreamList.removeWhere((key, value) { + if (key.contains(deviceId)) value.cancel(); + return key.contains(deviceId); + }); + // _bluetoothDeviceList.removeWhere((element) => element.id == deviceId); + } + + Future _getBleCharacteristic({ + required String deviceId, + required String serviceId, + required String characteristicId, + }) async { + final device = _getDeviceById(deviceId); + List services = await device?.discoverServices() ?? []; + BluetoothService? service; + for (BluetoothService bleService in services) { + if (bleService.uuid.toString() == serviceId.toString()) { + service = bleService; + break; + } + } + return await service?.getCharacteristic(characteristicId.toString()); + } + + BluetoothDevice? _getDeviceById(String id) => _bluetoothDeviceList[id]; + + @override + Future enableBluetooth() { + throw UnimplementedError(); + } +} + +extension _BluetoothDeviceExtension on BluetoothDevice { + BleScanResult toBleScanResult({ + int? rssi, + UnmodifiableMapView? manufacturerDataMap, + }) { + return BleScanResult( + name: name, + deviceId: id, + manufacturerData: manufacturerDataMap?.toUint8List(), + manufacturerDataHead: manufacturerDataMap?.toUint8List(), + rssi: rssi, + ); + } +} + +extension _UnmodifiableMapViewExtension on UnmodifiableMapView { + Uint8List toUint8List() { + int totalLength = + values.fold(0, (prev, element) => prev + element.lengthInBytes); + Uint8List result = Uint8List(totalLength); + int offset = 0; + for (var entry in entries) { + var byteData = entry.value; + var sublist = byteData.buffer.asUint8List(); + result.setRange(offset, offset + sublist.length, sublist); + offset += sublist.length; + } + return result; + } +} diff --git a/lib/universal_ble.dart b/lib/universal_ble.dart new file mode 100644 index 0000000..794e6f3 --- /dev/null +++ b/lib/universal_ble.dart @@ -0,0 +1,7 @@ +library universal_ble; + +export 'package:universal_ble/src/universal_ble_platform_interface.dart'; +export 'package:universal_ble/src/universal_ble.dart'; +export 'package:universal_ble/src/models/model_exports.dart'; + + diff --git a/pigeon/universal_ble.dart b/pigeon/universal_ble.dart new file mode 100644 index 0000000..731a253 --- /dev/null +++ b/pigeon/universal_ble.dart @@ -0,0 +1,129 @@ +import 'package:pigeon/pigeon.dart'; + +// dart run pigeon --input pigeon/universal_ble.dart +@ConfigurePigeon( + PigeonOptions( + dartPackageName: 'universal_ble', + dartOut: 'lib/src/universal_ble_pigeon/universal_ble.g.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/com/navideck/universal_ble/UniversalBle.g.kt', + swiftOut: 'darwin/Classes/UniversalBle.g.swift', + kotlinOptions: KotlinOptions(package: 'com.navideck.universal_ble'), + swiftOptions: SwiftOptions(), + cppOptions: CppOptions(namespace: 'universal_ble'), + cppHeaderOut: 'windows/UniversalBle.g.h', + cppSourceOut: 'windows/UniversalBle.g.cpp', + debugGenerators: true, + ), +) + +/// Flutter -> Native +@HostApi() +abstract class UniversalBlePlatformChannel { + @async + int getBluetoothAvailabilityState(); + + @async + bool enableBluetooth(); + + void startScan(); + + void stopScan(); + + void connect(String deviceId); + + void disconnect(String deviceId); + + void setNotifiable( + String deviceId, + String service, + String characteristic, + int bleInputProperty, + ); + + @async + List discoverServices(String deviceId); + + @async + Uint8List readValue( + String deviceId, + String service, + String characteristic, + ); + + @async + int requestMtu(String deviceId, int expectedMtu); + + @async + void writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + int bleOutputProperty, + ); + + @async + bool isPaired(String deviceId); + + void pair(String deviceId); + + void unPair(String deviceId); + + @async + List getConnectedDevices( + List withServices, + ); +} + +/// Native -> Flutter +@FlutterApi() +abstract class UniversalBleCallbackChannel { + void onAvailabilityChanged(int state); + + void onPairStateChange(String deviceId, bool isPaired, String? error); + + void onScanResult(UniversalBleScanResult result); + + void onValueChanged( + String deviceId, + String characteristicId, + Uint8List value, + ); + + void onConnectionChanged( + String deviceId, + int state, + ); +} + +class UniversalBleScanResult { + final String deviceId; + final String? name; + final bool? isPaired; + final int? rssi; + final Uint8List? manufacturerData; + final Uint8List? manufacturerDataHead; + + UniversalBleScanResult({ + required this.name, + required this.deviceId, + required this.isPaired, + required this.rssi, + required this.manufacturerData, + required this.manufacturerDataHead, + }); +} + +class UniversalBleService { + String uuid; + List? characteristics; + UniversalBleService(this.uuid, this.characteristics); +} + +class UniversalBleCharacteristic { + String uuid; + List properties; + UniversalBleCharacteristic(this.uuid, this.properties); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..d81c7f5 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,39 @@ +name: universal_ble +description: A cross-platform (Android/iOS/MacOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter +version: 0.8.0 +homepage: https://navideck.com +repository: https://github.com/Navideck/universal_ble +issue_tracker: https://github.com/Navideck/universal_ble/issues + +environment: + sdk: ">=3.1.3 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.6 + collection: ^1.17.2 + convert: ^3.1.1 + flutter_web_bluetooth: ^0.2.2 + bluez: ^0.8.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + pigeon: ^13.0.0 +flutter: + plugin: + platforms: + android: + package: com.navideck.universal_ble + pluginClass: UniversalBlePlugin + ios: + pluginClass: UniversalBlePlugin + sharedDarwinSource: true + macos: + pluginClass: UniversalBlePlugin + sharedDarwinSource: true + windows: + pluginClass: UniversalBlePluginCApi diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..3979c5b --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.21) +set(PROJECT_NAME "universal_ble") +project(${PROJECT_NAME} LANGUAGES CXX) +cmake_policy(VERSION 3.14...3.25) +include(FetchContent) +set(CPPWINRT_VERSION "2.0.220418.1") +set(PLUGIN_NAME "universal_ble_plugin") + +################ NuGet intall begin ################ +FetchContent_Declare(nuget + URL "https://dist.nuget.org/win-x86-commandline/v6.0.0/nuget.exe" + URL_HASH SHA256=04eb6c4fe4213907e2773e1be1bbbd730e9a655a3c9c58387ce8d4a714a5b9e1 + DOWNLOAD_NO_EXTRACT true +) + +find_program(NUGET nuget) +if (NOT NUGET) + message("Nuget.exe not found, trying to download or use cached version.") + FetchContent_MakeAvailable(nuget) + set(NUGET ${nuget_SOURCE_DIR}/nuget.exe) +endif() + +execute_process(COMMAND + ${NUGET} install "Microsoft.Windows.CppWinRT" -Version ${CPPWINRT_VERSION} -OutputDirectory packages + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE ret) +if (NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to install nuget package Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}") +endif() + +################ NuGet install end ################ + + +list(APPEND PLUGIN_SOURCES + "universal_ble_plugin.cpp" + "universal_ble_plugin.h" + "universal_enum.h" + "Utils.cpp" + "Utils.h" + "UniversalBle.g.cpp" + "UniversalBle.g.h" +) + +add_library(${PLUGIN_NAME} SHARED + "include/universal_ble/universal_ble_plugin_c_api.h" + "universal_ble_plugin_c_api.cpp" + ${PLUGIN_SOURCES} +) + + +apply_standard_settings(${PLUGIN_NAME}) + + +################ NuGet import begin ################ +set_target_properties(${PLUGIN_NAME} PROPERTIES VS_PROJECT_IMPORT + ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/build/native/Microsoft.Windows.CppWinRT.props +) + +target_link_libraries(${PLUGIN_NAME} PRIVATE + ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/build/native/Microsoft.Windows.CppWinRT.targets +) +################ NuGet import end ################ + +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + + +set(universal_ble_bundled_libraries + "" + PARENT_SCOPE +) diff --git a/windows/UniversalBle.g.cpp b/windows/UniversalBle.g.cpp new file mode 100644 index 0000000..4eae7b1 --- /dev/null +++ b/windows/UniversalBle.g.cpp @@ -0,0 +1,940 @@ +// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "UniversalBle.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace universal_ble { +using flutter::BasicMessageChannel; +using flutter::CustomEncodableValue; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +// UniversalBleScanResult + +UniversalBleScanResult::UniversalBleScanResult(const std::string& device_id) + : device_id_(device_id) {} + +UniversalBleScanResult::UniversalBleScanResult( + const std::string& device_id, + const std::string* name, + const bool* is_paired, + const int64_t* rssi, + const std::vector* manufacturer_data, + const std::vector* manufacturer_data_head) + : device_id_(device_id), + name_(name ? std::optional(*name) : std::nullopt), + is_paired_(is_paired ? std::optional(*is_paired) : std::nullopt), + rssi_(rssi ? std::optional(*rssi) : std::nullopt), + manufacturer_data_(manufacturer_data ? std::optional>(*manufacturer_data) : std::nullopt), + manufacturer_data_head_(manufacturer_data_head ? std::optional>(*manufacturer_data_head) : std::nullopt) {} + +const std::string& UniversalBleScanResult::device_id() const { + return device_id_; +} + +void UniversalBleScanResult::set_device_id(std::string_view value_arg) { + device_id_ = value_arg; +} + + +const std::string* UniversalBleScanResult::name() const { + return name_ ? &(*name_) : nullptr; +} + +void UniversalBleScanResult::set_name(const std::string_view* value_arg) { + name_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void UniversalBleScanResult::set_name(std::string_view value_arg) { + name_ = value_arg; +} + + +const bool* UniversalBleScanResult::is_paired() const { + return is_paired_ ? &(*is_paired_) : nullptr; +} + +void UniversalBleScanResult::set_is_paired(const bool* value_arg) { + is_paired_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void UniversalBleScanResult::set_is_paired(bool value_arg) { + is_paired_ = value_arg; +} + + +const int64_t* UniversalBleScanResult::rssi() const { + return rssi_ ? &(*rssi_) : nullptr; +} + +void UniversalBleScanResult::set_rssi(const int64_t* value_arg) { + rssi_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void UniversalBleScanResult::set_rssi(int64_t value_arg) { + rssi_ = value_arg; +} + + +const std::vector* UniversalBleScanResult::manufacturer_data() const { + return manufacturer_data_ ? &(*manufacturer_data_) : nullptr; +} + +void UniversalBleScanResult::set_manufacturer_data(const std::vector* value_arg) { + manufacturer_data_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +} + +void UniversalBleScanResult::set_manufacturer_data(const std::vector& value_arg) { + manufacturer_data_ = value_arg; +} + + +const std::vector* UniversalBleScanResult::manufacturer_data_head() const { + return manufacturer_data_head_ ? &(*manufacturer_data_head_) : nullptr; +} + +void UniversalBleScanResult::set_manufacturer_data_head(const std::vector* value_arg) { + manufacturer_data_head_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +} + +void UniversalBleScanResult::set_manufacturer_data_head(const std::vector& value_arg) { + manufacturer_data_head_ = value_arg; +} + + +EncodableList UniversalBleScanResult::ToEncodableList() const { + EncodableList list; + list.reserve(6); + list.push_back(EncodableValue(device_id_)); + list.push_back(name_ ? EncodableValue(*name_) : EncodableValue()); + list.push_back(is_paired_ ? EncodableValue(*is_paired_) : EncodableValue()); + list.push_back(rssi_ ? EncodableValue(*rssi_) : EncodableValue()); + list.push_back(manufacturer_data_ ? EncodableValue(*manufacturer_data_) : EncodableValue()); + list.push_back(manufacturer_data_head_ ? EncodableValue(*manufacturer_data_head_) : EncodableValue()); + return list; +} + +UniversalBleScanResult UniversalBleScanResult::FromEncodableList(const EncodableList& list) { + UniversalBleScanResult decoded( + std::get(list[0])); + auto& encodable_name = list[1]; + if (!encodable_name.IsNull()) { + decoded.set_name(std::get(encodable_name)); + } + auto& encodable_is_paired = list[2]; + if (!encodable_is_paired.IsNull()) { + decoded.set_is_paired(std::get(encodable_is_paired)); + } + auto& encodable_rssi = list[3]; + if (!encodable_rssi.IsNull()) { + decoded.set_rssi(encodable_rssi.LongValue()); + } + auto& encodable_manufacturer_data = list[4]; + if (!encodable_manufacturer_data.IsNull()) { + decoded.set_manufacturer_data(std::get>(encodable_manufacturer_data)); + } + auto& encodable_manufacturer_data_head = list[5]; + if (!encodable_manufacturer_data_head.IsNull()) { + decoded.set_manufacturer_data_head(std::get>(encodable_manufacturer_data_head)); + } + return decoded; +} + +// UniversalBleService + +UniversalBleService::UniversalBleService(const std::string& uuid) + : uuid_(uuid) {} + +UniversalBleService::UniversalBleService( + const std::string& uuid, + const EncodableList* characteristics) + : uuid_(uuid), + characteristics_(characteristics ? std::optional(*characteristics) : std::nullopt) {} + +const std::string& UniversalBleService::uuid() const { + return uuid_; +} + +void UniversalBleService::set_uuid(std::string_view value_arg) { + uuid_ = value_arg; +} + + +const EncodableList* UniversalBleService::characteristics() const { + return characteristics_ ? &(*characteristics_) : nullptr; +} + +void UniversalBleService::set_characteristics(const EncodableList* value_arg) { + characteristics_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void UniversalBleService::set_characteristics(const EncodableList& value_arg) { + characteristics_ = value_arg; +} + + +EncodableList UniversalBleService::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(uuid_)); + list.push_back(characteristics_ ? EncodableValue(*characteristics_) : EncodableValue()); + return list; +} + +UniversalBleService UniversalBleService::FromEncodableList(const EncodableList& list) { + UniversalBleService decoded( + std::get(list[0])); + auto& encodable_characteristics = list[1]; + if (!encodable_characteristics.IsNull()) { + decoded.set_characteristics(std::get(encodable_characteristics)); + } + return decoded; +} + +// UniversalBleCharacteristic + +UniversalBleCharacteristic::UniversalBleCharacteristic( + const std::string& uuid, + const EncodableList& properties) + : uuid_(uuid), + properties_(properties) {} + +const std::string& UniversalBleCharacteristic::uuid() const { + return uuid_; +} + +void UniversalBleCharacteristic::set_uuid(std::string_view value_arg) { + uuid_ = value_arg; +} + + +const EncodableList& UniversalBleCharacteristic::properties() const { + return properties_; +} + +void UniversalBleCharacteristic::set_properties(const EncodableList& value_arg) { + properties_ = value_arg; +} + + +EncodableList UniversalBleCharacteristic::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(uuid_)); + list.push_back(EncodableValue(properties_)); + return list; +} + +UniversalBleCharacteristic UniversalBleCharacteristic::FromEncodableList(const EncodableList& list) { + UniversalBleCharacteristic decoded( + std::get(list[0]), + std::get(list[1])); + return decoded; +} + + +UniversalBlePlatformChannelCodecSerializer::UniversalBlePlatformChannelCodecSerializer() {} + +EncodableValue UniversalBlePlatformChannelCodecSerializer::ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const { + switch (type) { + case 128: + return CustomEncodableValue(UniversalBleCharacteristic::FromEncodableList(std::get(ReadValue(stream)))); + case 129: + return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); + case 130: + return CustomEncodableValue(UniversalBleService::FromEncodableList(std::get(ReadValue(stream)))); + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void UniversalBlePlatformChannelCodecSerializer::WriteValue( + const EncodableValue& value, + flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = std::get_if(&value)) { + if (custom_value->type() == typeid(UniversalBleCharacteristic)) { + stream->WriteByte(128); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(UniversalBleScanResult)) { + stream->WriteByte(129); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(UniversalBleService)) { + stream->WriteByte(130); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/// The codec used by UniversalBlePlatformChannel. +const flutter::StandardMessageCodec& UniversalBlePlatformChannel::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&UniversalBlePlatformChannelCodecSerializer::GetInstance()); +} + +// Sets up an instance of `UniversalBlePlatformChannel` to handle messages through the `binary_messenger`. +void UniversalBlePlatformChannel::SetUp( + flutter::BinaryMessenger* binary_messenger, + UniversalBlePlatformChannel* api) { + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->GetBluetoothAvailabilityState([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->EnableBluetooth([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + std::optional output = api->StartScan(); + 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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + std::optional output = api->StopScan(); + 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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + std::optional output = api->Connect(device_id_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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + std::optional output = api->Disconnect(device_id_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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + const auto& encodable_service_arg = args.at(1); + if (encodable_service_arg.IsNull()) { + reply(WrapError("service_arg unexpectedly null.")); + return; + } + const auto& service_arg = std::get(encodable_service_arg); + const auto& encodable_characteristic_arg = args.at(2); + if (encodable_characteristic_arg.IsNull()) { + reply(WrapError("characteristic_arg unexpectedly null.")); + return; + } + const auto& characteristic_arg = std::get(encodable_characteristic_arg); + const auto& encodable_ble_input_property_arg = args.at(3); + if (encodable_ble_input_property_arg.IsNull()) { + reply(WrapError("ble_input_property_arg unexpectedly null.")); + return; + } + const int64_t ble_input_property_arg = encodable_ble_input_property_arg.LongValue(); + std::optional output = api->SetNotifiable(device_id_arg, service_arg, characteristic_arg, ble_input_property_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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + api->DiscoverServices(device_id_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + const auto& encodable_service_arg = args.at(1); + if (encodable_service_arg.IsNull()) { + reply(WrapError("service_arg unexpectedly null.")); + return; + } + const auto& service_arg = std::get(encodable_service_arg); + const auto& encodable_characteristic_arg = args.at(2); + if (encodable_characteristic_arg.IsNull()) { + reply(WrapError("characteristic_arg unexpectedly null.")); + return; + } + const auto& characteristic_arg = std::get(encodable_characteristic_arg); + api->ReadValue(device_id_arg, service_arg, characteristic_arg, [reply](ErrorOr>&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + const auto& encodable_expected_mtu_arg = args.at(1); + if (encodable_expected_mtu_arg.IsNull()) { + reply(WrapError("expected_mtu_arg unexpectedly null.")); + return; + } + const int64_t expected_mtu_arg = encodable_expected_mtu_arg.LongValue(); + api->RequestMtu(device_id_arg, expected_mtu_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + const auto& encodable_service_arg = args.at(1); + if (encodable_service_arg.IsNull()) { + reply(WrapError("service_arg unexpectedly null.")); + return; + } + const auto& service_arg = std::get(encodable_service_arg); + const auto& encodable_characteristic_arg = args.at(2); + if (encodable_characteristic_arg.IsNull()) { + reply(WrapError("characteristic_arg unexpectedly null.")); + return; + } + const auto& characteristic_arg = std::get(encodable_characteristic_arg); + const auto& encodable_value_arg = args.at(3); + if (encodable_value_arg.IsNull()) { + reply(WrapError("value_arg unexpectedly null.")); + return; + } + const auto& value_arg = std::get>(encodable_value_arg); + const auto& encodable_ble_output_property_arg = args.at(4); + if (encodable_ble_output_property_arg.IsNull()) { + reply(WrapError("ble_output_property_arg unexpectedly null.")); + return; + } + const int64_t ble_output_property_arg = encodable_ble_output_property_arg.LongValue(); + api->WriteValue(device_id_arg, service_arg, characteristic_arg, value_arg, ble_output_property_arg, [reply](std::optional&& output) { + 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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + api->IsPaired(device_id_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + std::optional output = api->Pair(device_id_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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_device_id_arg = args.at(0); + if (encodable_device_id_arg.IsNull()) { + reply(WrapError("device_id_arg unexpectedly null.")); + return; + } + const auto& device_id_arg = std::get(encodable_device_id_arg); + std::optional output = api->UnPair(device_id_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); + } + } + { + auto channel = std::make_unique>(binary_messenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_with_services_arg = args.at(0); + if (encodable_with_services_arg.IsNull()) { + reply(WrapError("with_services_arg unexpectedly null.")); + return; + } + const auto& with_services_arg = std::get(encodable_with_services_arg); + api->GetConnectedDevices(with_services_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + 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) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue UniversalBlePlatformChannel::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + + +UniversalBleCallbackChannelCodecSerializer::UniversalBleCallbackChannelCodecSerializer() {} + +EncodableValue UniversalBleCallbackChannelCodecSerializer::ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const { + switch (type) { + case 128: + return CustomEncodableValue(UniversalBleScanResult::FromEncodableList(std::get(ReadValue(stream)))); + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void UniversalBleCallbackChannelCodecSerializer::WriteValue( + const EncodableValue& value, + flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = std::get_if(&value)) { + if (custom_value->type() == typeid(UniversalBleScanResult)) { + stream->WriteByte(128); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +// Generated class from Pigeon that represents Flutter messages that can be called from C++. +UniversalBleCallbackChannel::UniversalBleCallbackChannel(flutter::BinaryMessenger* binary_messenger) + : binary_messenger_(binary_messenger) {} + +const flutter::StandardMessageCodec& UniversalBleCallbackChannel::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&UniversalBleCallbackChannelCodecSerializer::GetInstance()); +} + +void UniversalBleCallbackChannel::OnAvailabilityChanged( + int64_t state_arg, + std::function&& on_success, + std::function&& on_error) { + auto channel = std::make_unique>(binary_messenger_, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged", &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(state_arg), + }); + channel->Send(encoded_api_arguments, [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); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError( std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(FlutterError("channel-error", "Unable to establish connection on channel.", EncodableValue(""))); + } + }); +} + +void UniversalBleCallbackChannel::OnPairStateChange( + const std::string& device_id_arg, + bool is_paired_arg, + const std::string* error_arg, + std::function&& on_success, + std::function&& on_error) { + auto channel = std::make_unique>(binary_messenger_, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange", &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(device_id_arg), + EncodableValue(is_paired_arg), + error_arg ? EncodableValue(*error_arg) : EncodableValue(), + }); + channel->Send(encoded_api_arguments, [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); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError( std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(FlutterError("channel-error", "Unable to establish connection on channel.", EncodableValue(""))); + } + }); +} + +void UniversalBleCallbackChannel::OnScanResult( + const UniversalBleScanResult& result_arg, + std::function&& on_success, + std::function&& on_error) { + auto channel = std::make_unique>(binary_messenger_, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult", &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + CustomEncodableValue(result_arg), + }); + channel->Send(encoded_api_arguments, [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); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError( std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(FlutterError("channel-error", "Unable to establish connection on channel.", EncodableValue(""))); + } + }); +} + +void UniversalBleCallbackChannel::OnValueChanged( + const std::string& device_id_arg, + const std::string& characteristic_id_arg, + const std::vector& value_arg, + std::function&& on_success, + std::function&& on_error) { + auto channel = std::make_unique>(binary_messenger_, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged", &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(device_id_arg), + EncodableValue(characteristic_id_arg), + EncodableValue(value_arg), + }); + channel->Send(encoded_api_arguments, [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); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError( std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(FlutterError("channel-error", "Unable to establish connection on channel.", EncodableValue(""))); + } + }); +} + +void UniversalBleCallbackChannel::OnConnectionChanged( + const std::string& device_id_arg, + int64_t state_arg, + std::function&& on_success, + std::function&& on_error) { + auto channel = std::make_unique>(binary_messenger_, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged", &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(EncodableList{ + EncodableValue(device_id_arg), + EncodableValue(state_arg), + }); + channel->Send(encoded_api_arguments, [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); + const auto& encodable_return_value = *response; + const auto* list_return_value = std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError( std::get(list_return_value->at(0)), std::get(list_return_value->at(1)), list_return_value->at(2))); + } else { + on_success(); + } + } else { + on_error(FlutterError("channel-error", "Unable to establish connection on channel.", EncodableValue(""))); + } + }); +} + +} // namespace universal_ble diff --git a/windows/UniversalBle.g.h b/windows/UniversalBle.g.h new file mode 100644 index 0000000..b2fef5f --- /dev/null +++ b/windows/UniversalBle.g.h @@ -0,0 +1,311 @@ +// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_UNIVERSALBLE_G_H_ +#define PIGEON_UNIVERSALBLE_G_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace universal_ble { + + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) + : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template class ErrorOr { + public: + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class UniversalBlePlatformChannel; + friend class UniversalBleCallbackChannel; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class UniversalBleScanResult { + public: + // Constructs an object setting all non-nullable fields. + explicit UniversalBleScanResult(const std::string& device_id); + + // Constructs an object setting all fields. + explicit UniversalBleScanResult( + const std::string& device_id, + const std::string* name, + const bool* is_paired, + const int64_t* rssi, + const std::vector* manufacturer_data, + const std::vector* manufacturer_data_head); + + const std::string& device_id() const; + void set_device_id(std::string_view value_arg); + + const std::string* name() const; + void set_name(const std::string_view* value_arg); + void set_name(std::string_view value_arg); + + const bool* is_paired() const; + void set_is_paired(const bool* value_arg); + void set_is_paired(bool value_arg); + + const int64_t* rssi() const; + void set_rssi(const int64_t* value_arg); + void set_rssi(int64_t value_arg); + + const std::vector* manufacturer_data() const; + void set_manufacturer_data(const std::vector* value_arg); + void set_manufacturer_data(const std::vector& value_arg); + + const std::vector* manufacturer_data_head() const; + void set_manufacturer_data_head(const std::vector* value_arg); + void set_manufacturer_data_head(const std::vector& value_arg); + + + private: + static UniversalBleScanResult FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBlePlatformChannelCodecSerializer; + friend class UniversalBleCallbackChannel; + friend class UniversalBleCallbackChannelCodecSerializer; + std::string device_id_; + std::optional name_; + std::optional is_paired_; + std::optional rssi_; + std::optional> manufacturer_data_; + std::optional> manufacturer_data_head_; + +}; + + +// Generated class from Pigeon that represents data sent in messages. +class UniversalBleService { + public: + // Constructs an object setting all non-nullable fields. + explicit UniversalBleService(const std::string& uuid); + + // Constructs an object setting all fields. + explicit UniversalBleService( + const std::string& uuid, + const flutter::EncodableList* characteristics); + + const std::string& uuid() const; + void set_uuid(std::string_view value_arg); + + const flutter::EncodableList* characteristics() const; + void set_characteristics(const flutter::EncodableList* value_arg); + void set_characteristics(const flutter::EncodableList& value_arg); + + + private: + static UniversalBleService FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBlePlatformChannelCodecSerializer; + friend class UniversalBleCallbackChannel; + friend class UniversalBleCallbackChannelCodecSerializer; + std::string uuid_; + std::optional characteristics_; + +}; + + +// Generated class from Pigeon that represents data sent in messages. +class UniversalBleCharacteristic { + public: + // Constructs an object setting all fields. + explicit UniversalBleCharacteristic( + const std::string& uuid, + const flutter::EncodableList& properties); + + const std::string& uuid() const; + void set_uuid(std::string_view value_arg); + + const flutter::EncodableList& properties() const; + void set_properties(const flutter::EncodableList& value_arg); + + + private: + static UniversalBleCharacteristic FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class UniversalBlePlatformChannel; + friend class UniversalBlePlatformChannelCodecSerializer; + friend class UniversalBleCallbackChannel; + friend class UniversalBleCallbackChannelCodecSerializer; + std::string uuid_; + flutter::EncodableList properties_; + +}; + +class UniversalBlePlatformChannelCodecSerializer : public flutter::StandardCodecSerializer { + public: + UniversalBlePlatformChannelCodecSerializer(); + inline static UniversalBlePlatformChannelCodecSerializer& GetInstance() { + static UniversalBlePlatformChannelCodecSerializer sInstance; + return sInstance; + } + + void WriteValue( + const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const override; + +}; + +// Flutter -> Native +// +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class UniversalBlePlatformChannel { + public: + UniversalBlePlatformChannel(const UniversalBlePlatformChannel&) = delete; + UniversalBlePlatformChannel& operator=(const UniversalBlePlatformChannel&) = delete; + virtual ~UniversalBlePlatformChannel() {} + virtual void GetBluetoothAvailabilityState(std::function reply)> result) = 0; + virtual void EnableBluetooth(std::function reply)> result) = 0; + virtual std::optional StartScan() = 0; + virtual std::optional StopScan() = 0; + virtual std::optional Connect(const std::string& device_id) = 0; + virtual std::optional Disconnect(const std::string& device_id) = 0; + virtual std::optional SetNotifiable( + const std::string& device_id, + const std::string& service, + const std::string& characteristic, + int64_t ble_input_property) = 0; + virtual void DiscoverServices( + const std::string& device_id, + std::function reply)> result) = 0; + virtual void ReadValue( + const std::string& device_id, + const std::string& service, + const std::string& characteristic, + std::function> reply)> result) = 0; + virtual void RequestMtu( + const std::string& device_id, + int64_t expected_mtu, + std::function reply)> result) = 0; + virtual 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) = 0; + virtual void IsPaired( + const std::string& device_id, + std::function reply)> result) = 0; + virtual std::optional Pair(const std::string& device_id) = 0; + virtual std::optional UnPair(const std::string& device_id) = 0; + virtual void GetConnectedDevices( + const flutter::EncodableList& with_services, + std::function reply)> result) = 0; + + // The codec used by UniversalBlePlatformChannel. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `UniversalBlePlatformChannel` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + UniversalBlePlatformChannel* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + UniversalBlePlatformChannel() = default; + +}; +class UniversalBleCallbackChannelCodecSerializer : public flutter::StandardCodecSerializer { + public: + UniversalBleCallbackChannelCodecSerializer(); + inline static UniversalBleCallbackChannelCodecSerializer& GetInstance() { + static UniversalBleCallbackChannelCodecSerializer sInstance; + return sInstance; + } + + void WriteValue( + const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const override; + +}; + +// Native -> Flutter +// +// Generated class from Pigeon that represents Flutter messages that can be called from C++. +class UniversalBleCallbackChannel { + public: + UniversalBleCallbackChannel(flutter::BinaryMessenger* binary_messenger); + static const flutter::StandardMessageCodec& GetCodec(); + void OnAvailabilityChanged( + int64_t state, + std::function&& on_success, + std::function&& on_error); + void OnPairStateChange( + const std::string& device_id, + bool is_paired, + const std::string* error, + std::function&& on_success, + std::function&& on_error); + void OnScanResult( + const UniversalBleScanResult& result, + std::function&& on_success, + std::function&& on_error); + void OnValueChanged( + const std::string& device_id, + const std::string& characteristic_id, + const std::vector& value, + std::function&& on_success, + std::function&& on_error); + void OnConnectionChanged( + const std::string& device_id, + int64_t state, + std::function&& on_success, + std::function&& on_error); + + private: + flutter::BinaryMessenger* binary_messenger_; +}; + +} // namespace universal_ble +#endif // PIGEON_UNIVERSALBLE_G_H_ diff --git a/windows/Utils.cpp b/windows/Utils.cpp new file mode 100644 index 0000000..da72b84 --- /dev/null +++ b/windows/Utils.cpp @@ -0,0 +1,130 @@ +#include "Utils.h" + +#include +#include +#include + +#include + +#if WDK_NTDDI_VERSION < NTDDI_WIN10_VB +#error "Windows SDK version before 10.0.19041.0 is not supported" +#elif WDK_NTDDI_VERSION == NTDDI_WIN10_VB +// For Windows SDK version before 10.0.19041.0, remap functions to post-10.0.19041.0 versions +#define WINRT_IMPL_CoGetApartmentType WINRT_CoGetApartmentType +#endif + +#define MAC_ADDRESS_STR_LENGTH (size_t)17 // Two chars per byte, 5 chars for colon + +namespace universal_ble +{ + + std::string _mac_address_to_str(uint64_t mac_address) + { + uint8_t *mac_ptr = (uint8_t *)&mac_address; + char mac_str[MAC_ADDRESS_STR_LENGTH + 1] = {0}; + snprintf(mac_str, MAC_ADDRESS_STR_LENGTH + 1, "%02x:%02x:%02x:%02x:%02x:%02x", mac_ptr[5], mac_ptr[4], mac_ptr[3], + mac_ptr[2], mac_ptr[1], mac_ptr[0]); + return std::string(mac_str); + } + + uint64_t _str_to_mac_address(std::string mac_str) + { + // TODO: Validate input - Expected Format: XX:XX:XX:XX:XX:XX + uint64_t mac_address_number = 0; + uint8_t *mac_ptr = (uint8_t *)&mac_address_number; + sscanf_s(mac_str.c_str(), "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", &mac_ptr[5], &mac_ptr[4], &mac_ptr[3], + &mac_ptr[2], &mac_ptr[1], &mac_ptr[0]); + return mac_address_number; + } + + winrt::guid uuid_to_guid(const std::string &uuid) + { + // TODO: Add proper cleanup / validation + std::stringstream helper; + for (int i = 0; i < uuid.length(); i++) + { + if (uuid[i] != '-') + { + helper << uuid[i]; + } + } + std::string clean_uuid = helper.str(); + winrt::guid guid; + uint64_t *data4_ptr = (uint64_t *)guid.Data4; + + guid.Data1 = static_cast(std::strtoul(clean_uuid.substr(0, 8).c_str(), nullptr, 16)); + guid.Data2 = static_cast(std::strtoul(clean_uuid.substr(8, 4).c_str(), nullptr, 16)); + guid.Data3 = static_cast(std::strtoul(clean_uuid.substr(12, 4).c_str(), nullptr, 16)); + *data4_ptr = _byteswap_uint64(std::strtoull(clean_uuid.substr(16, 16).c_str(), nullptr, 16)); + + return guid; + } + + std::string guid_to_uuid(const winrt::guid &guid) + { + std::stringstream helper; + // TODO: It might be cleaner to use snprintf instead of string streams. + + for (uint32_t i = 0; i < 4; i++) + { + // * NOTE: We're performing a byte swap! + helper << std::hex << std::setw(2) << std::setfill('0') << (int)((uint8_t *)&guid.Data1)[3 - i]; + } + helper << '-'; + for (uint32_t i = 0; i < 2; i++) + { + // * NOTE: We're performing a byte swap! + helper << std::hex << std::setw(2) << std::setfill('0') << (int)((uint8_t *)&guid.Data2)[1 - i]; + } + helper << '-'; + for (uint32_t i = 0; i < 2; i++) + { + // * NOTE: We're performing a byte swap! + helper << std::hex << std::setw(2) << std::setfill('0') << (int)((uint8_t *)&guid.Data3)[1 - i]; + } + helper << '-'; + for (uint32_t i = 0; i < 2; i++) + { + helper << std::hex << std::setw(2) << std::setfill('0') << (int)guid.Data4[i]; + } + helper << '-'; + for (uint32_t i = 0; i < 6; i++) + { + helper << std::hex << std::setw(2) << std::setfill('0') << (int)guid.Data4[2 + i]; + } + return helper.str(); + } + + std::vector to_bytevc(IBuffer buffer) + { + auto reader = DataReader::FromBuffer(buffer); + auto result = std::vector(reader.UnconsumedBufferLength()); + reader.ReadBytes(result); + return result; + } + + IBuffer from_bytevc(std::vector bytes) + { + auto writer = DataWriter(); + writer.WriteBytes(bytes); + return writer.DetachBuffer(); + } + + std::string to_hexstring(std::vector bytes) + { + auto ss = std::stringstream(); + for (auto b : bytes) + ss << std::setw(2) << std::setfill('0') << std::hex << static_cast(b); + return ss.str(); + } + + std::string to_uuidstr(winrt::guid guid) + { + char chars[36 + 1]; + sprintf_s(chars, "%08x-%04hx-%04hx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx", + guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], + guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + return std::string{chars}; + } + +} // namespace SimpleBLE diff --git a/windows/Utils.h b/windows/Utils.h new file mode 100644 index 0000000..a5c588e --- /dev/null +++ b/windows/Utils.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include + +#include "winrt/Windows.Foundation.h" +#include "winrt/Windows.Storage.Streams.h" +#include "winrt/base.h" +#include "UniversalBle.g.h" + +using namespace winrt::Windows; +using namespace winrt::Windows::Storage::Streams; + +constexpr uint32_t TEN_SECONDS_IN_MSECS = 10000; + +namespace universal_ble +{ + + std::string _mac_address_to_str(uint64_t mac_address); + uint64_t _str_to_mac_address(std::string mac_address); + + winrt::guid uuid_to_guid(const std::string &uuid); + std::string guid_to_uuid(const winrt::guid &guid); + + std::vector to_bytevc(IBuffer buffer); + IBuffer from_bytevc(std::vector bytes); + std::string to_hexstring(std::vector bytes); + + std::string to_uuidstr(winrt::guid guid); + + /// Structs for passing data between background to ui thread + struct ConnectionStateStruct + { + std::string deviceId; + int64_t connectionState; + ConnectionStateStruct(std::string deviceId, int64_t connectionState) + : deviceId(deviceId), connectionState(connectionState) {} + }; + + struct PairStateStruct + { + std::string deviceId; + bool isPaired; + std::string errorMessage; + PairStateStruct(std::string deviceId, bool isPaired, std::string errorMessage) + : deviceId(deviceId), isPaired(isPaired), errorMessage(errorMessage) {} + }; + + struct ValueChangeStruct + { + std::string deviceId; + std::string characteristicId; + std::vector value; + ValueChangeStruct(std::string deviceId, std::string characteristicId, std::vector value) + : deviceId(deviceId), characteristicId(characteristicId), value(value) {} + }; + + /// To call async functions synchronously + template + static auto async_get(async_t const &async) + { + if (async.Status() == Foundation::AsyncStatus::Started) + { + wait_for_completed(async, TEN_SECONDS_IN_MSECS); + } + try + { + return async.GetResults(); + } + catch (const winrt::hresult_error &err) + { + throw FlutterError(winrt::to_string(err.message())); + } + catch (const std::exception &err) + { + throw FlutterError(err.what()); + } + } + +} // namespace universal_ble \ No newline at end of file diff --git a/windows/include/universal_ble/universal_ble_plugin_c_api.h b/windows/include/universal_ble/universal_ble_plugin_c_api.h new file mode 100644 index 0000000..37dd653 --- /dev/null +++ b/windows/include/universal_ble/universal_ble_plugin_c_api.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_UNIVERSAL_BLE_PLUGIN_C_API_H_ +#define FLUTTER_PLUGIN_UNIVERSAL_BLE_PLUGIN_C_API_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void UniversalBlePluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_UNIVERSAL_BLE_PLUGIN_C_API_H_ diff --git a/windows/universal_ble_plugin.cpp b/windows/universal_ble_plugin.cpp new file mode 100644 index 0000000..17eb51c --- /dev/null +++ b/windows/universal_ble_plugin.cpp @@ -0,0 +1,1002 @@ +#include "universal_ble_plugin.h" +#include + +#include + +#include +#include +#include +#include +#include +#include +#include "UniversalBle.g.h" +#include "Utils.h" +#include "universal_enum.h" +#include + +#define WM_AVAILABILITY_CHANGE WM_USER + 101 +#define WM_PAIR_CHANGE WM_USER + 102 +#define WM_SCAN_RESULT WM_USER + 103 +#define WM_VALUE_CHANGE WM_USER + 104 +#define WM_CONNECTION_CHANGE WM_USER + 105 + +namespace universal_ble +{ + using universal_ble::ErrorOr; + using universal_ble::UniversalBleCallbackChannel; + using universal_ble::UniversalBlePlatformChannel; + using universal_ble::UniversalBleScanResult; + + std::unique_ptr callbackChannel; + std::map characteristicsTokens{}; + + union uint16_t_union + { + uint16_t uint16; + byte bytes[sizeof(uint16_t)]; + }; + + void UniversalBlePlugin::RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar) + { + auto plugin = std::make_unique(registrar); + UniversalBlePlatformChannel::SetUp(registrar->messenger(), plugin.get()); + callbackChannel = std::make_unique(registrar->messenger()); + registrar->AddPlugin(std::move(plugin)); + } + + UniversalBlePlugin::UniversalBlePlugin(flutter::PluginRegistrarWindows *registrar) : registrar_(registrar) + { + if (window_proc_delegate_id_ == -1) + { + window_proc_delegate_id_ = registrar_->RegisterTopLevelWindowProcDelegate( + std::bind( + &UniversalBlePlugin::WindowProcDelegate, this, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4) + + ); + } + InitializeAsync(); + } + + UniversalBlePlugin::~UniversalBlePlugin() + { + if (window_proc_delegate_id_ != -1) + { + registrar_->UnregisterTopLevelWindowProcDelegate( + static_cast(window_proc_delegate_id_)); + } + } + + HWND UniversalBlePlugin::GetWindow() + { + return ::GetAncestor(registrar_->GetView()->GetNativeWindow(), GA_ROOT); + } + + /// Background Messages Handler + std::optional UniversalBlePlugin::WindowProcDelegate(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) + { + switch (message) + { + case WM_SCAN_RESULT: + { + UniversalBleScanResult *scanResult = reinterpret_cast(wp); + callbackChannel->OnScanResult(*scanResult, SuccessCallback, ErrorCallback); + delete scanResult; + return 0; + } + case WM_AVAILABILITY_CHANGE: + { + callbackChannel->OnAvailabilityChanged(static_cast(wp), SuccessCallback, ErrorCallback); + return 0; + } + case WM_CONNECTION_CHANGE: + { + ConnectionStateStruct *connectionStateStruct = reinterpret_cast(wp); + callbackChannel->OnConnectionChanged(connectionStateStruct->deviceId, connectionStateStruct->connectionState, SuccessCallback, ErrorCallback); + delete connectionStateStruct; + return 0; + } + case WM_VALUE_CHANGE: + { + ValueChangeStruct *valueChangeStruct = reinterpret_cast(wp); + callbackChannel->OnValueChanged(valueChangeStruct->deviceId, valueChangeStruct->characteristicId, valueChangeStruct->value, SuccessCallback, ErrorCallback); + delete valueChangeStruct; + return 0; + } + case WM_PAIR_CHANGE: + { + PairStateStruct *pairStateStruct = reinterpret_cast(wp); + callbackChannel->OnPairStateChange(pairStateStruct->deviceId, pairStateStruct->isPaired, &pairStateStruct->errorMessage, SuccessCallback, ErrorCallback); + delete pairStateStruct; + return 0; + } + } + return std::nullopt; + } + + void UniversalBlePlugin::PostConnectionUpdate(uint64_t bluetoothAddress, ConnectionState connectionState) + { + ConnectionStateStruct *connectionStateStruct = new ConnectionStateStruct(_mac_address_to_str(bluetoothAddress), static_cast(connectionState)); + ::PostMessage(GetWindow(), WM_CONNECTION_CHANGE, reinterpret_cast(connectionStateStruct), 0); + } + + // UniversalBlePlatformChannel implementation. + void UniversalBlePlugin::GetBluetoothAvailabilityState(std::function reply)> result) + { + if (!bluetoothRadio) + result(static_cast(AvailabilityState::unsupported)); + else + result(static_cast(getAvailabilityStateFromRadio(bluetoothRadio.State()))); + }; + + void UniversalBlePlugin::EnableBluetooth(std::function reply)> result) + { + if (!bluetoothRadio) + { + result(FlutterError("Bluetooth is not available")); + return; + } + if (bluetoothRadio.State() == RadioState::On) + { + result(true); + return; + } + auto async_c = bluetoothRadio.SetStateAsync(RadioState::On); + async_c.Completed([&, result](IAsyncOperation const &sender, AsyncStatus const args) + { + auto radioAccessStatus = sender.GetResults(); + if (radioAccessStatus == RadioAccessStatus::Allowed) + { + result(true); + } + else + { + result(FlutterError("Failed to enable bluetooth")); + } }); + } + + std::optional UniversalBlePlugin::StartScan() + { + if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) + { + if (!bluetoothLEWatcher) + { + bluetoothLEWatcher = BluetoothLEAdvertisementWatcher(); + bluetoothLEWatcherReceivedToken = bluetoothLEWatcher.Received({this, &UniversalBlePlugin::BluetoothLEWatcher_Received}); + } + bluetoothLEWatcher.Start(); + return std::nullopt; + } + else + { + return FlutterError("Bluetooth is not available"); + } + }; + + std::optional UniversalBlePlugin::StopScan() + { + if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) + { + if (bluetoothLEWatcher) + { + bluetoothLEWatcher.Stop(); + bluetoothLEWatcher.Received(bluetoothLEWatcherReceivedToken); + } + bluetoothLEWatcher = nullptr; + return std::nullopt; + } + else + { + return FlutterError("Bluetooth is not available"); + } + }; + + std::optional UniversalBlePlugin::Connect(const std::string &device_id) + { + auto isConnectable = deviceConnectableStatus[_str_to_mac_address(device_id)]; + if (isConnectable == false) + { + return FlutterError("Device is not connectable"); + } + + ConnectAsync(_str_to_mac_address(device_id)); + return std::nullopt; + }; + + std::optional UniversalBlePlugin::Disconnect(const std::string &device_id) + { + auto deviceAddress = _str_to_mac_address(device_id); + CleanConnection(deviceAddress); + PostConnectionUpdate(deviceAddress, ConnectionState::disconnected); + return std::nullopt; + }; + + void UniversalBlePlugin::DiscoverServices( + const std::string &device_id, + std::function reply)> result) + { + try + { + auto it = connectedDevices.find(_str_to_mac_address(device_id)); + if (it == connectedDevices.end()) + { + result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); + return; + } + auto deviceAgent = *it->second; + DiscoverServicesAsync(deviceAgent, result); + } + catch (const FlutterError &err) + { + return result(err); + } + catch (...) + { + return result(FlutterError("Unknown error")); + } + } + + std::optional UniversalBlePlugin::SetNotifiable( + const std::string &device_id, + const std::string &service, + const std::string &characteristic, + int64_t ble_input_property) + { + try + { + auto it = connectedDevices.find(_str_to_mac_address(device_id)); + if (it == connectedDevices.end()) + return FlutterError("IllegalArgument", "Unknown devicesId:" + device_id); + auto bluetoothAgent = *it->second; + gatt_characteristic_t &gatt_characteristic_holder = bluetoothAgent._fetch_characteristic(service, characteristic); + GattCharacteristic gattCharacteristic = gatt_characteristic_holder.obj; + auto descriptorValue = GattClientCharacteristicConfigurationDescriptorValue::None; + auto properties = gattCharacteristic.CharacteristicProperties(); + + if (ble_input_property == static_cast(BleInputProperty::notification)) + { + descriptorValue = GattClientCharacteristicConfigurationDescriptorValue::Notify; + if ((properties & GattCharacteristicProperties::Notify) == GattCharacteristicProperties::None) + { + return FlutterError("Characteristic does not support notify"); + } + } + else if (ble_input_property == static_cast(BleInputProperty::indication)) + { + descriptorValue = GattClientCharacteristicConfigurationDescriptorValue::Indicate; + if ((properties & GattCharacteristicProperties::Indicate) == GattCharacteristicProperties::None) + { + return FlutterError("Characteristic does not support indicate"); + } + } + + SetNotifiableAsync(bluetoothAgent, service, characteristic, descriptorValue); + return std::nullopt; + } + catch (const FlutterError &err) + { + return err; + } + catch (...) + { + return FlutterError("Unknown error"); + } + }; + + void UniversalBlePlugin::ReadValue( + const std::string &device_id, + const std::string &service, + const std::string &characteristic, + std::function> reply)> result) + { + try + { + auto it = connectedDevices.find(_str_to_mac_address(device_id)); + if (it == connectedDevices.end()) + { + result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); + return; + } + auto bluetoothAgent = *it->second; + gatt_characteristic_t &gatt_characteristic_holder = bluetoothAgent._fetch_characteristic(service, characteristic); + GattCharacteristic gattCharacteristic = gatt_characteristic_holder.obj; + auto properties = gattCharacteristic.CharacteristicProperties(); + if ((properties & GattCharacteristicProperties::Read) == GattCharacteristicProperties::None) + { + result(FlutterError("Characteristic does not support read")); + return; + } + auto async_c = gattCharacteristic.ReadValueAsync(Devices::Bluetooth::BluetoothCacheMode::Uncached); + async_c.Completed([&, result](IAsyncOperation const &sender, AsyncStatus const args) + { + auto readValueResult = sender.GetResults(); + switch (readValueResult.Status()) + { + case GenericAttributeProfile::GattCommunicationStatus::Success: + result(to_bytevc(readValueResult.Value())); + return; + case GenericAttributeProfile::GattCommunicationStatus::Unreachable: + result(FlutterError("Unreachable","Failed to read value")); + return; + case GenericAttributeProfile::GattCommunicationStatus::ProtocolError: + result(FlutterError("ProtocolError","Failed to read value")); + return; + case GenericAttributeProfile::GattCommunicationStatus::AccessDenied: + result(FlutterError("AccessDenied","Failed to read value")); + return; + default: + result(FlutterError("Failed","Failed to read value")); + return; + } }); + } + catch (const FlutterError &err) + { + return result(err); + } + catch (...) + { + return result(FlutterError("Unknown error")); + } + } + + void UniversalBlePlugin::WriteValue( + const std::string &device_id, + const std::string &service, + const std::string &characteristic, + const std::vector &value, + int64_t ble_output_property, + std::function reply)> result) + { + try + { + auto it = connectedDevices.find(_str_to_mac_address(device_id)); + if (it == connectedDevices.end()) + { + result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); + return; + } + auto bluetoothAgent = *it->second; + gatt_characteristic_t &gatt_characteristic_holder = bluetoothAgent._fetch_characteristic(service, characteristic); + GattCharacteristic gattCharacteristic = gatt_characteristic_holder.obj; + auto properties = gattCharacteristic.CharacteristicProperties(); + auto writeOption = GattWriteOption::WriteWithResponse; + if (ble_output_property == static_cast(BleOutputProperty::withoutResponse)) + { + writeOption = GattWriteOption::WriteWithoutResponse; + if ((properties & GattCharacteristicProperties::WriteWithoutResponse) == GattCharacteristicProperties::None) + { + result(FlutterError("Characteristic does not support WriteWithoutResponse")); + return; + } + } + else + { + if ((properties & GattCharacteristicProperties::Write) == GattCharacteristicProperties::None) + { + result(FlutterError("Characteristic does not support Write")); + return; + } + } + auto async_c = gatt_characteristic_holder.obj.WriteValueAsync(from_bytevc(value), writeOption); + async_c.Completed([&, result](IAsyncOperation const &sender, AsyncStatus const args) + { + auto writeResult = sender.GetResults(); + switch (writeResult) + { + case GenericAttributeProfile::GattCommunicationStatus::Success: + result(std::nullopt); + return; + case GenericAttributeProfile::GattCommunicationStatus::Unreachable: + result(FlutterError("Unreachable", "Failed to write value")); + return; + case GenericAttributeProfile::GattCommunicationStatus::ProtocolError: + result(FlutterError("ProtocolError", "Failed to write value")); + return; + case GenericAttributeProfile::GattCommunicationStatus::AccessDenied: + result(FlutterError("AccessDenied", "Failed to write value")); + return; + default: + result(FlutterError("Failed", "Failed to write value")); + return; + } }); + } + catch (const FlutterError &err) + { + result(err); + } + catch (...) + { + result(FlutterError("Unknown error")); + } + } + + void UniversalBlePlugin::RequestMtu( + const std::string &device_id, + int64_t expected_mtu, + std::function reply)> result) + { + try + { + auto it = connectedDevices.find(_str_to_mac_address(device_id)); + if (it == connectedDevices.end()) + { + result(FlutterError("IllegalArgument", "Unknown devicesId:" + device_id)); + return; + } + auto bluetoothAgent = *it->second; + auto async_c = GattSession::FromDeviceIdAsync(bluetoothAgent.device.BluetoothDeviceId()); + async_c.Completed([&, result](IAsyncOperation const &sender, AsyncStatus const args) + { + auto gattSession = sender.GetResults(); + result((int64_t)gattSession.MaxPduSize()); }); + } + catch (const FlutterError &err) + { + result(err); + } + } + + void UniversalBlePlugin::IsPaired( + const std::string &device_id, + std::function reply)> result) + { + IsPairedAsync(device_id, result); + }; + + std::optional UniversalBlePlugin::Pair(const std::string &device_id) + { + try + { + auto device = async_get(BluetoothLEDevice::FromBluetoothAddressAsync(_str_to_mac_address(device_id))); + auto deviceInformation = device.DeviceInformation(); + bool isPaired = deviceInformation.Pairing().IsPaired(); + if (isPaired) + return FlutterError("Device is already paired"); + bool canPair = deviceInformation.Pairing().CanPair(); + if (!canPair) + return FlutterError("Device is not pairable"); + + auto customPairing = deviceInformation.Pairing().Custom(); + winrt::event_token token = customPairing.PairingRequested([this](const Enumeration::DeviceInformationCustomPairing &sender, const Enumeration::DevicePairingRequestedEventArgs &eventArgs) + { + // eventArgs.AcceptWithPasswordCredential(nullptr, nullptr); + // eventArgs.Pin(); + // Accept all pairing request + eventArgs.Accept(); }); + // DevicePairingKinds => None, ConfirmOnly, DisplayPin, ProvidePin, ConfirmPinMatch, ProvidePasswordCredential + // DevicePairingProtectionLevel => Default, None, Encryption, EncryptionAndAuthentication + auto async_c = customPairing.PairAsync( + Enumeration::DevicePairingKinds::ConfirmOnly, + Enumeration::DevicePairingProtectionLevel::None); + async_c.Completed([this, customPairing, token, device_id](IAsyncOperation const &sender, AsyncStatus const args) + { + auto result = sender.GetResults(); + customPairing.PairingRequested(token); + auto isPaired = result.Status() == Enumeration::DevicePairingResultStatus::Paired; + // Post to main thread + PairStateStruct *pairStateStruct = new PairStateStruct(device_id, isPaired, parsePairingFailError(result)); + ::PostMessage(GetWindow(), WM_PAIR_CHANGE, reinterpret_cast(pairStateStruct), 0); }); + + return std::nullopt; + } + catch (const FlutterError &err) + { + return err; + } + }; + + std::optional UniversalBlePlugin::UnPair(const std::string &device_id) + { + try + { + auto device = async_get(BluetoothLEDevice::FromBluetoothAddressAsync(_str_to_mac_address(device_id))); + auto deviceInformation = device.DeviceInformation(); + bool isPaired = deviceInformation.Pairing().IsPaired(); + if (!isPaired) + return FlutterError("Device is not paired"); + + auto deviceUnpairingResult = async_get(deviceInformation.Pairing().UnpairAsync()); + auto unpairingStatus = deviceUnpairingResult.Status(); + if (unpairingStatus == Enumeration::DeviceUnpairingResultStatus::Failed) + return FlutterError("Failed to unpair device"); + if (unpairingStatus == Enumeration::DeviceUnpairingResultStatus::AccessDenied) + return FlutterError("Access denied"); + if (unpairingStatus == Enumeration::DeviceUnpairingResultStatus::AlreadyUnpaired) + return FlutterError("Device is already unpaired"); + if (unpairingStatus == Enumeration::DeviceUnpairingResultStatus::OperationAlreadyInProgress) + return FlutterError("OperationAlreadyInProgress"); + return std::nullopt; + } + catch (const FlutterError &err) + { + return err; + } + }; + + void UniversalBlePlugin::GetConnectedDevices( + const flutter::EncodableList &with_services, + std::function reply)> result) + { + std::vector with_services_str = std::vector(); + for (const auto &item : with_services) + { + auto serviceId = std::get(item); + with_services_str.push_back(serviceId); + } + GetConnectedDevicesAsync(with_services_str, result); + } + + /// Helper Methods + winrt::fire_and_forget UniversalBlePlugin::InitializeAsync() + { + auto bluetoothAdapter = co_await BluetoothAdapter::GetDefaultAsync(); + bluetoothRadio = co_await bluetoothAdapter.GetRadioAsync(); + if (bluetoothRadio) + { + radioStateChangedRevoker = bluetoothRadio.StateChanged(winrt::auto_revoke, {this, &UniversalBlePlugin::Radio_StateChanged}); + } + } + + AvailabilityState UniversalBlePlugin::getAvailabilityStateFromRadio(RadioState radioState) + { + auto state = [=]() -> AvailabilityState + { + if (radioState == RadioState::Unknown) + { + return AvailabilityState::unknown; + } + else if (radioState == RadioState::Off) + { + return AvailabilityState::poweredOff; + } + else if (radioState == RadioState::On) + { + return AvailabilityState::poweredOn; + } + else if (radioState == RadioState::Disabled) + { + return AvailabilityState::unsupported; + } + else + { + return AvailabilityState::unknown; + } + }(); + return state; + } + + void UniversalBlePlugin::Radio_StateChanged(Radio radio, IInspectable args) + { + auto radioState = !radio ? RadioState::Disabled : radio.State(); + if (oldRadioState == radioState) + { + return; + } + oldRadioState = radioState; + auto state = getAvailabilityStateFromRadio(radioState); + ::PostMessage(GetWindow(), WM_AVAILABILITY_CHANGE, static_cast(state), 0); + } + + std::vector parseManufacturerDataHead(BluetoothLEAdvertisement advertisement, std::string deviceId) + { + try + { + if (advertisement.ManufacturerData().Size() == 0) + { + return std::vector(); + } + auto manufacturerData = advertisement.ManufacturerData().GetAt(0); + // FIXME Compat with REG_DWORD_BIG_ENDIAN + uint8_t *prefix = uint16_t_union{manufacturerData.CompanyId()}.bytes; + auto result = std::vector{prefix, prefix + sizeof(uint16_t_union)}; + + auto data = to_bytevc(manufacturerData.Data()); + result.insert(result.end(), data.begin(), data.end()); + return result; + } + catch (...) + { + std::cout << "Error in parsing manufacturer data: " << deviceId << std::endl; + return std::vector(); + } + } + + void UniversalBlePlugin::BluetoothLEWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args) + { + try + { + auto deviceId = _mac_address_to_str(args.BluetoothAddress()); + auto universalScanResult = new UniversalBleScanResult(deviceId); + hstring localName = args.Advertisement().LocalName(); + if (!localName.empty()) + universalScanResult->set_name(winrt::to_string(localName)); + + universalScanResult->set_manufacturer_data_head(parseManufacturerDataHead(args.Advertisement(), deviceId)); + universalScanResult->set_rssi(args.RawSignalStrengthInDBm()); + // Cache connectable status + deviceConnectableStatus[_str_to_mac_address(deviceId)] = args.IsConnectable(); + ::PostMessage(GetWindow(), WM_SCAN_RESULT, reinterpret_cast(universalScanResult), 0); + } + catch (...) + { + std::cout << "ScanResultErrorInParsing" << std::endl; + } + } + + std::string GattCommunicationStatusToString(GattCommunicationStatus status) + { + switch (status) + { + case GattCommunicationStatus::Success: + return "Success"; + case GattCommunicationStatus::Unreachable: + return "Unreachable"; + case GattCommunicationStatus::ProtocolError: + return "ProtocolError"; + case GattCommunicationStatus::AccessDenied: + return "AccessDenied"; + default: + return "Unknown"; + } + } + + winrt::fire_and_forget UniversalBlePlugin::ConnectAsync(uint64_t bluetoothAddress) + { + auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetoothAddress); + auto servicesResult = co_await device.GetGattServicesAsync((BluetoothCacheMode::Uncached)); + if (servicesResult.Status() != GattCommunicationStatus::Success) + { + PostConnectionUpdate(bluetoothAddress, ConnectionState::disconnected); + co_return; + } + + std::map gatt_map_; + auto gatt_services = servicesResult.Services(); + for (GattDeviceService &&service : gatt_services) + { + gatt_service_t gatt_service; + gatt_service.obj = service; + std::string service_uuid = guid_to_uuid(service.Uuid()); + auto characteristics_result = co_await service.GetCharacteristicsAsync(BluetoothCacheMode::Uncached); + if (characteristics_result.Status() != GattCommunicationStatus::Success) + { + std::cout << "Failed to get characteristics for service: " << service_uuid << ", With Status: " << GattCommunicationStatusToString(characteristics_result.Status()) << std::endl; + continue; + // PostConnectionUpdate(bluetoothAddress, ConnectionState::disconnected); + // co_return; + } + auto gatt_characteristics = characteristics_result.Characteristics(); + for (GattCharacteristic &&characteristic : gatt_characteristics) + { + gatt_characteristic_t gatt_characteristic; + gatt_characteristic.obj = characteristic; + std::string characteristic_uuid = guid_to_uuid(characteristic.Uuid()); + gatt_service.characteristics.emplace(characteristic_uuid, std::move(gatt_characteristic)); + } + gatt_map_.emplace(service_uuid, std::move(gatt_service)); + } + + auto connnectionStatusChangedToken = device.ConnectionStatusChanged({this, &UniversalBlePlugin::BluetoothLEDevice_ConnectionStatusChanged}); + auto deviceAgent = std::make_unique(device, connnectionStatusChangedToken, gatt_map_); + auto pair = std::make_pair(bluetoothAddress, std::move(deviceAgent)); + connectedDevices.insert(std::move(pair)); + PostConnectionUpdate(bluetoothAddress, ConnectionState::connected); + } + + void UniversalBlePlugin::BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args) + { + if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) + { + CleanConnection(sender.BluetoothAddress()); + PostConnectionUpdate(sender.BluetoothAddress(), ConnectionState::disconnected); + } + } + + void UniversalBlePlugin::CleanConnection(uint64_t bluetoothAddress) + { + auto node = connectedDevices.extract(bluetoothAddress); + if (!node.empty()) + { + auto deviceAgent = std::move(node.mapped()); + deviceAgent->device.ConnectionStatusChanged(deviceAgent->connnectionStatusChangedToken); + // Clean up all characteristics tokens + for (auto &servicePair : deviceAgent->gatt_map_) + { + auto &service = servicePair.second; + for (auto &characteristicPair : service.characteristics) + { + gatt_characteristic_t &characteristic = characteristicPair.second; + auto gattCharacteristic = characteristic.obj; + auto uniqTokenKey = to_uuidstr(gattCharacteristic.Uuid()) + _mac_address_to_str(gattCharacteristic.Service().Device().BluetoothAddress()); + if (characteristicsTokens.count(uniqTokenKey) != 0) + { + characteristic.obj.ValueChanged(characteristicsTokens[uniqTokenKey]); + characteristicsTokens[uniqTokenKey] = {0}; + } + } + } + deviceAgent->gatt_map_.clear(); + } + } + + winrt::fire_and_forget UniversalBlePlugin::GetConnectedDevicesAsync( + std::vector with_services, + std::function reply)> result) + { + try + { + auto selector = BluetoothLEDevice::GetDeviceSelectorFromConnectionStatus(BluetoothConnectionStatus::Connected); + Enumeration::DeviceInformationCollection devices = co_await Enumeration::DeviceInformation::FindAllAsync(selector); + flutter::EncodableList results = flutter::EncodableList(); + for (auto &&deviceInfo : devices) + { + BluetoothLEDevice device = co_await BluetoothLEDevice::FromIdAsync(deviceInfo.Id()); + auto deviceId = _mac_address_to_str(device.BluetoothAddress()); + // Filter by services + if (!with_services.empty()) + { + auto serviceResult = co_await device.GetGattServicesAsync(BluetoothCacheMode::Cached); + if (serviceResult.Status() == GattCommunicationStatus::Success) + { + bool hasService = false; + for (auto service : serviceResult.Services()) + { + std::string serviceUUID = to_uuidstr(service.Uuid()); + if (std::find(with_services.begin(), with_services.end(), serviceUUID) != with_services.end()) + { + hasService = true; + break; + } + } + if (!hasService) + continue; + } + } + // Add to results, if pass all filters + auto universalScanResult = UniversalBleScanResult(deviceId); + universalScanResult.set_name(winrt::to_string(deviceInfo.Name())); + universalScanResult.set_is_paired(deviceInfo.Pairing().IsPaired()); + results.push_back(flutter::CustomEncodableValue(universalScanResult)); + deviceConnectableStatus[_str_to_mac_address(deviceId)] = true; + } + result(results); + } + catch (...) + { + result(FlutterError("Unknown error")); + } + } + + winrt::fire_and_forget UniversalBlePlugin::DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, std::function reply)> result) + { + try + { + auto deviceId = _mac_address_to_str(bluetoothDeviceAgent.device.BluetoothAddress()); + auto serviceResult = co_await bluetoothDeviceAgent.device.GetGattServicesAsync(); + if (serviceResult.Status() != GattCommunicationStatus::Success) + { + result(FlutterError("DiscoverServiceError: No services found for device")); + co_return; + } + auto services = serviceResult.Services(); + auto universalServices = flutter::EncodableList(); + for (auto service : serviceResult.Services()) + { + auto characteristicResult = co_await service.GetCharacteristicsAsync(); + flutter::EncodableList universalCharacteristics; + if (characteristicResult.Status() == GattCommunicationStatus::Success) + { + for (auto c : characteristicResult.Characteristics()) + { + flutter::EncodableList properties = flutter::EncodableList(); + auto propertiesValue = c.CharacteristicProperties(); + if ((propertiesValue & GattCharacteristicProperties::Broadcast) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::broadcast)); + } + if ((propertiesValue & GattCharacteristicProperties::Read) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::read)); + } + if ((propertiesValue & GattCharacteristicProperties::Write) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::write)); + } + if ((propertiesValue & GattCharacteristicProperties::WriteWithoutResponse) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::writeWithoutResponse)); + } + if ((propertiesValue & GattCharacteristicProperties::Notify) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::notify)); + } + if ((propertiesValue & GattCharacteristicProperties::Indicate) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::indicate)); + } + if ((propertiesValue & GattCharacteristicProperties::AuthenticatedSignedWrites) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::authenticatedSignedWrites)); + } + if ((propertiesValue & GattCharacteristicProperties::ExtendedProperties) != GattCharacteristicProperties::None) + { + properties.push_back(static_cast(CharacteristicProperty::extendedProperties)); + } + universalCharacteristics.push_back( + flutter::CustomEncodableValue(UniversalBleCharacteristic(to_uuidstr(c.Uuid()), properties))); + } + } + else + { + std::cout << "Failed to get characteristics for service: " << to_uuidstr(service.Uuid()) << ", With Status: " << GattCommunicationStatusToString(characteristicResult.Status()) << std::endl; + } + auto universalBleService = UniversalBleService(to_uuidstr(service.Uuid())); + universalBleService.set_characteristics(universalCharacteristics); + universalServices.push_back(flutter::CustomEncodableValue(universalBleService)); + } + result(universalServices); + } + catch (...) + { + result(FlutterError("DiscoverServiceError: Unknown error")); + std::cerr << "DiscoverServiceError: Unknown error" << '\n'; + } + } + + winrt::fire_and_forget UniversalBlePlugin::IsPairedAsync( + std::string device_id, + std::function reply)> result) + { + try + { + auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(_str_to_mac_address(device_id)); + bool isPaired = device.DeviceInformation().Pairing().IsPaired(); + result(isPaired); + } + catch (...) + { + std::cout << "IsPairedAsync: Error " << std::endl; + result(FlutterError("Unknown error")); + } + }; + + winrt::fire_and_forget UniversalBlePlugin::SetNotifiableAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, const std::string &service, + const std::string &characteristic, GattClientCharacteristicConfigurationDescriptorValue descriptorValue) + { + gatt_characteristic_t &gatt_characteristic_holder = bluetoothDeviceAgent._fetch_characteristic(service, characteristic); + GattCharacteristic gattCharacteristic = gatt_characteristic_holder.obj; + + auto uuid = to_uuidstr(gattCharacteristic.Uuid()); + // Write to the descriptor. + try + { + auto status = co_await gattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(descriptorValue); + switch (status) + { + case GattCommunicationStatus::Success: + break; + case GattCommunicationStatus::Unreachable: + std::cout << "FailedToSubscribe: Unreachable" << to_uuidstr(gattCharacteristic.Uuid()) << std::endl; + break; + case GattCommunicationStatus::ProtocolError: + std::cout << "FailedToSubscribe: ProtocolError" << to_uuidstr(gattCharacteristic.Uuid()) << std::endl; + break; + case GattCommunicationStatus::AccessDenied: + std::cout << "FailedToSubscribe: AccessDenied" << to_uuidstr(gattCharacteristic.Uuid()) << std::endl; + break; + default: + std::cout << "FailedToSubscribe: Unknown" << to_uuidstr(gattCharacteristic.Uuid()) << std::endl; + } + if (status != GattCommunicationStatus::Success) + co_return; + } + catch (...) + { + std::cout << "FailedToPerformThisOperationOn: " << to_uuidstr(gattCharacteristic.Uuid()) << std::endl; + co_return; + } + // create uniqKey with uuid and deviceId + // TODO: store token key in gatt_characteristic_t struct instead of using map + auto uniqTokenKey = uuid + _mac_address_to_str(gattCharacteristic.Service().Device().BluetoothAddress()); + + // Register/UnRegister handler for the ValueChanged event. + if (descriptorValue == GattClientCharacteristicConfigurationDescriptorValue::None) + { + if (characteristicsTokens.count(uniqTokenKey) != 0) + { + gattCharacteristic.ValueChanged(characteristicsTokens[uniqTokenKey]); + characteristicsTokens[uuid] = {0}; + std::cout << "Unsubscribed " << to_uuidstr(gattCharacteristic.Uuid()) << std::endl; + } + } + else + { + // If a notification for the given characteristic is already in progress, swap the callbacks. + if (characteristicsTokens.count(uniqTokenKey) != 0) + { + std::cout << "A notification for the given characteristic is already in progress. Swapping callbacks." << std::endl; + gattCharacteristic.ValueChanged(characteristicsTokens[uniqTokenKey]); + characteristicsTokens[uniqTokenKey] = {0}; + } + characteristicsTokens[uniqTokenKey] = gattCharacteristic.ValueChanged({this, &UniversalBlePlugin::GattCharacteristic_ValueChanged}); + } + } + + void UniversalBlePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) + { + auto uuid = to_uuidstr(sender.Uuid()); + auto bytes = to_bytevc(args.CharacteristicValue()); + ValueChangeStruct *valueChangeStruct = new ValueChangeStruct(_mac_address_to_str(sender.Service().Device().BluetoothAddress()), uuid, bytes); + ::PostMessage(GetWindow(), WM_VALUE_CHANGE, reinterpret_cast(valueChangeStruct), 0); + } + + std::string UniversalBlePlugin::parsePairingFailError(Enumeration::DevicePairingResult result) + { + switch (result.Status()) + { + case Enumeration::DevicePairingResultStatus::Paired: + return ""; + case Enumeration::DevicePairingResultStatus::AlreadyPaired: + return "AlreadyPaired"; + + case Enumeration::DevicePairingResultStatus::ConnectionRejected: + return "ConnectionRejected"; + + case Enumeration::DevicePairingResultStatus::NotPaired: + return "NotPaired"; + + case Enumeration::DevicePairingResultStatus::NotReadyToPair: + return "NotReadyToPair"; + + case Enumeration::DevicePairingResultStatus::TooManyConnections: + return "TooManyConnections"; + + case Enumeration::DevicePairingResultStatus::HardwareFailure: + return "HardwareFailure"; + + case Enumeration::DevicePairingResultStatus::AuthenticationTimeout: + return "AuthenticationTimeout"; + + case Enumeration::DevicePairingResultStatus::AuthenticationNotAllowed: + return "AuthenticationNotAllowed"; + + case Enumeration::DevicePairingResultStatus::AuthenticationFailure: + return "AuthenticationFailure"; + + case Enumeration::DevicePairingResultStatus::NoSupportedProfiles: + return "NoSupportedProfiles"; + + case Enumeration::DevicePairingResultStatus::ProtectionLevelCouldNotBeMet: + return "ProtectionLevelCouldNotBeMet"; + + case Enumeration::DevicePairingResultStatus::AccessDenied: + return "AccessDenied"; + + case Enumeration::DevicePairingResultStatus::InvalidCeremonyData: + return "InvalidCeremonyData"; + + case Enumeration::DevicePairingResultStatus::PairingCanceled: + return "PairingCanceled"; + + case Enumeration::DevicePairingResultStatus::OperationAlreadyInProgress: + return "OperationAlreadyInProgress"; + + case Enumeration::DevicePairingResultStatus::RequiredHandlerNotRegistered: + return "RequiredHandlerNotRegistered"; + + case Enumeration::DevicePairingResultStatus::RejectedByHandler: + return "RejectedByHandler"; + + case Enumeration::DevicePairingResultStatus::RemoteDeviceHasAssociation: + return "RemoteDeviceHasAssociation"; + + case Enumeration::DevicePairingResultStatus::Failed: + return "Failed to pair"; + + default: + return "Failed to pair"; + } + } + +} // namespace universal_ble \ No newline at end of file diff --git a/windows/universal_ble_plugin.h b/windows/universal_ble_plugin.h new file mode 100644 index 0000000..a083033 --- /dev/null +++ b/windows/universal_ble_plugin.h @@ -0,0 +1,171 @@ +#ifndef FLUTTER_PLUGIN_UNIVERSAL_BLE_PLUGIN_H_ +#define FLUTTER_PLUGIN_UNIVERSAL_BLE_PLUGIN_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "Utils.h" +#include "UniversalBle.g.h" +#include "universal_enum.h" + +namespace universal_ble +{ + using namespace winrt; + using namespace winrt::Windows::Devices; + using namespace winrt::Windows::Foundation; + using namespace winrt::Windows::Foundation::Collections; + using namespace winrt::Windows::Storage::Streams; + using namespace winrt::Windows::Devices::Radios; + using namespace winrt::Windows::Devices::Bluetooth; + using namespace winrt::Windows::Devices::Bluetooth::Advertisement; + using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; + using namespace Windows::Devices::Enumeration; + + struct gatt_characteristic_t + { + GattCharacteristic obj = nullptr; + // winrt::event_token value_changed_token; + }; + + struct gatt_service_t + { + GattDeviceService obj = nullptr; + std::map characteristics; + }; + + struct BluetoothDeviceAgent + { + BluetoothLEDevice device; + winrt::event_token connnectionStatusChangedToken; + std::map gatt_map_; + + BluetoothDeviceAgent(BluetoothLEDevice device, winrt::event_token connnectionStatusChangedToken, std::map gatt_map_) + : device(device), + connnectionStatusChangedToken(connnectionStatusChangedToken), + gatt_map_(gatt_map_) {} + + ~BluetoothDeviceAgent() + { + device = nullptr; + } + + gatt_characteristic_t &_fetch_characteristic(const std::string &service_uuid, + const std::string &characteristic_uuid) + { + if (gatt_map_.count(service_uuid) == 0) + { + throw FlutterError("Service not found"); + } + + if (gatt_map_[service_uuid].characteristics.count(characteristic_uuid) == 0) + { + throw FlutterError("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); + + UniversalBlePlugin(flutter::PluginRegistrarWindows *registrar); + + ~UniversalBlePlugin(); + + static void SuccessCallback() {} + static void ErrorCallback(const FlutterError &error) + { + std::cout << "ErrorCallback: " << error.message() << std::endl; + } + + // Disallow copy and assign. + UniversalBlePlugin(const UniversalBlePlugin &) = delete; + UniversalBlePlugin &operator=(const UniversalBlePlugin &) = delete; + + flutter::PluginRegistrarWindows *registrar_; + int64_t window_proc_delegate_id_ = -1; + + HWND GetWindow(); + std::optional WindowProcDelegate(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); + winrt::fire_and_forget InitializeAsync(); + Radio bluetoothRadio{nullptr}; + void Radio_StateChanged(Radio sender, IInspectable args); + RadioState oldRadioState = RadioState::Unknown; + BluetoothLEAdvertisementWatcher bluetoothLEWatcher{nullptr}; + winrt::event_token bluetoothLEWatcherReceivedToken; + void BluetoothLEWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args); + std::map> connectedDevices{}; + std::map deviceConnectableStatus{}; + winrt::event_revoker radioStateChangedRevoker; + winrt::fire_and_forget ConnectAsync(uint64_t bluetoothAddress); + void BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args); + void CleanConnection(uint64_t bluetoothAddress); + winrt::fire_and_forget DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, std::function reply)>); + winrt::fire_and_forget SetNotifiableAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, const std::string &service, + const std::string &characteristic, GattClientCharacteristicConfigurationDescriptorValue descriptorValue); + void GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args); + AvailabilityState getAvailabilityStateFromRadio(RadioState radioState); + std::string parsePairingFailError(Enumeration::DevicePairingResult result); + void PostConnectionUpdate(uint64_t bluetoothAddress, ConnectionState connectionState); + winrt::fire_and_forget GetConnectedDevicesAsync(std::vector with_services, + std::function reply)> result); + winrt::fire_and_forget IsPairedAsync(std::string device_id, std::function reply)> result); + + // UniversalBlePlatformChannel implementation. + void GetBluetoothAvailabilityState(std::function reply)> result) override; + void EnableBluetooth(std::function reply)> result) override; + std::optional StartScan() override; + std::optional StopScan() override; + std::optional Connect(const std::string &device_id) override; + std::optional Disconnect(const std::string &device_id) override; + void DiscoverServices( + const std::string &device_id, + std::function reply)> result) override; + std::optional SetNotifiable( + const std::string &device_id, + const std::string &service, + const std::string &characteristic, + int64_t ble_input_property) 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; + std::optional Pair(const std::string &device_id) override; + std::optional UnPair(const std::string &device_id) override; + void GetConnectedDevices( + const flutter::EncodableList &with_services, + std::function reply)> result); + }; + +} // namespace universal_ble + +#endif // FLUTTER_PLUGIN_UNIVERSAL_BLE_PLUGIN_H_ \ No newline at end of file diff --git a/windows/universal_ble_plugin_c_api.cpp b/windows/universal_ble_plugin_c_api.cpp new file mode 100644 index 0000000..fdbf1de --- /dev/null +++ b/windows/universal_ble_plugin_c_api.cpp @@ -0,0 +1,12 @@ +#include "include/universal_ble/universal_ble_plugin_c_api.h" + +#include + +#include "universal_ble_plugin.h" + +void UniversalBlePluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + universal_ble::UniversalBlePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/windows/universal_enum.h b/windows/universal_enum.h new file mode 100644 index 0000000..473655c --- /dev/null +++ b/windows/universal_enum.h @@ -0,0 +1,47 @@ +#pragma once + +namespace universal_ble +{ + + enum class ConnectionState : int + { + connected = 0, + disconnected = 1, + }; + + enum class CharacteristicProperty : int + { + broadcast = 0, + read = 1, + writeWithoutResponse = 2, + write = 3, + notify = 4, + indicate = 5, + authenticatedSignedWrites = 6, + extendedProperties = 7, + }; + + enum class BleInputProperty : int + { + disabled = 0, + notification = 1, + indication = 2, + }; + + enum class BleOutputProperty : int + { + withResponse = 0, + withoutResponse = 1, + }; + + enum class AvailabilityState : int + { + unknown = 0, + resetting = 1, + unsupported = 2, + unauthorized = 3, + poweredOff = 4, + poweredOn = 5, + }; + +} // namespace universal_ble \ No newline at end of file