diff --git a/packages/quick_blue/.metadata b/packages/quick_blue/.metadata index 8c15ad7..99f5d2e 100644 --- a/packages/quick_blue/.metadata +++ b/packages/quick_blue/.metadata @@ -1,10 +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. +# This file should be version controlled. version: - revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 channel: stable project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: linux + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: windows + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # 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/packages/quick_blue/android/build.gradle b/packages/quick_blue/android/build.gradle index 429b831..575a638 100644 --- a/packages/quick_blue/android/build.gradle +++ b/packages/quick_blue/android/build.gradle @@ -2,19 +2,19 @@ group 'com.example.quick_blue' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } -rootProject.allprojects { +allprojects { repositories { google() mavenCentral() @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 31 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -38,13 +38,27 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' } defaultConfig { minSdkVersion 21 } -} -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + 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/packages/quick_blue/android/gradle/wrapper/gradle-wrapper.jar b/packages/quick_blue/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/packages/quick_blue/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/quick_blue/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_blue/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/packages/quick_blue/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/quick_blue/android/gradlew b/packages/quick_blue/android/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/packages/quick_blue/android/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/quick_blue/android/gradlew.bat b/packages/quick_blue/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/packages/quick_blue/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/quick_blue/android/src/main/AndroidManifest.xml b/packages/quick_blue/android/src/main/AndroidManifest.xml index 05d674f..6dbadaa 100644 --- a/packages/quick_blue/android/src/main/AndroidManifest.xml +++ b/packages/quick_blue/android/src/main/AndroidManifest.xml @@ -1,17 +1,13 @@ - - + xmlns:tools="http://schemas.android.com/tools" + package="com.example.quick_blue"> + + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt b/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt index 624c046..b0a1761 100644 --- a/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt +++ b/packages/quick_blue/android/src/main/kotlin/com/example/quick_blue/QuickBluePlugin.kt @@ -3,14 +3,17 @@ package com.example.quick_blue import android.annotation.SuppressLint import android.bluetooth.* import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult -import android.content.Context +import android.bluetooth.le.ScanSettings import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.Handler import android.os.Looper +import android.os.ParcelUuid import android.util.Log import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -52,6 +55,7 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand broadcastReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) ) + } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { @@ -73,107 +77,138 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand mainThreadHandler.post { messageChannel.send(message) } } + fun trace() = Arrays.toString(Throwable().stackTrace) + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "isBluetoothAvailable" -> { - result.success(bluetoothManager.adapter.isEnabled) - } - "startScan" -> { - bluetoothManager.adapter.bluetoothLeScanner?.startScan(scanCallback) - result.success(null) - } - "stopScan" -> { - bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback) - result.success(null) - } - "connect" -> { - val deviceId = call.argument("deviceId")!! - if (knownGatts.find { it.device.address == deviceId } != null) { - return result.success(null) + try { + when (call.method) { + "isBluetoothAvailable" -> { + result.success(bluetoothManager.adapter.isEnabled) } - val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) - val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - remoteDevice.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) - } else { - remoteDevice.connectGatt(context, false, gattCallback) + "startScan" -> { + val serviceUUIDs = call.argument>("serviceUUIDs") + if (serviceUUIDs != null && serviceUUIDs.size > 0) { + val filters: ArrayList = ArrayList() + for (serviceUUID in serviceUUIDs) { + val filter = ScanFilter.Builder() + .setServiceUuid(parseToParcelUuid(serviceUUID)) + .build() + filters.add(filter) + } + + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + + bluetoothManager.adapter.bluetoothLeScanner?.startScan(filters, settings, scanCallback) + } else { + bluetoothManager.adapter.bluetoothLeScanner?.startScan(scanCallback) + } + result.success(null) } - knownGatts.add(gatt) - result.success(null) - // TODO connecting - } - "disconnect" -> { - val deviceId = call.argument("deviceId")!! - val gatt = knownGatts.find { it.device.address == deviceId } - ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", null) - cleanConnection(gatt) - result.success(null) - //FIXME If `disconnect` is called before BluetoothGatt.STATE_CONNECTED - // there will be no `disconnected` message any more - } - "discoverServices" -> { - val deviceId = call.argument("deviceId")!! - val gatt = knownGatts.find { it.device.address == deviceId } - ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", null) - gatt.discoverServices() - result.success(null) - } - "setNotifiable" -> { - val deviceId = call.argument("deviceId")!! - val service = call.argument("service")!! - val characteristic = call.argument("characteristic")!! - val bleInputProperty = call.argument("bleInputProperty")!! - val gatt = knownGatts.find { it.device.address == deviceId } - ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", null) - val c = gatt.getCharacteristic(service, characteristic) - ?: return result.error("IllegalArgument", "Unknown characteristic: $characteristic", null) - gatt.setNotifiable(c, bleInputProperty) - result.success(null) - } - "readValue" -> { - val deviceId = call.argument("deviceId")!! - val service = call.argument("service")!! - val characteristic = call.argument("characteristic")!! - val gatt = knownGatts.find { it.device.address == deviceId } - ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", null) - val c = gatt.getCharacteristic(service, characteristic) - ?: return result.error("IllegalArgument", "Unknown characteristic: $characteristic", null) - if (gatt.readCharacteristic(c)) + "stopScan" -> { + bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback) result.success(null) - else - result.error("Characteristic unavailable", null, null) - } - "writeValue" -> { - val deviceId = call.argument("deviceId")!! - val service = call.argument("service")!! - val characteristic = call.argument("characteristic")!! - val value = call.argument("value")!! - val gatt = knownGatts.find { it.device.address == deviceId } - ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", null) - val c = gatt.getCharacteristic(service, characteristic) - ?: return result.error("IllegalArgument", "Unknown characteristic: $characteristic", null) - c.value = value - if (gatt.writeCharacteristic(c)) + } + "connect" -> { + val deviceId = call.argument("deviceId")!! + if (knownGatts.find { it.device.address == deviceId } != null) { + return result.success(null) + } + val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId) + val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + remoteDevice.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + } else { + remoteDevice.connectGatt(context, false, gattCallback) + } + knownGatts.add(gatt) result.success(null) - else - result.error("Characteristic unavailable", null, null) - } - "requestMtu" -> { - val deviceId = call.argument("deviceId")!! - val expectedMtu = call.argument("expectedMtu")!! - val gatt = knownGatts.find { it.device.address == deviceId } - ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", null) - gatt.requestMtu(expectedMtu) - result.success(null) - } - else -> { - result.notImplemented() + // TODO connecting + } + "disconnect" -> { + val deviceId = call.argument("deviceId")!! + val gatt = knownGatts.find { it.device.address == deviceId } + ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", trace()) + cleanConnection(gatt) + result.success(null) + //FIXME If `disconnect` is called before BluetoothGatt.STATE_CONNECTED + // there will be no `disconnected` message any more + } + "discoverServices" -> { + val deviceId = call.argument("deviceId")!! + val gatt = knownGatts.find { it.device.address == deviceId } + ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", trace()) + gatt.discoverServices() + result.success(null) + } + "setNotifiable" -> { + val deviceId = call.argument("deviceId")!! + val service = call.argument("service")!! + val characteristic = call.argument("characteristic")!! + val bleInputProperty = call.argument("bleInputProperty")!! + val gatt = knownGatts.find { it.device.address == deviceId } + ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", trace()) + val s = gatt.getService(UUID.fromString(service)) + ?: return result.error("IllegalArgument", "Unknown service: $service", trace()) + val c = s.getCharacteristic(UUID.fromString(characteristic)) + ?: return result.error("IllegalArgument", "Unknown characteristic: $characteristic", trace()) + gatt.setNotifiable(c, bleInputProperty) + result.success(null) + } + "readValue" -> { + val deviceId = call.argument("deviceId")!! + val service = call.argument("service")!! + val characteristic = call.argument("characteristic")!! + val gatt = knownGatts.find { it.device.address == deviceId } + ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", trace()) + val c = gatt.getCharacteristic(service, characteristic) + ?: return result.error("IllegalArgument", "Unknown characteristic: $characteristic", trace()) + if (gatt.readCharacteristic(c)) + result.success(null) + else + result.error("Characteristic unavailable", null, trace()) + } + "writeValue" -> { + val deviceId = call.argument("deviceId")!! + val service = call.argument("service")!! + val characteristic = call.argument("characteristic")!! + val value = call.argument("value")!! + val gatt = knownGatts.find { it.device.address == deviceId } + ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", trace()) + val c = gatt.getCharacteristic(service, characteristic) + ?: return result.error("IllegalArgument", "Unknown characteristic: $characteristic", trace()) + c.value = value + if (gatt.writeCharacteristic(c)) { + result.success(null) + } else { + result.error("Characteristic unavailable", null, trace()) + } + } + "requestMtu" -> { + val deviceId = call.argument("deviceId")!! + val expectedMtu = call.argument("expectedMtu")!! + val gatt = knownGatts.find { it.device.address == deviceId } + ?: return result.error("IllegalArgument", "Unknown deviceId: $deviceId", trace()) + val success = gatt.requestMtu(expectedMtu) + if (success) + result.success(null) + else + result.error("Unable to set MTU", null, trace()) + } + else -> { + result.notImplemented() + } } + } catch (e: Throwable) { + e.printStackTrace() + result.error("Error", "Error", trace()) } } private fun cleanConnection(gatt: BluetoothGatt) { - knownGatts.remove(gatt) + gatt.close() gatt.disconnect() + knownGatts.remove(gatt) } enum class AvailabilityState(val value: Int) { @@ -206,11 +241,10 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand private val scanCallback = object : ScanCallback() { override fun onScanFailed(errorCode: Int) { - Log.v(TAG, "onScanFailed: $errorCode") + //Log.v(TAG, "onScanFailed: $errorCode") } override fun onScanResult(callbackType: Int, result: ScanResult) { - Log.v(TAG, "onScanResult: $callbackType + $result") scanResultSink?.success(mapOf( "name" to (result.device.name ?: ""), "deviceId" to result.device.address, @@ -220,7 +254,7 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand } override fun onBatchScanResults(results: MutableList?) { - Log.v(TAG, "onBatchScanResults: $results") + //Log.v(TAG, "onBatchScanResults: $results") } } @@ -248,7 +282,7 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - Log.v(TAG, "onConnectionStateChange: device(${gatt.device.address}) status($status), newState($newState)") + //Log.v(TAG, "onConnectionStateChange: device(${gatt.device.address}) status($status), newState($newState)") if (newState == BluetoothGatt.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { sendMessage(messageConnector, mapOf( "deviceId" to gatt.device.address, @@ -264,15 +298,15 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - Log.v(TAG, "onServicesDiscovered ${gatt.device.address} $status") + //Log.v(TAG, "onServicesDiscovered ${gatt.device.address} $status") if (status != BluetoothGatt.GATT_SUCCESS) return gatt.services?.forEach { service -> - Log.v(TAG, "Service " + service.uuid) + //Log.v(TAG, "Service " + service.uuid) service.characteristics.forEach { characteristic -> - Log.v(TAG, " Characteristic ${characteristic.uuid}") + //Log.v(TAG, " Characteristic ${characteristic.uuid}") characteristic.descriptors.forEach { - Log.v(TAG, " Descriptor ${it.uuid}") + //Log.v(TAG, " Descriptor ${it.uuid}") } } @@ -290,11 +324,15 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand sendMessage(messageConnector, mapOf( "mtuConfig" to mtu )) + } else { + sendMessage(messageConnector, mapOf( + "mtuConfig" to -1 + )) } } override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { - Log.v(TAG, "onCharacteristicRead ${characteristic.uuid}, ${characteristic.value.contentToString()}") + //Log.v(TAG, "onCharacteristicRead ${characteristic.uuid}, ${characteristic.value.contentToString()}") sendMessage(messageConnector, mapOf( "deviceId" to gatt.device.address, "characteristicValue" to mapOf( @@ -305,11 +343,11 @@ class QuickBluePlugin: FlutterPlugin, MethodCallHandler, EventChannel.StreamHand } override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic, status: Int) { - Log.v(TAG, "onCharacteristicWrite ${characteristic.uuid}, ${characteristic.value.contentToString()} $status") + //Log.v(TAG, "onCharacteristicWrite ${characteristic.uuid}, ${characteristic.value.contentToString()} $status") } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - Log.v(TAG, "onCharacteristicChanged ${characteristic.uuid}, ${characteristic.value.contentToString()}") + //Log.v(TAG, "onCharacteristicChanged ${characteristic.uuid}, ${characteristic.value.contentToString()}") sendMessage(messageConnector, mapOf( "deviceId" to gatt.device.address, "characteristicValue" to mapOf( @@ -333,7 +371,7 @@ fun Short.toByteArray(byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): ByteArray ByteBuffer.allocate(2 /*Short.SIZE_BYTES*/).order(byteOrder).putShort(this).array() fun BluetoothGatt.getCharacteristic(service: String, characteristic: String): BluetoothGattCharacteristic? = - getService(UUID.fromString(service)).getCharacteristic(UUID.fromString(characteristic)) + getService(UUID.fromString(service))?.getCharacteristic(UUID.fromString(characteristic)) private val DESC__CLIENT_CHAR_CONFIGURATION = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") @@ -344,6 +382,23 @@ fun BluetoothGatt.setNotifiable(gattCharacteristic: BluetoothGattCharacteristic, "indication" -> BluetoothGattDescriptor.ENABLE_INDICATION_VALUE to true else -> BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE to false } - descriptor.value = value - setCharacteristicNotification(descriptor.characteristic, enable) && writeDescriptor(descriptor) + if (setCharacteristicNotification(gattCharacteristic, enable) && descriptor != null) { + descriptor.value = value + writeDescriptor(descriptor) + } +} +const val baseBluetoothUuidPostfix = "0000-1000-8000-00805F9B34FB" + +fun parseToParcelUuid(uuid: String): ParcelUuid { + return when (uuid.length) { + 4 -> { + ParcelUuid(UUID.fromString("0000$uuid-$baseBluetoothUuidPostfix")) + } + 8 -> { + ParcelUuid(UUID.fromString("$uuid-$baseBluetoothUuidPostfix")) + } + else -> { + ParcelUuid.fromString(uuid) + } + } } \ No newline at end of file diff --git a/packages/quick_blue/darwin/QuickBlueDarwin.swift b/packages/quick_blue/darwin/QuickBlueDarwin.swift index 3ff685b..2fb9782 100644 --- a/packages/quick_blue/darwin/QuickBlueDarwin.swift +++ b/packages/quick_blue/darwin/QuickBlueDarwin.swift @@ -13,269 +13,280 @@ let GATT_HEADER_LENGTH = 3 let GSS_SUFFIX = "0000-1000-8000-00805f9b34fb" extension CBUUID { - public var uuidStr: String { - get { - uuidString.lowercased() + public var uuidStr: String { + get { + uuidString.lowercased() + } } - } } extension CBPeripheral { - // FIXME https://forums.developer.apple.com/thread/84375 - public var uuid: UUID { - get { - value(forKey: "identifier") as! NSUUID as UUID + // FIXME https://forums.developer.apple.com/thread/84375 + public var uuid: UUID { + get { + value(forKey: "identifier") as! NSUUID as UUID + } } - } - public func getCharacteristic(_ characteristic: String, of service: String) -> CBCharacteristic? { - let s = self.services?.first { - $0.uuid.uuidStr == service || "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)" == service + public func getCharacteristic(_ characteristic: String, of service: String) -> CBCharacteristic? { + let s = self.services?.first { + $0.uuid.uuidStr == service || "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)" == service + } + let c = s?.characteristics?.first { + $0.uuid.uuidStr == characteristic || "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)" == characteristic + } + return c } - let c = s?.characteristics?.first { - $0.uuid.uuidStr == characteristic || "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)" == characteristic - } - return c - } - public func setNotifiable(_ bleInputProperty: String, for characteristic: String, of service: String) { - guard let characteristic = getCharacteristic(characteristic, of: service) else{ - return + public func setNotifiable(_ bleInputProperty: String, for characteristic: String, of service: String) { + guard let characteristic = getCharacteristic(characteristic, of: service) else{ + return + } + setNotifyValue(bleInputProperty != "disabled", for: characteristic) } - setNotifyValue(bleInputProperty != "disabled", for: characteristic) - } } public class QuickBlueDarwin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - #if os(iOS) - let messenger = registrar.messenger() - #elseif os(OSX) - let messenger = registrar.messenger - #endif - let method = FlutterMethodChannel(name: "quick_blue/method", binaryMessenger: messenger) - let eventAvailabilityChange = FlutterEventChannel(name: "quick_blue/event.availabilityChange", binaryMessenger: messenger) - let eventScanResult = FlutterEventChannel(name: "quick_blue/event.scanResult", binaryMessenger: messenger) - let messageConnector = FlutterBasicMessageChannel(name: "quick_blue/message.connector", binaryMessenger: messenger) + public static func register(with registrar: FlutterPluginRegistrar) { +#if os(iOS) + let messenger = registrar.messenger() +#elseif os(OSX) + let messenger = registrar.messenger +#endif + let method = FlutterMethodChannel(name: "quick_blue/method", binaryMessenger: messenger) + let eventAvailabilityChange = FlutterEventChannel(name: "quick_blue/event.availabilityChange", binaryMessenger: messenger) + let eventScanResult = FlutterEventChannel(name: "quick_blue/event.scanResult", binaryMessenger: messenger) + let messageConnector = FlutterBasicMessageChannel(name: "quick_blue/message.connector", binaryMessenger: messenger) - let instance = QuickBlueDarwin() - registrar.addMethodCallDelegate(instance, channel: method) - eventAvailabilityChange.setStreamHandler(instance) - eventScanResult.setStreamHandler(instance) - instance.messageConnector = messageConnector - } + let instance = QuickBlueDarwin() + registrar.addMethodCallDelegate(instance, channel: method) + eventAvailabilityChange.setStreamHandler(instance) + eventScanResult.setStreamHandler(instance) + instance.messageConnector = messageConnector + } - private lazy var manager: CBCentralManager = { CBCentralManager(delegate: self, queue: nil) }() - private var discoveredPeripherals = Dictionary() + private lazy var manager: CBCentralManager = { CBCentralManager(delegate: self, queue: nil) }() + private var discoveredPeripherals = Dictionary() - private var availabilityChangeSink: FlutterEventSink? - private var scanResultSink: FlutterEventSink? - private var messageConnector: FlutterBasicMessageChannel! + private var availabilityChangeSink: FlutterEventSink? + private var scanResultSink: FlutterEventSink? + private var messageConnector: FlutterBasicMessageChannel! - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "isBluetoothAvailable": - result(manager.state == .poweredOn) - case "startScan": - manager.scanForPeripherals(withServices: nil) - result(nil) - case "stopScan": - manager.stopScan() - result(nil) + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "isBluetoothAvailable": + result(manager.state == .poweredOn) + case "startScan": + let arguments = call.arguments as! Dictionary + let serviceUUIDs = arguments["serviceUUIDs"] as? [String] + if serviceUUIDs != nil && serviceUUIDs!.count > 0 { + let serviceCBUUID = serviceUUIDs!.map({ uuid in + CBUUID(string: uuid) + }) + manager.scanForPeripherals(withServices: serviceCBUUID) + } else { + manager.scanForPeripherals(withServices: nil) + } + result(nil) + case "stopScan": + manager.stopScan() + result(nil) case "connect": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - peripheral.delegate = self - manager.connect(peripheral) - result(nil) - case "disconnect": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - if (peripheral.state != .disconnected) { - manager.cancelPeripheralConnection(peripheral) - } - result(nil) - case "discoverServices": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - peripheral.discoverServices(nil) - result(nil) - case "setNotifiable": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - let service = arguments["service"] as! String - let characteristic = arguments["characteristic"] as! String - let bleInputProperty = arguments["bleInputProperty"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - guard let c = peripheral.getCharacteristic(characteristic, of: service) else { - result(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)) - return - } - peripheral.setNotifyValue(bleInputProperty != "disabled", for: c) - result(nil) - case "readValue": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - let service = arguments["service"] as! String - let characteristic = arguments["characteristic"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - guard let c = peripheral.getCharacteristic(characteristic, of: service) else { - result(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)) - return - } - peripheral.readValue(for: c) - result(nil) - case "writeValue": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - let service = arguments["service"] as! String - let characteristic = arguments["characteristic"] as! String - let value = arguments["value"] as! FlutterStandardTypedData - let bleOutputProperty = arguments["bleOutputProperty"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - let type = bleOutputProperty == "withoutResponse" ? CBCharacteristicWriteType.withoutResponse : CBCharacteristicWriteType.withResponse - guard let c = peripheral.getCharacteristic(characteristic, of: service) else { - result(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)) - return - } - peripheral.writeValue(value.data, for: c, type: type) - result(nil) - case "requestMtu": - let arguments = call.arguments as! Dictionary - let deviceId = arguments["deviceId"] as! String - guard let peripheral = discoveredPeripherals[deviceId] else { - result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) - return - } - result(nil) - let mtu = peripheral.maximumWriteValueLength(for: .withoutResponse) - print("peripheral.maximumWriteValueLengthForType:CBCharacteristicWriteWithoutResponse \(mtu)") - messageConnector.sendMessage(["mtuConfig": mtu + GATT_HEADER_LENGTH]) - default: - result(FlutterMethodNotImplemented) + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + peripheral.delegate = self + manager.connect(peripheral) + result(nil) + case "disconnect": + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + if (peripheral.state != .disconnected) { + manager.cancelPeripheralConnection(peripheral) + } + result(nil) + case "discoverServices": + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + peripheral.discoverServices(nil) + result(nil) + case "setNotifiable": + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + let service = arguments["service"] as! String + let characteristic = arguments["characteristic"] as! String + let bleInputProperty = arguments["bleInputProperty"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + guard let c = peripheral.getCharacteristic(characteristic, of: service) else { + result(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)) + return + } + peripheral.setNotifyValue(bleInputProperty != "disabled", for: c) + result(nil) + case "readValue": + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + let service = arguments["service"] as! String + let characteristic = arguments["characteristic"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + guard let c = peripheral.getCharacteristic(characteristic, of: service) else { + result(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)) + return + } + peripheral.readValue(for: c) + result(nil) + case "writeValue": + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + let service = arguments["service"] as! String + let characteristic = arguments["characteristic"] as! String + let value = arguments["value"] as! FlutterStandardTypedData + let bleOutputProperty = arguments["bleOutputProperty"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + let type = bleOutputProperty == "withoutResponse" ? CBCharacteristicWriteType.withoutResponse : CBCharacteristicWriteType.withResponse + guard let c = peripheral.getCharacteristic(characteristic, of: service) else { + result(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)) + return + } + peripheral.writeValue(value.data, for: c, type: type) + result(nil) + case "requestMtu": + let arguments = call.arguments as! Dictionary + let deviceId = arguments["deviceId"] as! String + guard let peripheral = discoveredPeripherals[deviceId] else { + result(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(deviceId)", details: nil)) + return + } + result(nil) + let mtu = peripheral.maximumWriteValueLength(for: .withoutResponse) + // print("peripheral.maximumWriteValueLengthForType:CBCharacteristicWriteWithoutResponse \(mtu)") + messageConnector.sendMessage(["mtuConfig": mtu + GATT_HEADER_LENGTH]) + default: + result(FlutterMethodNotImplemented) + } } - } } extension QuickBlueDarwin: CBCentralManagerDelegate { - public func centralManagerDidUpdateState(_ central: CBCentralManager) { - availabilityChangeSink?(central.state.rawValue) - } + public func centralManagerDidUpdateState(_ central: CBCentralManager) { + availabilityChangeSink?(central.state.rawValue) + } - public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - print("centralManager:didDiscoverPeripheral \(peripheral.name ?? "nil") \(peripheral.uuid.uuidString)") - discoveredPeripherals[peripheral.uuid.uuidString] = peripheral + public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + // print("centralManager:didDiscoverPeripheral \(peripheral.name ?? "nil") \(peripheral.uuid.uuidString)") + discoveredPeripherals[peripheral.uuid.uuidString] = peripheral - let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data - scanResultSink?([ - "name": peripheral.name ?? "", - "deviceId": peripheral.uuid.uuidString, - "manufacturerData": FlutterStandardTypedData(bytes: manufacturerData ?? Data()), - "rssi": RSSI, - ]) - } + let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data + scanResultSink?([ + "name": peripheral.name ?? "", + "deviceId": peripheral.uuid.uuidString, + "manufacturerData": FlutterStandardTypedData(bytes: manufacturerData ?? Data()), + "rssi": RSSI, + ]) + } - public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - print("centralManager:didConnect \(peripheral.uuid.uuidString)") - messageConnector.sendMessage([ - "deviceId": peripheral.uuid.uuidString, - "ConnectionState": "connected", - ]) - } - - public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - print("centralManager:didDisconnectPeripheral: \(peripheral.uuid.uuidString) error: \(String(describing: error))") - messageConnector.sendMessage([ - "deviceId": peripheral.uuid.uuidString, - "ConnectionState": "disconnected", - ]) - } + public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + // print("centralManager:didConnect \(peripheral.uuid.uuidString)") + messageConnector.sendMessage([ + "deviceId": peripheral.uuid.uuidString, + "ConnectionState": "connected", + ]) + } + + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + // print("centralManager:didDisconnectPeripheral: \(peripheral.uuid.uuidString) error: \(String(describing: error))") + messageConnector.sendMessage([ + "deviceId": peripheral.uuid.uuidString, + "ConnectionState": "disconnected", + ]) + } } extension QuickBlueDarwin: FlutterStreamHandler { - open func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - guard let args = arguments as? Dictionary, let name = args["name"] as? String else { - return nil + open func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + guard let args = arguments as? Dictionary, let name = args["name"] as? String else { + return nil + } + print("QuickBlueDarwin onListenWithArguments: \(name)") + if name == "availabilityChange" { + availabilityChangeSink = events + availabilityChangeSink?(manager.state.rawValue) // Initializes CBCentralManager and returns the current state when hot restarting + } else if name == "scanResult" { + scanResultSink = events + } + return nil } - print("QuickBlueDarwin onListenWithArguments: \(name)") - if name == "availabilityChange" { - availabilityChangeSink = events - availabilityChangeSink?(manager.state.rawValue) // Initializes CBCentralManager and returns the current state when hot restarting - } else if name == "scanResult" { - scanResultSink = events - } - return nil - } - open func onCancel(withArguments arguments: Any?) -> FlutterError? { - guard let args = arguments as? Dictionary, let name = args["name"] as? String else { - return nil - } - print("QuickBlueDarwin onCancelWithArguments: \(name)") - if name == "availabilityChange" { - availabilityChangeSink = nil - } else if name == "scanResult" { - scanResultSink = nil + open func onCancel(withArguments arguments: Any?) -> FlutterError? { + guard let args = arguments as? Dictionary, let name = args["name"] as? String else { + return nil + } + print("QuickBlueDarwin onCancelWithArguments: \(name)") + if name == "availabilityChange" { + availabilityChangeSink = nil + } else if name == "scanResult" { + scanResultSink = nil + } + return nil } - return nil - } } extension QuickBlueDarwin: CBPeripheralDelegate { - public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - print("peripheral: \(peripheral.uuid.uuidString) didDiscoverServices error: \(String(describing: error))") - for service in peripheral.services! { - peripheral.discoverCharacteristics(nil, for: service) + public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + // print("peripheral: \(peripheral.uuid.uuidString) didDiscoverServices error: \(String(describing: error))") + for service in peripheral.services! { + // if service.uuid == CBUUID(string: "FF00") { + peripheral.discoverCharacteristics(nil, for: service) + // } + } } - } - - public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - for characteristic in service.characteristics! { - print("peripheral:didDiscoverCharacteristicsForService (\(service.uuid.uuidStr), \(characteristic.uuid.uuidStr)") + + public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + // for characteristic in service.characteristics! { + // print("peripheral:didDiscoverCharacteristicsForService (\(service.uuid.uuidStr), \(characteristic.uuid.uuidStr)") + // } + self.messageConnector.sendMessage([ + "deviceId": peripheral.uuid.uuidString, + "ServiceState": "discovered", + "service": service.uuid.uuidStr, + "characteristics": service.characteristics!.map { $0.uuid.uuidStr } + ]) } - self.messageConnector.sendMessage([ - "deviceId": peripheral.uuid.uuidString, - "ServiceState": "discovered", - "service": service.uuid.uuidStr, - "characteristics": service.characteristics!.map { $0.uuid.uuidStr } - ]) - } - public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { - let data = characteristic.value as NSData? - print("peripheral:didWriteValueForCharacteristic \(characteristic.uuid.uuidStr) \(String(describing: data)) error: \(String(describing: error))") - } + public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { + let data = characteristic.value as NSData? + // print("peripheral:didWriteValueForCharacteristic \(characteristic.uuid.uuidStr) \(String(describing: data)) error: \(String(describing: error))") + } - public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - let data = characteristic.value as NSData? - print("peripheral:didUpdateValueForCharacteristic \(characteristic.uuid) \(String(describing: data)) error: \(String(describing: error))") - self.messageConnector.sendMessage([ - "deviceId": peripheral.uuid.uuidString, - "characteristicValue": [ - "characteristic": characteristic.uuid.uuidStr, - "value": FlutterStandardTypedData(bytes: characteristic.value!) - ] - ]) - } + public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + let data = characteristic.value as NSData? + // print("peripheral:didUpdateValueForCharacteristic \(characteristic.uuid) \(String(describing: data)) error: \(String(describing: error))") + self.messageConnector.sendMessage([ + "deviceId": peripheral.uuid.uuidString, + "characteristicValue": [ + "characteristic": characteristic.uuid.uuidStr, + "value": FlutterStandardTypedData(bytes: characteristic.value!) + ] + ]) + } } diff --git a/packages/quick_blue/example/.metadata b/packages/quick_blue/example/.metadata index fd70cab..732ba6d 100644 --- a/packages/quick_blue/example/.metadata +++ b/packages/quick_blue/example/.metadata @@ -1,10 +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. +# This file should be version controlled. version: - revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # 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/packages/quick_blue/example/android/build.gradle b/packages/quick_blue/example/android/build.gradle index 0cb14a5..f7eb7f6 100644 --- a/packages/quick_blue/example/android/build.gradle +++ b/packages/quick_blue/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/packages/quick_blue/example/lib/main.dart b/packages/quick_blue/example/lib/main.dart index d4e5708..13ea21b 100644 --- a/packages/quick_blue/example/lib/main.dart +++ b/packages/quick_blue/example/lib/main.dart @@ -87,7 +87,7 @@ class _MyAppState extends State { ElevatedButton( child: const Text('startScan'), onPressed: () { - QuickBlue.startScan(); + QuickBlue.startScan(serviceUUIDs: ['FF02']); }, ), ElevatedButton( diff --git a/packages/quick_blue/example/linux/flutter/generated_plugins.cmake b/packages/quick_blue/example/linux/flutter/generated_plugins.cmake index 51436ae..2e1de87 100644 --- a/packages/quick_blue/example/linux/flutter/generated_plugins.cmake +++ b/packages/quick_blue/example/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -13,3 +16,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) 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/packages/quick_blue/example/windows/flutter/generated_plugins.cmake b/packages/quick_blue/example/windows/flutter/generated_plugins.cmake index 3994668..55a705b 100644 --- a/packages/quick_blue/example/windows/flutter/generated_plugins.cmake +++ b/packages/quick_blue/example/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST quick_blue ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) 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/packages/quick_blue/lib/quick_blue.dart b/packages/quick_blue/lib/quick_blue.dart index 99a7a29..8ab461d 100644 --- a/packages/quick_blue/lib/quick_blue.dart +++ b/packages/quick_blue/lib/quick_blue.dart @@ -16,52 +16,65 @@ class QuickBlue { static void setInstance(QuickBluePlatform instance) => _platform = instance; - static void setLogger(QuickLogger logger) => - _platform.setLogger(logger); + static void setLogger(QuickLogger logger) => _platform.setLogger(logger); static Future isBluetoothAvailable() => _platform.isBluetoothAvailable(); - static Stream get availabilityChangeStream => _platform.availabilityChangeStream.map(AvailabilityState.parse); + static Stream get availabilityChangeStream => + _platform.availabilityChangeStream.map(AvailabilityState.parse); - static Future startScan() => _platform.startScan(); + static Future startScan({List? serviceUUIDs}) => + _platform.startScan(serviceUUIDs); - static void stopScan() => _platform.stopScan(); + static Future stopScan() => _platform.stopScan(); static Stream get scanResultStream { return _platform.scanResultStream - .map((item) => BlueScanResult.fromMap(item)); + .map((item) => BlueScanResult.fromMap(item)); } - static void connect(String deviceId) => _platform.connect(deviceId); + static Future connect(String deviceId) => _platform.connect(deviceId); - static void disconnect(String deviceId) => _platform.disconnect(deviceId); + static Future disconnect(String deviceId) => + _platform.disconnect(deviceId); static void setConnectionHandler(OnConnectionChanged? onConnectionChanged) { _platform.onConnectionChanged = onConnectionChanged; } - static void discoverServices(String deviceId) => _platform.discoverServices(deviceId); + static Future discoverServices(String deviceId) => + _platform.discoverServices(deviceId); static void setServiceHandler(OnServiceDiscovered? onServiceDiscovered) { _platform.onServiceDiscovered = onServiceDiscovered; } - static Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty) { - return _platform.setNotifiable(deviceId, service, characteristic, bleInputProperty); + static Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty) { + return _platform.setNotifiable( + deviceId, service, characteristic, bleInputProperty); } static void setValueHandler(OnValueChanged? onValueChanged) { _platform.onValueChanged = onValueChanged; } - static Future readValue(String deviceId, String service, String characteristic) { + static Future readValue( + String deviceId, String service, String characteristic) { return _platform.readValue(deviceId, service, characteristic); } - static Future writeValue(String deviceId, String service, String characteristic, Uint8List value, BleOutputProperty bleOutputProperty) { - return _platform.writeValue(deviceId, service, characteristic, value, bleOutputProperty); + static Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty) { + return _platform.writeValue( + deviceId, service, characteristic, value, bleOutputProperty); } - static Future requestMtu(String deviceId, int expectedMtu) => _platform.requestMtu(deviceId, expectedMtu); + static Future requestMtu(String deviceId, int expectedMtu) => + _platform.requestMtu(deviceId, expectedMtu); } diff --git a/packages/quick_blue/lib/src/method_channel_quick_blue.dart b/packages/quick_blue/lib/src/method_channel_quick_blue.dart index 47064b7..8c57a77 100644 --- a/packages/quick_blue/lib/src/method_channel_quick_blue.dart +++ b/packages/quick_blue/lib/src/method_channel_quick_blue.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; @@ -9,8 +8,10 @@ import 'quick_blue_platform_interface.dart'; class MethodChannelQuickBlue extends QuickBluePlatform { static const _method = MethodChannel('quick_blue/method'); static const _eventScanResult = EventChannel('quick_blue/event.scanResult'); - static const _eventAvailabilityChange = EventChannel('quick_blue/event.availabilityChange'); - static const _messageConnector = BasicMessageChannel('quick_blue/message.connector', StandardMessageCodec()); + static const _eventAvailabilityChange = + EventChannel('quick_blue/event.availabilityChange'); + static const _messageConnector = BasicMessageChannel( + 'quick_blue/message.connector', StandardMessageCodec()); MethodChannelQuickBlue() { _messageConnector.setMessageHandler(_handleConnectorMessage); @@ -33,14 +34,17 @@ class MethodChannelQuickBlue extends QuickBluePlatform { return result; } - final Stream _availabilityChangeStream = _eventAvailabilityChange.receiveBroadcastStream({'name': 'availabilityChange'}).cast(); + final Stream _availabilityChangeStream = _eventAvailabilityChange + .receiveBroadcastStream({'name': 'availabilityChange'}).cast(); @override Stream get availabilityChangeStream => _availabilityChangeStream; @override - Future startScan() async { - await _method.invokeMethod('startScan'); + Future startScan(List? serviceUUIDs) async { + await _method.invokeMethod('startScan', { + 'serviceUUIDs': serviceUUIDs, + }); _log('startScan invokeMethod success'); } @@ -50,28 +54,29 @@ class MethodChannelQuickBlue extends QuickBluePlatform { _log('stopScan invokeMethod success'); } - final Stream _scanResultStream = _eventScanResult.receiveBroadcastStream({'name': 'scanResult'}); + final Stream _scanResultStream = + _eventScanResult.receiveBroadcastStream({'name': 'scanResult'}); @override Stream get scanResultStream => _scanResultStream; @override - void connect(String deviceId) { - _method.invokeMethod('connect', { + Future connect(String deviceId) async { + await _method.invokeMethod('connect', { 'deviceId': deviceId, }).then((_) => _log('connect invokeMethod success')); } @override - void disconnect(String deviceId) { - _method.invokeMethod('disconnect', { + Future disconnect(String deviceId) async { + await _method.invokeMethod('disconnect', { 'deviceId': deviceId, }).then((_) => _log('disconnect invokeMethod success')); } @override - void discoverServices(String deviceId) { - _method.invokeMethod('discoverServices', { + Future discoverServices(String deviceId) async { + await _method.invokeMethod('discoverServices', { 'deviceId': deviceId, }).then((_) => _log('discoverServices invokeMethod success')); } @@ -80,29 +85,34 @@ class MethodChannelQuickBlue extends QuickBluePlatform { _log('_handleConnectorMessage $message', logLevel: Level.ALL); if (message['ConnectionState'] != null) { String deviceId = message['deviceId']; - BlueConnectionState connectionState = BlueConnectionState.parse(message['ConnectionState']); + BlueConnectionState connectionState = + BlueConnectionState.parse(message['ConnectionState']); onConnectionChanged?.call(deviceId, connectionState); } else if (message['ServiceState'] != null) { if (message['ServiceState'] == 'discovered') { String deviceId = message['deviceId']; String service = message['service']; - List characteristics = (message['characteristics'] as List).cast(); + List characteristics = ((message['characteristics'] ?? []) as List).cast(); onServiceDiscovered?.call(deviceId, service, characteristics); } } else if (message['characteristicValue'] != null) { String deviceId = message['deviceId']; var characteristicValue = message['characteristicValue']; String characteristic = characteristicValue['characteristic']; - Uint8List value = Uint8List.fromList(characteristicValue['value']); // In case of _Uint8ArrayView + Uint8List value = Uint8List.fromList( + characteristicValue['value']); // In case of _Uint8ArrayView onValueChanged?.call(deviceId, characteristic, value); } else if (message['mtuConfig'] != null) { _mtuConfigController.add(message['mtuConfig']); + } else { + _log('_handleConnectorMessage $message is not implented.'); } } @override - Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty) async { - _method.invokeMethod('setNotifiable', { + Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty) async { + await _method.invokeMethod('setNotifiable', { 'deviceId': deviceId, 'service': service, 'characteristic': characteristic, @@ -111,8 +121,9 @@ class MethodChannelQuickBlue extends QuickBluePlatform { } @override - Future readValue(String deviceId, String service, String characteristic) async { - _method.invokeMethod('readValue', { + Future readValue( + String deviceId, String service, String characteristic) async { + await _method.invokeMethod('readValue', { 'deviceId': deviceId, 'service': service, 'characteristic': characteristic, @@ -120,8 +131,14 @@ class MethodChannelQuickBlue extends QuickBluePlatform { } @override - Future writeValue(String deviceId, String service, String characteristic, Uint8List value, BleOutputProperty bleOutputProperty) async { - _method.invokeMethod('writeValue', { + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty, + ) async { + await _method.invokeMethod('writeValue', { 'deviceId': deviceId, 'service': service, 'characteristic': characteristic, @@ -140,10 +157,16 @@ class MethodChannelQuickBlue extends QuickBluePlatform { @override Future requestMtu(String deviceId, int expectedMtu) async { - _method.invokeMethod('requestMtu', { + await _method.invokeMethod('requestMtu', { 'deviceId': deviceId, 'expectedMtu': expectedMtu, }).then((_) => _log('requestMtu invokeMethod success')); - return await _mtuConfigController.stream.first; + int mtu = await _mtuConfigController.stream.first + .timeout(const Duration(milliseconds: 1500)); + if (mtu == -1) { + throw Exception('Unable to set MTU size'); + } else { + return mtu; + } } } diff --git a/packages/quick_blue/lib/src/quick_blue_linux.dart b/packages/quick_blue/lib/src/quick_blue_linux.dart index 5952e35..632ebb8 100644 --- a/packages/quick_blue/lib/src/quick_blue_linux.dart +++ b/packages/quick_blue/lib/src/quick_blue_linux.dart @@ -24,13 +24,14 @@ class QuickBlueLinux extends QuickBluePlatform { if (!isInitialized) { await _client.connect(); - _activeAdapter ??= _client.adapters.firstWhereOrNull((adapter) => adapter.powered); + _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; + if (_client.adapters.isEmpty) { + throw Exception('Bluetooth adapter unavailable'); + } + await _client.adapters.first.setPowered(true); + _activeAdapter = _client.adapters.first; } _client.deviceAdded.listen(_onDeviceAdd); @@ -63,23 +64,29 @@ class QuickBlueLinux extends QuickBluePlatform { } // FIXME Close - final StreamController _availabilityStateController = StreamController.broadcast(); + final StreamController _availabilityStateController = + StreamController.broadcast(); @override - Stream get availabilityChangeStream =>_availabilityStateController.stream.map((state) => state.value); + Stream get availabilityChangeStream => + _availabilityStateController.stream.map((state) => state.value); AvailabilityState get availabilityState { - return _activeAdapter!.powered ? AvailabilityState.poweredOn : AvailabilityState.poweredOff; + return _activeAdapter!.powered + ? AvailabilityState.poweredOn + : AvailabilityState.poweredOff; } @override - Future startScan() async { + Future startScan(List? serviceUUIDs) async { await _ensureInitialized(); _log('startScan invoke success'); if (!_activeAdapter!.discovering) { - _activeAdapter!.startDiscovery(); + _activeAdapter!.startDiscovery(); //TODO This is async. Should it be awaited, or should it have error handlers attached? _client.devices.forEach(_onDeviceAdd); + } else { + _log('startScan not triggered because we are already scanning'); } } @@ -94,7 +101,8 @@ class QuickBlueLinux extends QuickBluePlatform { } // FIXME Close - final StreamController _scanResultController = StreamController.broadcast(); + final StreamController _scanResultController = + StreamController.broadcast(); @override Stream get scanResultStream => _scanResultController.stream; @@ -109,7 +117,8 @@ class QuickBlueLinux extends QuickBluePlatform { } BlueZDevice _findDeviceById(String deviceId) { - var device = _client.devices.firstWhereOrNull((device) => device.address == deviceId); + var device = _client.devices + .firstWhereOrNull((device) => device.address == deviceId); if (device == null) { throw Exception('Unknown deviceId:$deviceId'); } @@ -117,21 +126,21 @@ class QuickBlueLinux extends QuickBluePlatform { } @override - void connect(String deviceId) { - _findDeviceById(deviceId).connect().then((_) { + Future connect(String deviceId) async { + await _findDeviceById(deviceId).connect().then((_) { onConnectionChanged?.call(deviceId, BlueConnectionState.connected); }); } @override - void disconnect(String deviceId) { - _findDeviceById(deviceId).disconnect().then((_) { + Future disconnect(String deviceId) async { + await _findDeviceById(deviceId).disconnect().then((_) { onConnectionChanged?.call(deviceId, BlueConnectionState.disconnected); }); } @override - void discoverServices(String deviceId) { + Future discoverServices(String deviceId) async { var device = _findDeviceById(deviceId); for (var service in device.gattServices) { @@ -140,15 +149,20 @@ class QuickBlueLinux extends QuickBluePlatform { _log(" Characteristic ${characteristic.uuid}"); } - var characteristics = service.characteristics.map((e) => e.uuid.toString()).toList(); - onServiceDiscovered?.call(deviceId, service.uuid.toString(), characteristics); + var characteristics = + service.characteristics.map((e) => e.uuid.toString()).toList(); + onServiceDiscovered?.call( + deviceId, service.uuid.toString(), characteristics); } } - BlueZGattCharacteristic _getCharacteristic(String deviceId, String service, String characteristic) { + 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); + 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'); @@ -156,29 +170,36 @@ class QuickBlueLinux extends QuickBluePlatform { return c; } - final Map>> _characteristicPropertiesSubscriptions = {}; + final Map>> + _characteristicPropertiesSubscriptions = {}; @override - Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty) async { + Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty) async { var c = _getCharacteristic(deviceId, service, characteristic); - + if (bleInputProperty != BleInputProperty.disabled) { - c.startNotify(); + c.startNotify(); //TODO This is async. Should it be awaited, or should it have error handlers attached? void onPropertiesChanged(properties) { if (properties.contains('Value')) { - _log('onCharacteristicPropertiesChanged $characteristic, ${hex.encode(c.value)}'); - onValueChanged?.call(deviceId, characteristic, Uint8List.fromList(c.value)); + _log( + 'onCharacteristicPropertiesChanged $characteristic, ${hex.encode(c.value)}'); + onValueChanged?.call( + deviceId, characteristic, Uint8List.fromList(c.value)); } } - _characteristicPropertiesSubscriptions[characteristic] ??= c.propertiesChanged.listen(onPropertiesChanged); + + _characteristicPropertiesSubscriptions[characteristic] ??= + c.propertiesChanged.listen(onPropertiesChanged); } else { - c.stopNotify(); - _characteristicPropertiesSubscriptions.remove(characteristic)?.cancel(); + c.stopNotify(); //TODO This is async. Should it be awaited, or should it have error handlers attached? + _characteristicPropertiesSubscriptions.remove(characteristic)?.cancel(); //TODO This is async. Should it be awaited, or should it have error handlers attached? } } @override - Future readValue(String deviceId, String service, String characteristic) async { + Future readValue( + String deviceId, String service, String characteristic) async { var c = _getCharacteristic(deviceId, service, characteristic); var data = await c.readValue(); @@ -187,7 +208,12 @@ class QuickBlueLinux extends QuickBluePlatform { } @override - Future writeValue(String deviceId, String service, String characteristic, Uint8List value, BleOutputProperty bleOutputProperty) async { + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty) async { var c = _getCharacteristic(deviceId, service, characteristic); if (bleOutputProperty == BleOutputProperty.withResponse) { diff --git a/packages/quick_blue/lib/src/quick_blue_platform_interface.dart b/packages/quick_blue/lib/src/quick_blue_platform_interface.dart index 65d9940..632df05 100644 --- a/packages/quick_blue/lib/src/quick_blue_platform_interface.dart +++ b/packages/quick_blue/lib/src/quick_blue_platform_interface.dart @@ -11,11 +11,14 @@ export 'models.dart'; typedef QuickLogger = Logger; -typedef OnConnectionChanged = void Function(String deviceId, BlueConnectionState state); +typedef OnConnectionChanged = void Function( + String deviceId, BlueConnectionState state); -typedef OnServiceDiscovered = void Function(String deviceId, String serviceId, List characteristicIds); +typedef OnServiceDiscovered = void Function( + String deviceId, String serviceId, List characteristicIds); -typedef OnValueChanged = void Function(String deviceId, String characteristicId, Uint8List value); +typedef OnValueChanged = void Function( + String deviceId, String characteristicId, Uint8List value); abstract class QuickBluePlatform extends PlatformInterface { QuickBluePlatform() : super(token: _token); @@ -37,29 +40,36 @@ abstract class QuickBluePlatform extends PlatformInterface { Stream get availabilityChangeStream; - Future startScan(); + Future startScan(List? serviceUUIDs); Future stopScan(); Stream get scanResultStream; - void connect(String deviceId); + Future connect(String deviceId); - void disconnect(String deviceId); + Future disconnect(String deviceId); OnConnectionChanged? onConnectionChanged; - void discoverServices(String deviceId); + Future discoverServices(String deviceId); OnServiceDiscovered? onServiceDiscovered; - Future setNotifiable(String deviceId, String service, String characteristic, BleInputProperty bleInputProperty); + Future setNotifiable(String deviceId, String service, + String characteristic, BleInputProperty bleInputProperty); OnValueChanged? onValueChanged; - Future readValue(String deviceId, String service, String characteristic); + Future readValue( + String deviceId, String service, String characteristic); - Future writeValue(String deviceId, String service, String characteristic, Uint8List value, BleOutputProperty bleOutputProperty); + Future writeValue( + String deviceId, + String service, + String characteristic, + Uint8List value, + BleOutputProperty bleOutputProperty); Future requestMtu(String deviceId, int expectedMtu); } diff --git a/packages/quick_blue/pubspec.yaml b/packages/quick_blue/pubspec.yaml index 5e1ef52..e49a6b9 100644 --- a/packages/quick_blue/pubspec.yaml +++ b/packages/quick_blue/pubspec.yaml @@ -13,13 +13,14 @@ dependencies: sdk: flutter logging: ^1.0.2 plugin_platform_interface: ^2.1.2 - bluez: ^0.7.9 + bluez: ^0.8.1 convert: ^3.0.1 + collection: ^1.17.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^3.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/quick_blue/windows/quick_blue_plugin.cpp b/packages/quick_blue/windows/quick_blue_plugin.cpp index 9ec7456..e4331bd 100644 --- a/packages/quick_blue/windows/quick_blue_plugin.cpp +++ b/packages/quick_blue/windows/quick_blue_plugin.cpp @@ -24,310 +24,431 @@ #include #include +#include +#include +#include + #define GUID_FORMAT "%08x-%04hx-%04hx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx" #define GUID_ARG(guid) 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] -namespace { - -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 flutter::EncodableValue; -using flutter::EncodableMap; -using flutter::EncodableList; - -union uint16_t_union { - uint16_t uint16; - byte bytes[sizeof(uint16_t)]; -}; - -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) +namespace +{ + + 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 flutter::EncodableList; + using flutter::EncodableMap; + using flutter::EncodableValue; + + union uint16_t_union + { + uint16_t uint16; + byte bytes[sizeof(uint16_t)]; + }; + + 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, GUID_FORMAT, GUID_ARG(guid)); - return std::string{ chars }; -} - -struct BluetoothDeviceAgent { - BluetoothLEDevice device; - winrt::event_token connnectionStatusChangedToken; - std::map gattServices; - std::map gattCharacteristics; - std::map valueChangedTokens; - - BluetoothDeviceAgent(BluetoothLEDevice device, winrt::event_token connnectionStatusChangedToken) - : device(device), - connnectionStatusChangedToken(connnectionStatusChangedToken) {} - - ~BluetoothDeviceAgent() { - device = nullptr; + return ss.str(); } - IAsyncOperation GetServiceAsync(std::string service) { - if (gattServices.count(service) == 0) { - auto serviceResult = co_await device.GetGattServicesAsync(); - if (serviceResult.Status() != GattCommunicationStatus::Success) - co_return nullptr; + union to_guid + { + uint8_t buf[16]; + winrt::guid guid; + }; + + const uint8_t BYTE_ORDER[] = {3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15}; + + winrt::guid make_guid(const char *value) + { + to_guid to_guid; + memset(&to_guid, 0, sizeof(to_guid)); + int offset = 0; + for (int i = 0; i < (int)strlen(value); i++) + { + if (value[i] >= '0' && value[i] <= '9') + { + uint8_t digit = value[i] - '0'; + to_guid.buf[BYTE_ORDER[offset / 2]] += offset % 2 == 0 ? digit << 4 : digit; + offset++; + } + else if (value[i] >= 'A' && value[i] <= 'F') + { + uint8_t digit = 10 + value[i] - 'A'; + to_guid.buf[BYTE_ORDER[offset / 2]] += offset % 2 == 0 ? digit << 4 : digit; + offset++; + } + else if (value[i] >= 'a' && value[i] <= 'f') + { + uint8_t digit = 10 + value[i] - 'a'; + to_guid.buf[BYTE_ORDER[offset / 2]] += offset % 2 == 0 ? digit << 4 : digit; + offset++; + } + else + { + // skip char + } + } + + return to_guid.guid; + } - for (auto s : serviceResult.Services()) - if (to_uuidstr(s.Uuid()) == service) - gattServices.insert(std::make_pair(service, s)); + std::string LongUUID(std::string str) + { + std::string baseBluetoothUuidPostfix("0000-1000-8000-00805F9B34FB"); + + switch (str.length()) + { + case 4: + return "0000" + str + "-" + baseBluetoothUuidPostfix; + case 8: + return str + "-" + baseBluetoothUuidPostfix; + default: + return str; } - co_return gattServices.at(service); } - IAsyncOperation GetCharacteristicAsync(std::string service, std::string characteristic) { - if (gattCharacteristics.count(characteristic) == 0) { - auto gattService = co_await GetServiceAsync(service); + std::string to_uuidstr(winrt::guid guid) + { + char chars[36 + 1]; + sprintf_s(chars, GUID_FORMAT, GUID_ARG(guid)); + return std::string{chars}; + } - auto characteristicResult = co_await gattService.GetCharacteristicsAsync(); - if (characteristicResult.Status() != GattCommunicationStatus::Success) - co_return nullptr; + struct BluetoothDeviceAgent + { + BluetoothLEDevice device; + winrt::event_token connnectionStatusChangedToken; + std::map gattServices; + std::map gattCharacteristics; + std::map valueChangedTokens; + + BluetoothDeviceAgent(BluetoothLEDevice device, winrt::event_token connnectionStatusChangedToken) + : device(device), + connnectionStatusChangedToken(connnectionStatusChangedToken) {} + + ~BluetoothDeviceAgent() + { + device = nullptr; + } - for (auto c : characteristicResult.Characteristics()) - if (to_uuidstr(c.Uuid()) == characteristic) - gattCharacteristics.insert(std::make_pair(characteristic, c)); + IAsyncOperation GetServiceAsync(std::string service) + { + if (gattServices.count(service) == 0) + { + auto serviceResult = co_await device.GetGattServicesAsync(); + if (serviceResult.Status() != GattCommunicationStatus::Success) + co_return nullptr; + + for (auto s : serviceResult.Services()) + if (to_uuidstr(s.Uuid()) == service) + gattServices.insert(std::make_pair(service, s)); + } + co_return gattServices.at(service); } - co_return gattCharacteristics.at(characteristic); - } -}; -class QuickBluePlugin : public flutter::Plugin, public flutter::StreamHandler { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + IAsyncOperation GetCharacteristicAsync(std::string service, std::string characteristic) + { + if (gattCharacteristics.count(characteristic) == 0) + { + auto gattService = co_await GetServiceAsync(service); - QuickBluePlugin(); + auto characteristicResult = co_await gattService.GetCharacteristicsAsync(); + if (characteristicResult.Status() != GattCommunicationStatus::Success) + co_return nullptr; - virtual ~QuickBluePlugin(); + for (auto c : characteristicResult.Characteristics()) + if (to_uuidstr(c.Uuid()) == characteristic) + gattCharacteristics.insert(std::make_pair(characteristic, c)); + } + co_return gattCharacteristics.at(characteristic); + } + }; + + class QuickBluePlugin : public flutter::Plugin, public flutter::StreamHandler + { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + QuickBluePlugin(); + + virtual ~QuickBluePlugin(); + + private: + winrt::fire_and_forget InitializeAsync(); + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); + + std::unique_ptr> OnListenInternal( + const EncodableValue *arguments, + std::unique_ptr> &&events) override; + std::unique_ptr> OnCancelInternal( + const EncodableValue *arguments) override; + + std::unique_ptr> message_connector_; + + std::unique_ptr> availability_change_sink_; + std::unique_ptr> scan_result_sink_; + + Radio bluetoothRadio{nullptr}; + void Radio_StateChanged(Radio sender, IInspectable args); + RadioState oldRadioState = RadioState::Unknown; + + BluetoothLEAdvertisementWatcher bluetoothLEWatcher{nullptr}; + winrt::event_token bluetoothLEWatcherReceivedToken; + winrt::fire_and_forget BluetoothLEWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args); + + std::map> connectedDevices{}; + 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); + + winrt::fire_and_forget SetNotifiableAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, std::string service, std::string characteristic, std::string bleInputProperty); + winrt::fire_and_forget RequestMtuAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, uint64_t expectedMtu); + winrt::fire_and_forget ReadValueAsync(GattCharacteristic &gattCharacteristic); + winrt::fire_and_forget WriteValueAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, std::string service, std::string characteristic, std::vector value, std::string bleOutputProperty); + void QuickBluePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args); + }; + + // static + void QuickBluePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) + { + auto method = + std::make_unique>( + registrar->messenger(), "quick_blue/method", + &flutter::StandardMethodCodec::GetInstance()); + auto event_availability_change = + std::make_unique>( + registrar->messenger(), "quick_blue/event.availabilityChange", + &flutter::StandardMethodCodec::GetInstance()); + auto event_scan_result = + std::make_unique>( + registrar->messenger(), "quick_blue/event.scanResult", + &flutter::StandardMethodCodec::GetInstance()); + auto message_connector_ = + std::make_unique>( + registrar->messenger(), "quick_blue/message.connector", + &flutter::StandardMessageCodec::GetInstance()); + + auto plugin = std::make_unique(); + + method->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) + { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); - private: - winrt::fire_and_forget InitializeAsync(); + auto availability_handler = std::make_unique< + flutter::StreamHandlerFunctions<>>( + [plugin_pointer = plugin.get()]( + const EncodableValue *arguments, + std::unique_ptr> &&events) + -> std::unique_ptr> + { + return plugin_pointer->OnListen(arguments, std::move(events)); + }, + [plugin_pointer = plugin.get()](const EncodableValue *arguments) + -> std::unique_ptr> + { + return plugin_pointer->OnCancel(arguments); + }); + auto scan_result_handler = std::make_unique< + flutter::StreamHandlerFunctions<>>( + [plugin_pointer = plugin.get()]( + const EncodableValue *arguments, + std::unique_ptr> &&events) + -> std::unique_ptr> + { + return plugin_pointer->OnListen(arguments, std::move(events)); + }, + [plugin_pointer = plugin.get()](const EncodableValue *arguments) + -> std::unique_ptr> + { + return plugin_pointer->OnCancel(arguments); + }); + event_availability_change->SetStreamHandler(std::move(availability_handler)); + event_scan_result->SetStreamHandler(std::move(scan_result_handler)); + plugin->message_connector_ = std::move(message_connector_); - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result); - - std::unique_ptr> OnListenInternal( - const EncodableValue* arguments, - std::unique_ptr>&& events) override; - std::unique_ptr> OnCancelInternal( - const EncodableValue* arguments) override; - - std::unique_ptr> message_connector_; - - std::unique_ptr> availability_change_sink_; - std::unique_ptr> scan_result_sink_; - - Radio bluetoothRadio{ nullptr }; - void Radio_StateChanged(Radio sender, IInspectable args); - RadioState oldRadioState = RadioState::Unknown; - - BluetoothLEAdvertisementWatcher bluetoothLEWatcher{ nullptr }; - winrt::event_token bluetoothLEWatcherReceivedToken; - winrt::fire_and_forget BluetoothLEWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args); - - std::map> connectedDevices{}; - 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); - - winrt::fire_and_forget SetNotifiableAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, GattCharacteristic& gattCharacteristic, std::string bleInputProperty); - winrt::fire_and_forget RequestMtuAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, uint64_t expectedMtu); - winrt::fire_and_forget ReadValueAsync(GattCharacteristic& gattCharacteristic); - winrt::fire_and_forget WriteValueAsync(GattCharacteristic& gattCharacteristic, std::vector value, std::string bleOutputProperty); - void QuickBluePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args); -}; - -// static -void QuickBluePlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows *registrar) { - auto method = - std::make_unique>( - registrar->messenger(), "quick_blue/method", - &flutter::StandardMethodCodec::GetInstance()); - auto event_availability_change = - std::make_unique>( - registrar->messenger(), "quick_blue/event.availabilityChange", - &flutter::StandardMethodCodec::GetInstance()); - auto event_scan_result = - std::make_unique>( - registrar->messenger(), "quick_blue/event.scanResult", - &flutter::StandardMethodCodec::GetInstance()); - auto message_connector_ = - std::make_unique>( - registrar->messenger(), "quick_blue/message.connector", - &flutter::StandardMessageCodec::GetInstance()); - - auto plugin = std::make_unique(); - - method->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto &call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); + registrar->AddPlugin(std::move(plugin)); + } - auto availability_handler = std::make_unique< - flutter::StreamHandlerFunctions<>>( - [plugin_pointer = plugin.get()]( - const EncodableValue* arguments, - std::unique_ptr>&& events) - -> std::unique_ptr> { - return plugin_pointer->OnListen(arguments, std::move(events)); - }, - [plugin_pointer = plugin.get()](const EncodableValue* arguments) - -> std::unique_ptr> { - return plugin_pointer->OnCancel(arguments); - }); - auto scan_result_handler = std::make_unique< - flutter::StreamHandlerFunctions<>>( - [plugin_pointer = plugin.get()]( - const EncodableValue* arguments, - std::unique_ptr>&& events) - -> std::unique_ptr> { - return plugin_pointer->OnListen(arguments, std::move(events)); - }, - [plugin_pointer = plugin.get()](const EncodableValue* arguments) - -> std::unique_ptr> { - return plugin_pointer->OnCancel(arguments); - }); - event_availability_change->SetStreamHandler(std::move(availability_handler)); - event_scan_result->SetStreamHandler(std::move(scan_result_handler)); - plugin->message_connector_ = std::move(message_connector_); - - registrar->AddPlugin(std::move(plugin)); -} - -QuickBluePlugin::QuickBluePlugin() { - InitializeAsync(); -} - -QuickBluePlugin::~QuickBluePlugin() {} - -winrt::fire_and_forget QuickBluePlugin::InitializeAsync() { - auto bluetoothAdapter = co_await BluetoothAdapter::GetDefaultAsync(); - bluetoothRadio = co_await bluetoothAdapter.GetRadioAsync(); - if (bluetoothRadio) { - radioStateChangedRevoker = bluetoothRadio.StateChanged(winrt::auto_revoke, { this, &QuickBluePlugin::Radio_StateChanged }); + QuickBluePlugin::QuickBluePlugin() + { + InitializeAsync(); } -} - -void QuickBluePlugin::HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result) { - auto method_name = method_call.method_name(); - OutputDebugString((L"HandleMethodCall " + winrt::to_hstring(method_name) + L"\n").c_str()); - if (method_name.compare("isBluetoothAvailable") == 0) { - result->Success(EncodableValue(bluetoothRadio && bluetoothRadio.State() == RadioState::On)); - } else if (method_name.compare("startScan") == 0) { - if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) { - if (!bluetoothLEWatcher) { - bluetoothLEWatcher = BluetoothLEAdvertisementWatcher(); - bluetoothLEWatcherReceivedToken = bluetoothLEWatcher.Received({ this, &QuickBluePlugin::BluetoothLEWatcher_Received }); + + QuickBluePlugin::~QuickBluePlugin() {} + + winrt::fire_and_forget QuickBluePlugin::InitializeAsync() + { + auto bluetoothAdapter = co_await BluetoothAdapter::GetDefaultAsync(); + bluetoothRadio = co_await bluetoothAdapter.GetRadioAsync(); + if (bluetoothRadio) + { + radioStateChangedRevoker = bluetoothRadio.StateChanged(winrt::auto_revoke, {this, &QuickBluePlugin::Radio_StateChanged}); + } + } + + void QuickBluePlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) + { + auto method_name = method_call.method_name(); + OutputDebugString((L"HandleMethodCall " + winrt::to_hstring(method_name) + L"\n").c_str()); + if (method_name.compare("isBluetoothAvailable") == 0) + { + result->Success(EncodableValue(bluetoothRadio && bluetoothRadio.State() == RadioState::On)); + } + else if (method_name.compare("startScan") == 0) + { + if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) + { + if (!bluetoothLEWatcher) + { + bluetoothLEWatcher = BluetoothLEAdvertisementWatcher(); + bluetoothLEWatcherReceivedToken = bluetoothLEWatcher.Received({this, &QuickBluePlugin::BluetoothLEWatcher_Received}); + } + + auto args = std::get(*method_call.arguments()); + auto serviceUUIDs = std::get(args[EncodableValue("serviceUUIDs")]); + + if (serviceUUIDs.size() > 0) + { + auto filter = BluetoothLEAdvertisementFilter(); + auto advert = BluetoothLEAdvertisement(); + for (size_t i = 0; i < serviceUUIDs.size(); i++) + { + std::string serviceString = std::get(serviceUUIDs[i]); + auto serviceGuid = make_guid(LongUUID(serviceString).c_str()); + advert.ServiceUuids().Append(serviceGuid); + OutputDebugStringA(to_uuidstr(serviceGuid).c_str()); + } + filter.Advertisement(advert); + bluetoothLEWatcher.AdvertisementFilter(filter); + } + else + { + } + + bluetoothLEWatcher.Start(); + result->Success(nullptr); } - bluetoothLEWatcher.Start(); - result->Success(nullptr); - } else { - result->Error("IllegalState", "Bluetooth unavailable"); - } - } else if (method_name.compare("stopScan") == 0) { - if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) { - if (bluetoothLEWatcher) { - bluetoothLEWatcher.Stop(); - bluetoothLEWatcher.Received(bluetoothLEWatcherReceivedToken); + else + { + result->Error("IllegalState", "Bluetooth unavailable"); + } + } + else if (method_name.compare("stopScan") == 0) + { + if (bluetoothRadio && bluetoothRadio.State() == RadioState::On) + { + if (bluetoothLEWatcher) + { + bluetoothLEWatcher.Stop(); + bluetoothLEWatcher.Received(bluetoothLEWatcherReceivedToken); + } + bluetoothLEWatcher = nullptr; + result->Success(nullptr); } - bluetoothLEWatcher = nullptr; + else + { + result->Error("IllegalState", "Bluetooth unavailable"); + } + } + else if (method_name.compare("connect") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + ConnectAsync(std::stoull(deviceId)); result->Success(nullptr); - } else { - result->Error("IllegalState", "Bluetooth unavailable"); - } - } else if (method_name.compare("connect") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - ConnectAsync(std::stoull(deviceId)); - result->Success(nullptr); - } else if (method_name.compare("disconnect") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - CleanConnection(std::stoull(deviceId)); - // TODO send `disconnected` message - result->Success(nullptr); - } else if (method_name.compare("discoverServices") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - auto it = connectedDevices.find(std::stoull(deviceId)); - if (it == connectedDevices.end()) { - result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); - return; } - DiscoverServicesAsync(*it->second); - result->Success(nullptr); - } else if (method_name.compare("setNotifiable") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - auto service = std::get(args[EncodableValue("service")]); - auto characteristic = std::get(args[EncodableValue("characteristic")]); - auto bleInputProperty = std::get(args[EncodableValue("bleInputProperty")]); - auto it = connectedDevices.find(std::stoull(deviceId)); - if (it == connectedDevices.end()) { - result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); - return; + else if (method_name.compare("disconnect") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + CleanConnection(std::stoull(deviceId)); + // TODO send `disconnected` message + result->Success(nullptr); } + else if (method_name.compare("discoverServices") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + auto it = connectedDevices.find(std::stoull(deviceId)); + if (it == connectedDevices.end()) + { + result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); + return; + } + DiscoverServicesAsync(*it->second); + result->Success(nullptr); + } + else if (method_name.compare("setNotifiable") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + auto service = std::get(args[EncodableValue("service")]); + auto characteristic = std::get(args[EncodableValue("characteristic")]); + auto bleInputProperty = std::get(args[EncodableValue("bleInputProperty")]); + auto it = connectedDevices.find(std::stoull(deviceId)); + if (it == connectedDevices.end()) + { + result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); + return; + } - auto bluetoothAgent = *it->second; - auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); - async_c.Completed([&, result_pointer = result.get()] - (IAsyncOperation const& sender, AsyncStatus const args) { - // FIXME https://github.com/woodemi/quick.flutter/pull/31#issuecomment-1159213902 - auto c = sender.GetResults(); - if (c == nullptr) { - result_pointer->Error("IllegalArgument", "Unknown characteristic:" + characteristic); - return; - } - SetNotifiableAsync(bluetoothAgent, c, bleInputProperty); - result_pointer->Success(nullptr); - }); - } else if (method_name.compare("readValue") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - auto service = std::get(args[EncodableValue("service")]); - auto characteristic = std::get(args[EncodableValue("characteristic")]); - auto it = connectedDevices.find(std::stoull(deviceId)); - if (it == connectedDevices.end()) { - result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); - return; + SetNotifiableAsync(*it->second, service, characteristic, bleInputProperty); + result->Success(nullptr); } + else if (method_name.compare("readValue") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + auto service = std::get(args[EncodableValue("service")]); + auto characteristic = std::get(args[EncodableValue("characteristic")]); + auto it = connectedDevices.find(std::stoull(deviceId)); + if (it == connectedDevices.end()) + { + result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); + return; + } - auto bluetoothAgent = *it->second; - auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); - async_c.Completed([&, result_pointer = result.get()] - (IAsyncOperation const& sender, AsyncStatus const args) { + auto bluetoothAgent = *it->second; + auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); + async_c.Completed([&, result_pointer = result.get()](IAsyncOperation const &sender, AsyncStatus const args) + { // FIXME https://github.com/woodemi/quick.flutter/pull/31#issuecomment-1159213902 auto c = sender.GetResults(); if (c == nullptr) { @@ -335,289 +456,330 @@ void QuickBluePlugin::HandleMethodCall( return; } ReadValueAsync(c); - result_pointer->Success(nullptr); - }); - } else if (method_name.compare("writeValue") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - auto service = std::get(args[EncodableValue("service")]); - auto characteristic = std::get(args[EncodableValue("characteristic")]); - auto value = std::get>(args[EncodableValue("value")]); - auto bleOutputProperty = std::get(args[EncodableValue("bleOutputProperty")]); - auto it = connectedDevices.find(std::stoull(deviceId)); - if (it == connectedDevices.end()) { - result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); - return; + result_pointer->Success(nullptr); }); } + else if (method_name.compare("writeValue") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + auto service = std::get(args[EncodableValue("service")]); + auto characteristic = std::get(args[EncodableValue("characteristic")]); + auto value = std::get>(args[EncodableValue("value")]); + auto bleOutputProperty = std::get(args[EncodableValue("bleOutputProperty")]); + auto it = connectedDevices.find(std::stoull(deviceId)); + if (it == connectedDevices.end()) + { + result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); + return; + } - auto bluetoothAgent = *it->second; - auto async_c = bluetoothAgent.GetCharacteristicAsync(service, characteristic); - async_c.Completed([&, result_pointer = result.get()] - (IAsyncOperation const& sender, AsyncStatus const args) { - // FIXME https://github.com/woodemi/quick.flutter/pull/31#issuecomment-1159213902 - auto c = sender.GetResults(); - if (c == nullptr) { - result_pointer->Error("IllegalArgument", "Unknown characteristic:" + characteristic); - return; - } - WriteValueAsync(c, value, bleOutputProperty); - result_pointer->Success(nullptr); - }); - } else if (method_name.compare("requestMtu") == 0) { - auto args = std::get(*method_call.arguments()); - auto deviceId = std::get(args[EncodableValue("deviceId")]); - auto expectedMtu = std::get(args[EncodableValue("expectedMtu")]); - auto it = connectedDevices.find(std::stoull(deviceId)); - if (it == connectedDevices.end()) { - result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); - return; + WriteValueAsync(*it->second, service, characteristic, value, bleOutputProperty); + result->Success(nullptr); } + else if (method_name.compare("requestMtu") == 0) + { + auto args = std::get(*method_call.arguments()); + auto deviceId = std::get(args[EncodableValue("deviceId")]); + auto expectedMtu = std::get(args[EncodableValue("expectedMtu")]); + auto it = connectedDevices.find(std::stoull(deviceId)); + if (it == connectedDevices.end()) + { + result->Error("IllegalArgument", "Unknown devicesId:" + deviceId); + return; + } - RequestMtuAsync(*it->second, expectedMtu); - result->Success(nullptr); - } else { - result->NotImplemented(); + RequestMtuAsync(*it->second, expectedMtu); + result->Success(nullptr); + } + else + { + result->NotImplemented(); + } } -} -std::vector parseManufacturerDataHead(BluetoothLEAdvertisement advertisement) -{ - 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; -} - -enum class AvailabilityState : int { - unknown = 0, - resetting = 1, - unsupported = 2, - unauthorized = 3, - poweredOff = 4, - poweredOn = 5, -}; - -void QuickBluePlugin::Radio_StateChanged(Radio radio, IInspectable args) { - auto radioState = !radio ? RadioState::Disabled : radio.State(); - // FIXME https://stackoverflow.com/questions/66099947/bluetooth-radio-statechanged-event-fires-twice/67723902#67723902 - if (oldRadioState == radioState) { - return; - } - oldRadioState = 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; - } - }(); - - if (availability_change_sink_) { - availability_change_sink_->Success(static_cast(state)); - } -} - -winrt::fire_and_forget QuickBluePlugin::BluetoothLEWatcher_Received( - BluetoothLEAdvertisementWatcher sender, - BluetoothLEAdvertisementReceivedEventArgs args) { - auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(args.BluetoothAddress()); - auto name = device ? device.Name() : args.Advertisement().LocalName(); - OutputDebugString((L"Received BluetoothAddress:" + winrt::to_hstring(args.BluetoothAddress()) - + L", Name:" + name + L", LocalName:" + args.Advertisement().LocalName() + L"\n").c_str()); - if (scan_result_sink_) { - scan_result_sink_->Success(EncodableMap{ - {"name", winrt::to_string(name)}, - {"deviceId", std::to_string(args.BluetoothAddress())}, - {"manufacturerDataHead", parseManufacturerDataHead(args.Advertisement())}, - {"rssi", args.RawSignalStrengthInDBm()}, - }); + std::vector parseManufacturerDataHead(BluetoothLEAdvertisement advertisement) + { + 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; } -} -std::unique_ptr> QuickBluePlugin::OnListenInternal( - const EncodableValue* arguments, std::unique_ptr>&& events) -{ - if (arguments == nullptr) { - return nullptr; + enum class AvailabilityState : int + { + unknown = 0, + resetting = 1, + unsupported = 2, + unauthorized = 3, + poweredOff = 4, + poweredOn = 5, + }; + + void QuickBluePlugin::Radio_StateChanged(Radio radio, IInspectable args) + { + auto radioState = !radio ? RadioState::Disabled : radio.State(); + // FIXME https://stackoverflow.com/questions/66099947/bluetooth-radio-statechanged-event-fires-twice/67723902#67723902 + if (oldRadioState == radioState) + { + return; + } + oldRadioState = 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; + } + }(); + + if (availability_change_sink_) + { + availability_change_sink_->Success(static_cast(state)); + } } - auto args = std::get(*arguments); - auto name = std::get(args[EncodableValue("name")]); - if (name.compare("availabilityChange") == 0) { - availability_change_sink_ = std::move(events); - Radio_StateChanged(bluetoothRadio, nullptr); - } else if (name.compare("scanResult") == 0) { - scan_result_sink_ = std::move(events); + + winrt::fire_and_forget QuickBluePlugin::BluetoothLEWatcher_Received( + BluetoothLEAdvertisementWatcher sender, + BluetoothLEAdvertisementReceivedEventArgs args) + { + auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(args.BluetoothAddress()); + auto name = device ? device.Name() : args.Advertisement().LocalName(); + OutputDebugString((L"Received BluetoothAddress:" + winrt::to_hstring(args.BluetoothAddress()) + L", Name:" + name + L", LocalName:" + args.Advertisement().LocalName() + L"\n").c_str()); + if (scan_result_sink_) + { + scan_result_sink_->Success(EncodableMap{ + {"name", winrt::to_string(name)}, + {"deviceId", std::to_string(args.BluetoothAddress())}, + {"manufacturerDataHead", parseManufacturerDataHead(args.Advertisement())}, + {"rssi", args.RawSignalStrengthInDBm()}, + }); + } } - return nullptr; -} -std::unique_ptr> QuickBluePlugin::OnCancelInternal( - const EncodableValue* arguments) -{ - if (arguments == nullptr) { + std::unique_ptr> QuickBluePlugin::OnListenInternal( + const EncodableValue *arguments, std::unique_ptr> &&events) + { + if (arguments == nullptr) + { + return nullptr; + } + auto args = std::get(*arguments); + auto name = std::get(args[EncodableValue("name")]); + if (name.compare("availabilityChange") == 0) + { + availability_change_sink_ = std::move(events); + Radio_StateChanged(bluetoothRadio, nullptr); + } + else if (name.compare("scanResult") == 0) + { + scan_result_sink_ = std::move(events); + } return nullptr; } - auto args = std::get(*arguments); - auto name = std::get(args[EncodableValue("name")]); - if (name.compare("availabilityChange") == 0) { - availability_change_sink_ = nullptr; - } else if (name.compare("scanResult") == 0) { + + std::unique_ptr> QuickBluePlugin::OnCancelInternal( + const EncodableValue *arguments) + { + if (arguments == nullptr) + { + return nullptr; + } + auto args = std::get(*arguments); + auto name = std::get(args[EncodableValue("name")]); + if (name.compare("availabilityChange") == 0) + { + availability_change_sink_ = nullptr; + } + else if (name.compare("scanResult") == 0) + { scan_result_sink_ = nullptr; + } + return nullptr; } - return nullptr; -} - -winrt::fire_and_forget QuickBluePlugin::ConnectAsync(uint64_t bluetoothAddress) { - auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetoothAddress); - auto servicesResult = co_await device.GetGattServicesAsync(); - if (servicesResult.Status() != GattCommunicationStatus::Success) { - OutputDebugString((L"GetGattServicesAsync error: " + winrt::to_hstring((int32_t)servicesResult.Status()) + L"\n").c_str()); - message_connector_->Send(EncodableMap{ - {"deviceId", std::to_string(bluetoothAddress)}, - {"ConnectionState", "disconnected"}, - }); - co_return; - } - auto connnectionStatusChangedToken = device.ConnectionStatusChanged({ this, &QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged }); - auto deviceAgent = std::make_unique(device, connnectionStatusChangedToken); - auto pair = std::make_pair(bluetoothAddress, std::move(deviceAgent)); - connectedDevices.insert(std::move(pair)); - - message_connector_->Send(EncodableMap{ - {"deviceId", std::to_string(bluetoothAddress)}, - {"ConnectionState", "connected"}, - }); -} - -void QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args) { - OutputDebugString((L"ConnectionStatusChanged " + winrt::to_hstring((int32_t)sender.ConnectionStatus()) + L"\n").c_str()); - if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) { - CleanConnection(sender.BluetoothAddress()); + + winrt::fire_and_forget QuickBluePlugin::ConnectAsync(uint64_t bluetoothAddress) + { + auto device = co_await BluetoothLEDevice::FromBluetoothAddressAsync(bluetoothAddress); + if (!device) { + OutputDebugString((L"ConnectAsync null device! "+ std::to_wstring(bluetoothAddress) +L"\n").c_str()); + co_return; + } + auto servicesResult = co_await device.GetGattServicesAsync(); + if (servicesResult.Status() != GattCommunicationStatus::Success) + { + OutputDebugString((L"GetGattServicesAsync error: " + winrt::to_hstring((int32_t)servicesResult.Status()) + L"\n").c_str()); + message_connector_->Send(EncodableMap{ + {"deviceId", std::to_string(bluetoothAddress)}, + {"ConnectionState", "disconnected"}, + }); + co_return; + } + auto connnectionStatusChangedToken = device.ConnectionStatusChanged({this, &QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged}); + auto deviceAgent = std::make_unique(device, connnectionStatusChangedToken); + auto pair = std::make_pair(bluetoothAddress, std::move(deviceAgent)); + connectedDevices.insert(std::move(pair)); + message_connector_->Send(EncodableMap{ - {"deviceId", std::to_string(sender.BluetoothAddress())}, - {"ConnectionState", "disconnected"}, + {"deviceId", std::to_string(bluetoothAddress)}, + {"ConnectionState", "connected"}, }); } -} -void QuickBluePlugin::CleanConnection(uint64_t bluetoothAddress) { - auto node = connectedDevices.extract(bluetoothAddress); - if (!node.empty()) { - auto deviceAgent = std::move(node.mapped()); - deviceAgent->device.ConnectionStatusChanged(deviceAgent->connnectionStatusChangedToken); - for (auto& tokenPair : deviceAgent->valueChangedTokens) { - deviceAgent->gattCharacteristics.at(tokenPair.first).ValueChanged(tokenPair.second); + void QuickBluePlugin::BluetoothLEDevice_ConnectionStatusChanged(BluetoothLEDevice sender, IInspectable args) + { + OutputDebugString((L"ConnectionStatusChanged " + winrt::to_hstring((int32_t)sender.ConnectionStatus()) + L"\n").c_str()); + if (sender.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) + { + CleanConnection(sender.BluetoothAddress()); + message_connector_->Send(EncodableMap{ + {"deviceId", std::to_string(sender.BluetoothAddress())}, + {"ConnectionState", "disconnected"}, + }); } } -} - -winrt::fire_and_forget QuickBluePlugin::DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent) { - auto serviceResult = co_await bluetoothDeviceAgent.device.GetGattServicesAsync(); - if (serviceResult.Status() != GattCommunicationStatus::Success) { - message_connector_->Send( - EncodableMap{ - {"deviceId", std::to_string(bluetoothDeviceAgent.device.BluetoothAddress())}, - {"ServiceState", "discovered"} + + void QuickBluePlugin::CleanConnection(uint64_t bluetoothAddress) + { + auto node = connectedDevices.extract(bluetoothAddress); + if (!node.empty()) + { + auto deviceAgent = std::move(node.mapped()); + deviceAgent->device.ConnectionStatusChanged(deviceAgent->connnectionStatusChangedToken); + for (auto &tokenPair : deviceAgent->valueChangedTokens) + { + deviceAgent->gattCharacteristics.at(tokenPair.first).ValueChanged(tokenPair.second); } - ); - co_return; + } } - for (auto s : serviceResult.Services()) { - auto characteristicResult = co_await s.GetCharacteristicsAsync(); - auto msg = EncodableMap{ - {"deviceId", std::to_string(bluetoothDeviceAgent.device.BluetoothAddress())}, - {"ServiceState", "discovered"}, - {"service", to_uuidstr(s.Uuid())} - }; - if (characteristicResult.Status() == GattCommunicationStatus::Success) { - EncodableList characteristics; - for (auto c : characteristicResult.Characteristics()) { - characteristics.push_back(to_uuidstr(c.Uuid())); + winrt::fire_and_forget QuickBluePlugin::DiscoverServicesAsync(BluetoothDeviceAgent &bluetoothDeviceAgent) + { + auto serviceResult = co_await bluetoothDeviceAgent.device.GetGattServicesAsync(); + if (serviceResult.Status() != GattCommunicationStatus::Success) + { + message_connector_->Send( + EncodableMap{ + {"deviceId", std::to_string(bluetoothDeviceAgent.device.BluetoothAddress())}, + {"ServiceState", "discovered"}}); + co_return; + } + + for (auto s : serviceResult.Services()) + { + auto characteristicResult = co_await s.GetCharacteristicsAsync(); + auto msg = EncodableMap{ + {"deviceId", std::to_string(bluetoothDeviceAgent.device.BluetoothAddress())}, + {"ServiceState", "discovered"}, + {"service", to_uuidstr(s.Uuid())}}; + if (characteristicResult.Status() == GattCommunicationStatus::Success) + { + EncodableList characteristics; + for (auto c : characteristicResult.Characteristics()) + { + characteristics.push_back(to_uuidstr(c.Uuid())); + } + msg.insert({"characteristics", characteristics}); } - msg.insert({"characteristics", characteristics}); + message_connector_->Send(msg); } - message_connector_->Send(msg); } -} - -winrt::fire_and_forget QuickBluePlugin::RequestMtuAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, uint64_t expectedMtu) { - OutputDebugString(L"RequestMtuAsync expectedMtu"); - auto gattSession = co_await GattSession::FromDeviceIdAsync(bluetoothDeviceAgent.device.BluetoothDeviceId()); - message_connector_->Send(EncodableMap{ - {"mtuConfig", (int64_t)gattSession.MaxPduSize()}, - }); -} - -winrt::fire_and_forget QuickBluePlugin::SetNotifiableAsync(BluetoothDeviceAgent& bluetoothDeviceAgent, GattCharacteristic& gattCharacteristic, std::string bleInputProperty) { - auto descriptorValue = bleInputProperty == "notification" ? GattClientCharacteristicConfigurationDescriptorValue::Notify - : bleInputProperty == "indication" ? GattClientCharacteristicConfigurationDescriptorValue::Indicate - : GattClientCharacteristicConfigurationDescriptorValue::None; - auto writeDescriptorStatus = co_await gattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(descriptorValue); - if (writeDescriptorStatus != GattCommunicationStatus::Success) - OutputDebugString((L"WriteClientCharacteristicConfigurationDescriptorAsync " + winrt::to_hstring((int32_t)writeDescriptorStatus) + L"\n").c_str()); - - auto uuid = to_uuidstr(gattCharacteristic.Uuid()); - if (bleInputProperty != "disabled") { - bluetoothDeviceAgent.valueChangedTokens[uuid] = gattCharacteristic.ValueChanged({ this, &QuickBluePlugin::GattCharacteristic_ValueChanged }); - } else { - gattCharacteristic.ValueChanged(std::exchange(bluetoothDeviceAgent.valueChangedTokens[uuid], {})); + + winrt::fire_and_forget QuickBluePlugin::RequestMtuAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, uint64_t expectedMtu) + { + OutputDebugString(L"RequestMtuAsync expectedMtu"); + auto gattSession = co_await GattSession::FromDeviceIdAsync(bluetoothDeviceAgent.device.BluetoothDeviceId()); + message_connector_->Send(EncodableMap{ + {"mtuConfig", (int64_t)gattSession.MaxPduSize()}, + }); } -} - -winrt::fire_and_forget QuickBluePlugin::ReadValueAsync(GattCharacteristic& gattCharacteristic) { - auto readValueResult = co_await gattCharacteristic.ReadValueAsync(); - auto uuid = to_uuidstr(gattCharacteristic.Uuid()); - auto bytes = to_bytevc(readValueResult.Value()); - OutputDebugString((L"ReadValueAsync " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(bytes)) + L"\n").c_str()); - message_connector_->Send(EncodableMap{ - {"deviceId", std::to_string(gattCharacteristic.Service().Device().BluetoothAddress())}, - {"characteristicValue", EncodableMap{ - {"characteristic", uuid}, - {"value", bytes}, - }}, - }); -} - -winrt::fire_and_forget QuickBluePlugin::WriteValueAsync(GattCharacteristic& gattCharacteristic, std::vector value, std::string bleOutputProperty) { - auto writeOption = bleOutputProperty.compare("withoutResponse") == 0 ? GattWriteOption::WriteWithoutResponse : GattWriteOption::WriteWithResponse; - auto writeValueStatus = co_await gattCharacteristic.WriteValueAsync(from_bytevc(value), writeOption); - auto uuid = to_uuidstr(gattCharacteristic.Uuid()); - OutputDebugString((L"WriteValueAsync " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(value)) + L", " + winrt::to_hstring((int32_t)writeValueStatus) + L"\n").c_str()); -} - -void QuickBluePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) { - auto uuid = to_uuidstr(sender.Uuid()); - auto bytes = to_bytevc(args.CharacteristicValue()); - OutputDebugString((L"GattCharacteristic_ValueChanged " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(bytes)) + L"\n").c_str()); - message_connector_->Send(EncodableMap{ - {"deviceId", std::to_string(sender.Service().Device().BluetoothAddress())}, - {"characteristicValue", EncodableMap{ - {"characteristic", uuid}, - {"value", bytes}, - }}, - }); -} - -} // namespace + + winrt::fire_and_forget QuickBluePlugin::SetNotifiableAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, std::string service, std::string characteristic, std::string bleInputProperty) + { + auto gattCharacteristic = co_await bluetoothDeviceAgent.GetCharacteristicAsync(service, characteristic); + auto descriptorValue = bleInputProperty == "notification" ? GattClientCharacteristicConfigurationDescriptorValue::Notify + : bleInputProperty == "indication" ? GattClientCharacteristicConfigurationDescriptorValue::Indicate + : GattClientCharacteristicConfigurationDescriptorValue::None; + auto writeDescriptorStatus = co_await gattCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(descriptorValue); + if (writeDescriptorStatus != GattCommunicationStatus::Success) + OutputDebugString((L"WriteClientCharacteristicConfigurationDescriptorAsync " + winrt::to_hstring((int32_t)writeDescriptorStatus) + L"\n").c_str()); + + if (bleInputProperty != "disabled") + { + bluetoothDeviceAgent.valueChangedTokens[characteristic] = gattCharacteristic.ValueChanged({this, &QuickBluePlugin::GattCharacteristic_ValueChanged}); + } + else + { + gattCharacteristic.ValueChanged(std::exchange(bluetoothDeviceAgent.valueChangedTokens[characteristic], {})); + } + } + + winrt::fire_and_forget QuickBluePlugin::ReadValueAsync(GattCharacteristic &gattCharacteristic) + { + auto readValueResult = co_await gattCharacteristic.ReadValueAsync(); + auto uuid = to_uuidstr(gattCharacteristic.Uuid()); + auto bytes = to_bytevc(readValueResult.Value()); + OutputDebugString((L"ReadValueAsync " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(bytes)) + L"\n").c_str()); + message_connector_->Send(EncodableMap{ + {"deviceId", std::to_string(gattCharacteristic.Service().Device().BluetoothAddress())}, + {"characteristicValue", EncodableMap{ + {"characteristic", uuid}, + {"value", bytes}, + }}, + }); + } + + winrt::fire_and_forget QuickBluePlugin::WriteValueAsync(BluetoothDeviceAgent &bluetoothDeviceAgent, std::string service, std::string characteristic, std::vector value, std::string bleOutputProperty) + { + auto gattCharacteristic = co_await bluetoothDeviceAgent.GetCharacteristicAsync(service, characteristic); + auto writeOption = bleOutputProperty.compare("withoutResponse") == 0 ? GattWriteOption::WriteWithoutResponse : GattWriteOption::WriteWithResponse; + auto writeValueStatus = co_await gattCharacteristic.WriteValueAsync(from_bytevc(value), writeOption); + OutputDebugString((L"WriteValueAsync " + winrt::to_hstring(characteristic) + L", " + winrt::to_hstring(to_hexstring(value)) + L", " + winrt::to_hstring((int32_t)writeValueStatus) + L"\n").c_str()); + } + + void QuickBluePlugin::GattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) + { + auto uuid = to_uuidstr(sender.Uuid()); + auto bytes = to_bytevc(args.CharacteristicValue()); + OutputDebugString((L"GattCharacteristic_ValueChanged " + winrt::to_hstring(uuid) + L", " + winrt::to_hstring(to_hexstring(bytes)) + L"\n").c_str()); + message_connector_->Send(EncodableMap{ + {"deviceId", std::to_string(sender.Service().Device().BluetoothAddress())}, + {"characteristicValue", EncodableMap{ + {"characteristic", uuid}, + {"value", bytes}, + }}, + }); + } + +} // namespace void QuickBluePluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { + FlutterDesktopPluginRegistrarRef registrar) +{ QuickBluePlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); -} +} \ No newline at end of file diff --git a/packages/quick_hid/example/test/widget_test.dart b/packages/quick_hid/example/test/widget_test.dart index d5b3979..68b06dc 100644 --- a/packages/quick_hid/example/test/widget_test.dart +++ b/packages/quick_hid/example/test/widget_test.dart @@ -8,7 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:quick_hid_example/main.dart'; +// ignore: avoid_relative_lib_imports +import '../lib/main.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { @@ -18,8 +19,8 @@ void main() { // Verify that platform version is retrieved. expect( find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), + (Widget widget) => + widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); diff --git a/packages/quick_hid/pubspec.yaml b/packages/quick_hid/pubspec.yaml index 4a02b4e..afad44f 100644 --- a/packages/quick_hid/pubspec.yaml +++ b/packages/quick_hid/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/quick_notify/example/analysis_options.yaml b/packages/quick_notify/example/analysis_options.yaml index 61b6c4d..53a0830 100644 --- a/packages/quick_notify/example/analysis_options.yaml +++ b/packages/quick_notify/example/analysis_options.yaml @@ -22,8 +22,7 @@ linter: # `// 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 + 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/packages/quick_notify/example/lib/main.dart b/packages/quick_notify/example/lib/main.dart index debcd8e..0d93eb1 100644 --- a/packages/quick_notify/example/lib/main.dart +++ b/packages/quick_notify/example/lib/main.dart @@ -3,10 +3,12 @@ import 'package:flutter/material.dart'; import 'package:quick_notify/quick_notify.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override _MyAppState createState() => _MyAppState(); } @@ -25,23 +27,24 @@ class _MyAppState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( - child: Text('hasPermission'), + child: const Text('hasPermission'), onPressed: () async { var hasPermission = await QuickNotify.hasPermission(); print('hasPermission $hasPermission'); }, ), ElevatedButton( - child: Text('requestPermission'), + child: const Text('requestPermission'), onPressed: () async { - var requestPermission = await QuickNotify.requestPermission(); + var requestPermission = + await QuickNotify.requestPermission(); print('requestPermission $requestPermission'); }, ), ], ), ElevatedButton( - child: Text('notify'), + child: const Text('notify'), onPressed: () { QuickNotify.notify( title: 'My title', diff --git a/packages/quick_notify/pubspec.yaml b/packages/quick_notify/pubspec.yaml index 3964215..7c9da5b 100644 --- a/packages/quick_notify/pubspec.yaml +++ b/packages/quick_notify/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/quick_scan/pubspec.yaml b/packages/quick_scan/pubspec.yaml index 09d02e5..b195e76 100644 --- a/packages/quick_scan/pubspec.yaml +++ b/packages/quick_scan/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/quick_usb/lib/src/quick_usb_android.dart b/packages/quick_usb/lib/src/quick_usb_android.dart index 356b5b1..ec9b112 100644 --- a/packages/quick_usb/lib/src/quick_usb_android.dart +++ b/packages/quick_usb/lib/src/quick_usb_android.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:quick_usb/src/common.dart'; @@ -25,7 +24,8 @@ class QuickUsbAndroid extends QuickUsbPlatform { @override Future> getDeviceList() async { - List> devices = (await _channel.invokeListMethod('getDeviceList'))!; + List> devices = + (await _channel.invokeListMethod('getDeviceList'))!; return devices.map((device) => UsbDevice.fromMap(device)).toList(); } diff --git a/packages/quick_usb/lib/src/quick_usb_desktop.dart b/packages/quick_usb/lib/src/quick_usb_desktop.dart index 82bb636..015ec44 100644 --- a/packages/quick_usb/lib/src/quick_usb_desktop.dart +++ b/packages/quick_usb/lib/src/quick_usb_desktop.dart @@ -1,6 +1,5 @@ import 'dart:ffi'; import 'dart:io'; -import 'dart:typed_data'; import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart'; @@ -32,7 +31,8 @@ class QuickUsbLinux extends _QuickUsbDesktop { // For example/.dart_tool/flutter_build/generated_main.dart static registerWith() { QuickUsbPlatform.instance = QuickUsbLinux(); - _libusb = Libusb(DynamicLibrary.open('${File(Platform.resolvedExecutable).parent.path}/lib/libusb-1.0.23.so')); + _libusb = Libusb(DynamicLibrary.open( + '${File(Platform.resolvedExecutable).parent.path}/lib/libusb-1.0.23.so')); } } @@ -89,7 +89,8 @@ class _QuickUsbDesktop extends QuickUsbPlatform { } @override - Future> getDevicesWithDescription({bool requestPermission = true}) async { + Future> getDevicesWithDescription( + {bool requestPermission = true}) async { var devices = await getDeviceList(); var result = []; for (var device in devices) { @@ -99,7 +100,8 @@ class _QuickUsbDesktop extends QuickUsbPlatform { } @override - Future getDeviceDescription(UsbDevice usbDevice, {bool requestPermission = true}) async { + Future getDeviceDescription(UsbDevice usbDevice, + {bool requestPermission = true}) async { String? manufacturer; String? product; String? serialNumber; diff --git a/packages/quick_usb/pubspec.yaml b/packages/quick_usb/pubspec.yaml index 76b3890..bdf6c35 100644 --- a/packages/quick_usb/pubspec.yaml +++ b/packages/quick_usb/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/quick_wlan/pubspec.yaml b/packages/quick_wlan/pubspec.yaml index 35aa83c..6dc0962 100644 --- a/packages/quick_wlan/pubspec.yaml +++ b/packages/quick_wlan/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^2.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec