From 3b2571d150ae300022a6352060e3a4a10888c59a Mon Sep 17 00:00:00 2001 From: yuntang <123@qq.com> Date: Tue, 23 Jun 2026 11:27:35 +0800 Subject: [PATCH] 2026-06-23T11:27:35 --- push.sh | 22 + src/.gitignore | 4 + src/App.uvue | 51 + src/android-logcat.log | 109 ++ src/index.html | 20 + src/launch-android.log | 119 +++ src/main.uts | 9 + src/manifest.json | 64 ++ src/pages.json | 23 + src/pages/data/data.uvue | 468 +++++++++ src/pages/index/index.uvue | 274 +++++ src/platformConfig.json | 7 + src/static/logo.png | Bin 0 -> 4023 bytes src/uni.scss | 76 ++ src/uni_modules/sp2-bluetooth/package.json | 50 + .../utssdk/app-android/AndroidManifest.xml | 24 + .../utssdk/app-android/hybrid.kt | 972 ++++++++++++++++++ .../utssdk/app-android/index.uts | 101 ++ .../sp2-bluetooth/utssdk/interface.uts | 21 + src/utils/ecBLE.js | 388 +++++++ 20 files changed, 2802 insertions(+) create mode 100644 push.sh create mode 100644 src/.gitignore create mode 100644 src/App.uvue create mode 100644 src/android-logcat.log create mode 100644 src/index.html create mode 100644 src/launch-android.log create mode 100644 src/main.uts create mode 100644 src/manifest.json create mode 100644 src/pages.json create mode 100644 src/pages/data/data.uvue create mode 100644 src/pages/index/index.uvue create mode 100644 src/platformConfig.json create mode 100644 src/static/logo.png create mode 100644 src/uni.scss create mode 100644 src/uni_modules/sp2-bluetooth/package.json create mode 100644 src/uni_modules/sp2-bluetooth/utssdk/app-android/AndroidManifest.xml create mode 100644 src/uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt create mode 100644 src/uni_modules/sp2-bluetooth/utssdk/app-android/index.uts create mode 100644 src/uni_modules/sp2-bluetooth/utssdk/interface.uts create mode 100644 src/utils/ecBLE.js diff --git a/push.sh b/push.sh new file mode 100644 index 0000000..b654beb --- /dev/null +++ b/push.sh @@ -0,0 +1,22 @@ +#!/bin/bash +datetime=`date +%FT%T` + +echo "-------------------------------------" +echo "git add -A" +echo "-------------------------------------" +git add -A + +echo "-------------------------------------" +echo "git status" +echo "-------------------------------------" +git status + +echo "-------------------------------------" +echo "git commit -m ${datetime} " +echo "-------------------------------------" +git commit -m "${datetime}" + +echo "-------------------------------------" +echo "git push" +echo "-------------------------------------" +git push \ No newline at end of file diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..2370852 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +unpackage/ +.hbuilderx/ +.DS_Store diff --git a/src/App.uvue b/src/App.uvue new file mode 100644 index 0000000..9101cf2 --- /dev/null +++ b/src/App.uvue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/src/android-logcat.log b/src/android-logcat.log new file mode 100644 index 0000000..08ade35 --- /dev/null +++ b/src/android-logcat.log @@ -0,0 +1,109 @@ +17:07:47.041 HBuilderX Version: 5.06 +17:07:47.042 项目 app_sp2 开始编译 +17:07:47.044 请注意运行模式下,因日志输出、sourcemap 以及未压缩源码等原因,性能和包体积,均不及发行模式。 +17:07:47.045 编译器版本:5.06(uni-app x) +17:07:47.047 正在编译中... +17:07:47.048 编译会生成大量临时文件,杀毒软件监控时会影响编译速度,并造成CPU升高。推荐把项目目录添加到杀毒软件的监控排除名单中。[添加] [帮助] +17:07:47.049 uts插件[sp2-bluetooth]文件未发生变化,跳过编译 +17:07:47.050 ​提示:uts插件[sp2-bluetooth]依赖的原生配置或三方SDK在运行至标准基座时不能生效,如需正常调用请使用自定义基座​ +17:07:47.051 检测到编译缓存部分失效,开始差量编译。详见:https://uniapp.dcloud.net.cn/uni-app-x/compiler/#cache +17:07:47.052 当前工程2个页面,正在编译为android class,此过程耗时较长. 17:07:47.054 当前工程2个页面,正在编译为android class,此过程耗时较长.. 17:07:47.055 当前工程2个页面,正在编译为android class,此过程耗时较长... 17:07:47.056 当前工程2个页面,正在编译为android class,此过程耗时较长. 17:07:47.057 当前工程2个页面,正在编译为android class,此过程耗时较长.. 17:07:47.058 当前工程2个页面,正在编译为android class,此过程耗时较长... 17:07:47.059 当前工程2个页面,正在编译为android class,此过程耗时较长. 17:07:47.060 当前工程2个页面,正在编译为android class,此过程耗时较长.. 17:07:47.060 当前工程2个页面,正在编译为android class,此过程耗时较长... 17:07:47.063 当前工程2个页面,正在编译为android class,此过程耗时较长. 17:07:47.065 当前工程2个页面,正在编译为android class,此过程耗时较长... +17:07:47.066 项目 app_sp2 UTS编译完毕。 +17:07:47.066 ready in 11771ms. +17:07:47.068 手机端调试基座版本号为5.06.14660,与本地版本相同,跳过更新 +17:07:47.069 正在建立手机连接... +17:07:47.069 正在同步手机端程序文件... +17:07:47.071 同步手机端程序文件成功 +17:07:47.072 正在启动uni-app x调试基座... +17:07:47.073 App Launch at App.uvue:7 +17:07:47.074 App Show at App.uvue:11 +17:07:47.075 应用启动到触发onLaunch耗时: 2278ms +17:07:47.075 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"13ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"130ms"}] +17:07:47.078 应用【app_sp2】已启动 +17:07:47.079 App Hide at App.uvue:15 +17:07:47.080 App Show at App.uvue:11 +17:07:47.083 App Hide at App.uvue:15 +17:07:47.084 App Show at App.uvue:11 +17:07:47.085 App Hide at App.uvue:15 +17:07:47.086 App Launch at App.uvue:7 +17:07:47.086 App Show at App.uvue:11 +17:07:47.088 应用启动到触发onLaunch耗时: 245ms +17:07:47.089 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"15ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"20ms"},{"跳转页面到onReady总耗时":"140ms"}] +17:07:47.089 开始差量编译... +17:07:47.091 正在同步手机端程序文件... +17:07:47.093 App Launch at App.uvue:7 +17:07:47.094 App Show at App.uvue:11 +17:07:47.096 应用启动到触发onLaunch耗时: 237ms +17:07:47.097 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"110ms"}] +17:07:47.098 项目 app_sp2 UTS编译完毕。 +17:07:47.099 正在同步手机端程序文件... +17:07:47.100 App Launch at App.uvue:7 +17:07:47.100 App Show at App.uvue:11 +17:07:47.102 应用启动到触发onLaunch耗时: 221ms +17:07:47.103 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"17ms"},{"跳转页面到onReady总耗时":"117ms"}] +17:07:47.105 App Hide at App.uvue:15 +17:07:47.105 App Launch at App.uvue:7 +17:07:47.106 App Show at App.uvue:11 +17:07:47.108 应用启动到触发onLaunch耗时: 304ms +17:07:47.109 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"10ms"},{"渲染":"1次","耗时":"17ms"},{"跳转页面到onReady总耗时":"122ms"}] +17:07:47.110 开始差量编译... +17:07:47.112 正在同步手机端程序文件... +17:07:47.113 App Launch at App.uvue:7 +17:07:47.114 App Show at App.uvue:11 +17:07:47.115 应用启动到触发onLaunch耗时: 213ms +17:07:47.116 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"10ms"},{"渲染":"1次","耗时":"17ms"},{"跳转页面到onReady总耗时":"120ms"}] +17:07:47.117 ​​warning: 'var value: ByteArray!' is deprecated. Deprecated in Java.​ +17:07:47.118 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:268:43 +17:07:47.119 266| +17:07:47.121 267| override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { +17:07:47.121 268| val data = characteristic.value +17:07:47.123 | ^ +17:07:47.123 269| if (data != null && data.isNotEmpty()) { +17:07:47.126 270| val hexString = data.joinToString("") { "%02X".format(it) } +17:07:47.129 ​​warning: 'var value: ByteArray!' is deprecated. Deprecated in Java.​ +17:07:47.129 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:522:32 +17:07:47.130 520| BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE +17:07:47.132 521| } +17:07:47.133 522| descriptor.value = enableValue +17:07:47.134 | ^ +17:07:47.134 523| val writeResult = gatt.writeDescriptor(descriptor) +17:07:47.136 524| Log.i(TAG, "enableCharacteristicNotification: descriptor=${descriptor.uuid}, writeResult=$writeResult") +17:07:47.137 ​​warning: 'fun writeDescriptor(p0: BluetoothGattDescriptor!): Boolean' is deprecated. Deprecated in Java.​ +17:07:47.138 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:523:44 +17:07:47.139 521| } +17:07:47.141 522| descriptor.value = enableValue +17:07:47.142 523| val writeResult = gatt.writeDescriptor(descriptor) +17:07:47.143 | ^ +17:07:47.144 524| Log.i(TAG, "enableCharacteristicNotification: descriptor=${descriptor.uuid}, writeResult=$writeResult") +17:07:47.144 525| if (!writeResult) { +17:07:47.146 ​​warning: 'var value: ByteArray!' is deprecated. Deprecated in Java.​ +17:07:47.148 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:557:32 +17:07:47.148 555| selectedWriteServiceUuid = service.uuid.toString() +17:07:47.149 556| selectedWriteCharacteristicUuid = characteristic.uuid.toString() +17:07:47.150 557| characteristic.value = data.toByteArray(Charsets.UTF_8) +17:07:47.152 | ^ +17:07:47.152 558| val writeResult = gatt.writeCharacteristic(characteristic) +17:07:47.153 559| Log.i(TAG, "writeBluetoothData: service=${service.uuid}, characteristic=${characteristic.uuid}, payload=$data, writeResult=$writeResult") +17:07:47.156 ​​warning: 'fun writeCharacteristic(p0: BluetoothGattCharacteristic!): Boolean' is deprecated. Deprecated in Java.​ +17:07:47.158 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:558:40 +17:07:47.159 556| selectedWriteCharacteristicUuid = characteristic.uuid.toString() +17:07:47.161 557| characteristic.value = data.toByteArray(Charsets.UTF_8) +17:07:47.162 558| val writeResult = gatt.writeCharacteristic(characteristic) +17:07:47.162 | ^ +17:07:47.164 559| Log.i(TAG, "writeBluetoothData: service=${service.uuid}, characteristic=${characteristic.uuid}, payload=$data, writeResult=$writeResult") +17:07:47.165 560| } else { +17:07:47.166 ​提示:uts插件[sp2-bluetooth]依赖的原生配置或三方SDK在运行至标准基座时不能生效,如需正常调用请使用自定义基座​ +17:07:47.167 项目 app_sp2 UTS编译完毕。 +17:07:47.168 正在同步手机端程序文件... +17:07:47.169 App Launch at App.uvue:7 +17:07:47.169 App Show at App.uvue:11 +17:07:47.172 应用启动到触发onLaunch耗时: 211ms +17:07:47.173 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"12ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"117ms"}] +17:07:47.174 进入页面:​/pages/data/data?deviceId=C6:91:6A:18:74:6A&deviceName=NB-C6916A18746A​ 。[{"创建dom元素个数":"26个","耗时":"17ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"152ms"}] +17:07:47.175 App Hide at App.uvue:15 +17:07:47.175 App Launch at App.uvue:7 +17:07:47.177 App Show at App.uvue:11 +17:07:47.178 应用启动到触发onLaunch耗时: 217ms +17:07:47.179 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"123ms"}] +17:07:47.179 进入页面:​/pages/data/data?deviceId=C6:91:6A:18:74:6A&deviceName=NB-C6916A18746A​ 。[{"创建dom元素个数":"26个","耗时":"17ms"},{"排版":"1次","耗时":"13ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"166ms"}] + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..8793fef --- /dev/null +++ b/src/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/src/launch-android.log b/src/launch-android.log new file mode 100644 index 0000000..cddd35a --- /dev/null +++ b/src/launch-android.log @@ -0,0 +1,119 @@ +16:54:58.262 HBuilderX Version: 5.06 +16:54:58.591 项目 app_sp2 开始编译 +16:54:59.968 请注意运行模式下,因日志输出、sourcemap 以及未压缩源码等原因,性能和包体积,均不及发行模式。 +16:54:59.969 编译器版本:5.06(uni-app x) +16:54:59.970 正在编译中... +16:54:59.972 编译会生成大量临时文件,杀毒软件监控时会影响编译速度,并造成CPU升高。推荐把项目目录添加到杀毒软件的监控排除名单中。[添加] [帮助] +16:55:04.443 uts插件[sp2-bluetooth]文件未发生变化,跳过编译 +16:55:04.445 ​提示:uts插件[sp2-bluetooth]依赖的原生配置或三方SDK在运行至标准基座时不能生效,如需正常调用请使用自定义基座​ +16:55:05.586 检测到编译缓存部分失效,开始差量编译。详见:https://uniapp.dcloud.net.cn/uni-app-x/compiler/#cache +16:55:06.134 当前工程2个页面,正在编译为android class,此过程耗时较长. 16:55:06.634 当前工程2个页面,正在编译为android class,此过程耗时较长.. 16:55:07.148 当前工程2个页面,正在编译为android class,此过程耗时较长... 16:55:07.649 当前工程2个页面,正在编译为android class,此过程耗时较长. 16:55:08.164 当前工程2个页面,正在编译为android class,此过程耗时较长.. 16:55:08.673 当前工程2个页面,正在编译为android class,此过程耗时较长... 16:55:09.186 当前工程2个页面,正在编译为android class,此过程耗时较长. 16:55:09.689 当前工程2个页面,正在编译为android class,此过程耗时较长.. 16:55:10.206 当前工程2个页面,正在编译为android class,此过程耗时较长... 16:55:10.720 当前工程2个页面,正在编译为android class,此过程耗时较长. 16:55:10.818 当前工程2个页面,正在编译为android class,此过程耗时较长... +16:55:10.825 项目 app_sp2 UTS编译完毕。 +16:55:10.840 ready in 11771ms. +16:55:11.510 手机端调试基座版本号为5.06.14660,与本地版本相同,跳过更新 +16:55:11.948 正在建立手机连接... +16:55:12.987 正在同步手机端程序文件... +16:55:13.144 同步手机端程序文件成功 +16:55:14.213 正在启动uni-app x调试基座... +16:55:14.413 App Launch at App.uvue:7 +16:55:14.416 App Show at App.uvue:11 +16:55:14.513 应用启动到触发onLaunch耗时: 2278ms +16:55:14.515 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"13ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"130ms"}] +16:55:15.257 应用【app_sp2】已启动 +16:58:39.489 App Hide at App.uvue:15 +16:59:01.747 App Show at App.uvue:11 +16:59:08.193 App Hide at App.uvue:15 +16:59:47.677 App Show at App.uvue:11 +17:00:06.599 App Hide at App.uvue:15 +17:00:29.099 App Launch at App.uvue:7 +17:00:29.100 App Show at App.uvue:11 +17:00:29.126 应用启动到触发onLaunch耗时: 245ms +17:00:29.128 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"15ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"20ms"},{"跳转页面到onReady总耗时":"140ms"}] +17:02:09.012 开始差量编译... +17:02:11.404 正在同步手机端程序文件... +17:02:12.134 App Launch at App.uvue:7 +17:02:12.137 App Show at App.uvue:11 +17:02:12.141 应用启动到触发onLaunch耗时: 237ms +17:02:12.154 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"110ms"}] +17:02:13.077 项目 app_sp2 UTS编译完毕。 +17:02:13.084 正在同步手机端程序文件... +17:02:13.790 App Launch at App.uvue:7 +17:02:13.793 App Show at App.uvue:11 +17:02:13.807 应用启动到触发onLaunch耗时: 221ms +17:02:13.809 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"17ms"},{"跳转页面到onReady总耗时":"117ms"}] +17:04:01.683 App Hide at App.uvue:15 +17:04:03.924 App Launch at App.uvue:7 +17:04:03.926 App Show at App.uvue:11 +17:04:03.955 应用启动到触发onLaunch耗时: 304ms +17:04:03.956 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"10ms"},{"渲染":"1次","耗时":"17ms"},{"跳转页面到onReady总耗时":"122ms"}] +17:05:54.863 开始差量编译... +17:05:57.926 正在同步手机端程序文件... +17:05:58.667 App Launch at App.uvue:7 +17:05:58.670 App Show at App.uvue:11 +17:05:58.682 应用启动到触发onLaunch耗时: 213ms +17:05:58.684 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"10ms"},{"渲染":"1次","耗时":"17ms"},{"跳转页面到onReady总耗时":"120ms"}] +17:06:01.307 ​​warning: 'var value: ByteArray!' is deprecated. Deprecated in Java.​ +17:06:01.312 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:268:43 +17:06:01.315 266| +17:06:01.317 267| override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { +17:06:01.322 268| val data = characteristic.value +17:06:01.325 | ^ +17:06:01.327 269| if (data != null && data.isNotEmpty()) { +17:06:01.329 270| val hexString = data.joinToString("") { "%02X".format(it) } +17:06:01.332 ​​warning: 'var value: ByteArray!' is deprecated. Deprecated in Java.​ +17:06:01.335 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:522:32 +17:06:01.337 520| BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE +17:06:01.338 521| } +17:06:01.341 522| descriptor.value = enableValue +17:06:01.344 | ^ +17:06:01.345 523| val writeResult = gatt.writeDescriptor(descriptor) +17:06:01.347 524| Log.i(TAG, "enableCharacteristicNotification: descriptor=${descriptor.uuid}, writeResult=$writeResult") +17:06:01.350 ​​warning: 'fun writeDescriptor(p0: BluetoothGattDescriptor!): Boolean' is deprecated. Deprecated in Java.​ +17:06:01.352 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:523:44 +17:06:01.353 521| } +17:06:01.354 522| descriptor.value = enableValue +17:06:01.356 523| val writeResult = gatt.writeDescriptor(descriptor) +17:06:01.358 | ^ +17:06:01.360 524| Log.i(TAG, "enableCharacteristicNotification: descriptor=${descriptor.uuid}, writeResult=$writeResult") +17:06:01.360 525| if (!writeResult) { +17:06:01.363 ​​warning: 'var value: ByteArray!' is deprecated. Deprecated in Java.​ +17:06:01.366 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:557:32 +17:06:01.368 555| selectedWriteServiceUuid = service.uuid.toString() +17:06:01.370 556| selectedWriteCharacteristicUuid = characteristic.uuid.toString() +17:06:01.371 557| characteristic.value = data.toByteArray(Charsets.UTF_8) +17:06:01.373 | ^ +17:06:01.374 558| val writeResult = gatt.writeCharacteristic(characteristic) +17:06:01.376 559| Log.i(TAG, "writeBluetoothData: service=${service.uuid}, characteristic=${characteristic.uuid}, payload=$data, writeResult=$writeResult") +17:06:01.377 ​​warning: 'fun writeCharacteristic(p0: BluetoothGattCharacteristic!): Boolean' is deprecated. Deprecated in Java.​ +17:06:01.379 at uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt:558:40 +17:06:01.381 556| selectedWriteCharacteristicUuid = characteristic.uuid.toString() +17:06:01.382 557| characteristic.value = data.toByteArray(Charsets.UTF_8) +17:06:01.384 558| val writeResult = gatt.writeCharacteristic(characteristic) +17:06:01.385 | ^ +17:06:01.388 559| Log.i(TAG, "writeBluetoothData: service=${service.uuid}, characteristic=${characteristic.uuid}, payload=$data, writeResult=$writeResult") +17:06:01.389 560| } else { +17:06:02.190 ​提示:uts插件[sp2-bluetooth]依赖的原生配置或三方SDK在运行至标准基座时不能生效,如需正常调用请使用自定义基座​ +17:06:03.302 项目 app_sp2 UTS编译完毕。 +17:06:03.309 正在同步手机端程序文件... +17:06:03.973 App Launch at App.uvue:7 +17:06:03.974 App Show at App.uvue:11 +17:06:03.986 应用启动到触发onLaunch耗时: 211ms +17:06:04.001 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"12ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"117ms"}] +17:06:23.305 进入页面:​/pages/data/data?deviceId=C6:91:6A:18:74:6A&deviceName=NB-C6916A18746A​ 。[{"创建dom元素个数":"26个","耗时":"17ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"152ms"}] +17:07:08.011 App Hide at App.uvue:15 +17:07:10.200 App Launch at App.uvue:7 +17:07:10.201 App Show at App.uvue:11 +17:07:10.225 应用启动到触发onLaunch耗时: 217ms +17:07:10.228 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"14ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"123ms"}] +17:07:14.858 进入页面:​/pages/data/data?deviceId=C6:91:6A:18:74:6A&deviceName=NB-C6916A18746A​ 。[{"创建dom元素个数":"26个","耗时":"17ms"},{"排版":"1次","耗时":"13ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"166ms"}] +17:09:52.869 App Hide at App.uvue:15 +17:10:10.104 App Launch at App.uvue:7 +17:10:10.105 App Show at App.uvue:11 +17:10:10.122 应用启动到触发onLaunch耗时: 226ms +17:10:10.123 进入页面:​/pages/index/index​ 。[{"创建dom元素个数":"15个","耗时":"13ms"},{"排版":"1次","耗时":"11ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"119ms"}] +17:10:13.312 进入页面:​/pages/data/data?deviceId=C6:91:6A:18:74:6A&deviceName=NB-C6916A18746A​ 。[{"创建dom元素个数":"26个","耗时":"17ms"},{"排版":"1次","耗时":"12ms"},{"渲染":"1次","耗时":"18ms"},{"跳转页面到onReady总耗时":"149ms"}] +17:11:12.674 App Hide at App.uvue:15 +17:15:57.583 App Show at App.uvue:11 +17:17:41.191 App Hide at App.uvue:15 +17:26:19.684 已停止运行... + diff --git a/src/main.uts b/src/main.uts new file mode 100644 index 0000000..8bdcc86 --- /dev/null +++ b/src/main.uts @@ -0,0 +1,9 @@ +import App from './App.uvue' + +import { createSSRApp } from 'vue' +export function createApp() { + const app = createSSRApp(App) + return { + app + } +} \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..a7b38f7 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,64 @@ +{ + "name": "app_sp2", + "appid": "__UNI__D50239E", + "description": "", + "versionName": "1.0.0", + "versionCode": "100", + "uni-app-x": {}, + /* 快应用特有相关 */ + "quickapp": {}, + /* 小程序特有相关 */ + "mp-weixin": { + "appid": "", + "setting": { + "urlCheck": false + }, + "usingComponents": true + }, + "mp-alipay": { + "usingComponents": true + }, + "mp-baidu": { + "usingComponents": true + }, + "mp-toutiao": { + "usingComponents": true + }, + "uniStatistics": { + "enable": false + }, + "vueVersion": "3", + "app": { + "distribute": { + "icons": { + "android": { + "hdpi": "", + "xhdpi": "", + "xxhdpi": "", + "xxxhdpi": "" + } + } + } + }, + "app-android": { + "distribute": { + "modules": {}, + "icons": { + "hdpi": "", + "xhdpi": "", + "xxhdpi": "", + "xxxhdpi": "" + }, + "splashScreens": { + "default": {} + } + } + }, + "app-ios": { + "distribute": { + "modules": {}, + "icons": {}, + "splashScreens": {} + } + } +} \ No newline at end of file diff --git a/src/pages.json b/src/pages.json new file mode 100644 index 0000000..9783d81 --- /dev/null +++ b/src/pages.json @@ -0,0 +1,23 @@ +{ + "pages": [ + { + "path": "pages/index/index", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/data/data", + "style": { + "navigationBarTitleText": "设备数据" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "uni-app x", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + }, + "uniIdRouter": {} +} diff --git a/src/pages/data/data.uvue b/src/pages/data/data.uvue new file mode 100644 index 0000000..0be921f --- /dev/null +++ b/src/pages/data/data.uvue @@ -0,0 +1,468 @@ + + + + + diff --git a/src/pages/index/index.uvue b/src/pages/index/index.uvue new file mode 100644 index 0000000..f03c974 --- /dev/null +++ b/src/pages/index/index.uvue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/platformConfig.json b/src/platformConfig.json new file mode 100644 index 0000000..896f669 --- /dev/null +++ b/src/platformConfig.json @@ -0,0 +1,7 @@ + +// 参考链接 https://doc.dcloud.net.cn/uni-app-x/tutorial/ls-plugin.html#setting +{ + "targets": [ + "APP-ANDROID" + ] +} \ No newline at end of file diff --git a/src/static/logo.png b/src/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5771e209bb677e2ebd5ff766ad5ee11790f305a GIT binary patch literal 4023 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 SYs}WS&%*SCNx89m_ + + + + + + + + + + + diff --git a/src/uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt b/src/uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt new file mode 100644 index 0000000..b0c4427 --- /dev/null +++ b/src/uni_modules/sp2-bluetooth/utssdk/app-android/hybrid.kt @@ -0,0 +1,972 @@ +package uts.sdk.modules.sp2Bluetooth + +import android.Manifest +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCallback +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothProfile +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.Context +import android.content.pm.PackageManager +import android.location.LocationManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import io.dcloud.uts.UTSAndroid +import java.util.ArrayList +import java.util.Collections +import java.util.HashSet + +object BluetoothNative { + + private const val TAG = "sp2-bluetooth" + + private val mainHandler = Handler(Looper.getMainLooper()) + + private var scanCallback: ScanCallback? = null + private var bluetoothGatt: BluetoothGatt? = null + + private var onDeviceFoundCallback: ((String, String) -> Unit)? = null + private var onScanErrorCallback: ((String) -> Unit)? = null + private var onConnectSuccessCallback: (() -> Unit)? = null + private var onConnectErrorCallback: ((String) -> Unit)? = null + private var onDataReceivedCallback: ((String, String) -> Unit)? = null + private var onDebugMessageCallback: ((String) -> Unit)? = null + private var lastDebugMessage = "未产生蓝牙调试状态" + private var notifyEventCount = 0 + private var readEventCount = 0 + private var pendingNotifyDescriptorWrites = 0 + private var hasNotifyDescriptorSuccess = false + private var readCursor = 0 + private var mtuRequestDone = false + private val readableTargets = Collections.synchronizedList(ArrayList>()) + private val pendingDebugMessages = Collections.synchronizedList(ArrayList()) + private var selectedNotifyServiceUuid: String? = null + private var selectedNotifyCharacteristicUuid: String? = null + private var selectedWriteServiceUuid: String? = null + private var selectedWriteCharacteristicUuid: String? = null + private val pendingDataPackets = Collections.synchronizedList(ArrayList>()) + + private var hasConnectedOnce = false + private val discoveredDeviceIds = Collections.synchronizedSet(HashSet()) + + private val SERVICE_UUID = "0000FFF0-0000-1000-8000-00805F9B34FB" + private val READ_CHARACTERISTIC_UUID = "0000FFF1-0000-1000-8000-00805F9B34FB" + private val WRITE_CHARACTERISTIC_UUID = "0000FFF2-0000-1000-8000-00805F9B34FB" + private const val CLIENT_CHARACTERISTIC_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" + private const val TARGET_MTU = 250 + fun startBluetoothScan( + onDeviceFound: (deviceId: String, name: String) -> Unit, + onError: (message: String) -> Unit + ) { + debug("开始扫描蓝牙设备") + Log.i(TAG, "startBluetoothScan: begin") + val context = UTSAndroid.getAppContext() + if (context == null) { + Log.e(TAG, "startBluetoothScan: context is null") + postScanError(onError, "无法获取 Android Context") + return + } + + val adapter = getBluetoothAdapter(context) + if (adapter == null) { + Log.e(TAG, "startBluetoothScan: bluetooth adapter unavailable") + postScanError(onError, "当前设备不支持蓝牙") + return + } + + if (!adapter.isEnabled) { + Log.e(TAG, "startBluetoothScan: bluetooth disabled") + postScanError(onError, "蓝牙未开启") + return + } + + val scanCheck = checkScanEnvironment(context) + if (scanCheck != null) { + Log.e(TAG, "startBluetoothScan: environment check failed -> $scanCheck") + postScanError(onError, scanCheck) + return + } + + stopBluetoothScan() + + val scanner = adapter.bluetoothLeScanner + if (scanner == null) { + Log.e(TAG, "startBluetoothScan: scanner is null") + postScanError(onError, "无法获取 BLE Scanner") + return + } + + discoveredDeviceIds.clear() + onDeviceFoundCallback = onDeviceFound + onScanErrorCallback = onError + + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + + scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + val device = result.device ?: return + val deviceId = device.address ?: return + val isNewDevice = discoveredDeviceIds.add(deviceId) + if (!isNewDevice) { + return + } + + val name = resolveDeviceName(result) + Log.i(TAG, "onScanResult: deviceId=$deviceId, name=$name") + postDeviceFound(deviceId, name) + } + + override fun onBatchScanResults(results: MutableList) { + for (result in results) { + onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result) + } + } + + override fun onScanFailed(errorCode: Int) { + Log.e(TAG, "onScanFailed: errorCode=$errorCode") + postScanError("BLE 扫描失败,errorCode=$errorCode") + } + } + + try { + scanner.startScan(null, settings, scanCallback) + Log.i(TAG, "startBluetoothScan: scanner started") + } catch (e: SecurityException) { + Log.e(TAG, "startBluetoothScan: permission denied", e) + postScanError(onError, "BLE 扫描权限不足: ${e.message ?: "unknown"}") + } catch (e: Throwable) { + Log.e(TAG, "startBluetoothScan: start failed", e) + postScanError(onError, "BLE 扫描启动失败: ${e.message ?: "unknown"}") + } + } + + fun stopBluetoothScan() { + Log.i(TAG, "stopBluetoothScan: stop requested") + val context = UTSAndroid.getAppContext() + val adapter = context?.let { getBluetoothAdapter(it) } + val scanner: BluetoothLeScanner? = adapter?.bluetoothLeScanner + + try { + if (scanner != null && scanCallback != null) { + scanner.stopScan(scanCallback) + } + } catch (_: Throwable) { + } finally { + scanCallback = null + onDeviceFoundCallback = null + onScanErrorCallback = null + discoveredDeviceIds.clear() + } + } + + fun connectBluetoothDevice( + deviceId: String, + onSuccess: () -> Unit, + onError: (message: String) -> Unit + ) { + debug("开始连接设备: $deviceId") + Log.i(TAG, "connectBluetoothDevice: deviceId=$deviceId") + val context = UTSAndroid.getAppContext() + if (context == null) { + Log.e(TAG, "connectBluetoothDevice: context is null") + postConnectError(onError, "无法获取 Android Context") + return + } + + val adapter = getBluetoothAdapter(context) + if (adapter == null) { + Log.e(TAG, "connectBluetoothDevice: bluetooth adapter unavailable") + postConnectError(onError, "当前设备不支持蓝牙") + return + } + + if (!adapter.isEnabled) { + Log.e(TAG, "connectBluetoothDevice: bluetooth disabled") + postConnectError(onError, "蓝牙未开启") + return + } + + val connectCheck = checkConnectEnvironment(context) + if (connectCheck != null) { + Log.e(TAG, "connectBluetoothDevice: environment check failed -> $connectCheck") + postConnectError(onError, connectCheck) + return + } + + val device = try { + adapter.getRemoteDevice(deviceId) + } catch (_: IllegalArgumentException) { + null + } + + if (device == null) { + Log.e(TAG, "connectBluetoothDevice: invalid deviceId=$deviceId") + postConnectError(onError, "无效的设备地址: $deviceId") + return + } + + disconnectBluetoothDevice() + + hasConnectedOnce = false + notifyEventCount = 0 + readEventCount = 0 + pendingNotifyDescriptorWrites = 0 + hasNotifyDescriptorSuccess = false + readCursor = 0 + mtuRequestDone = false + readableTargets.clear() + onConnectSuccessCallback = onSuccess + onConnectErrorCallback = onError + + val callback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + debug("连接状态变化: status=$status, newState=$newState") + Log.i(TAG, "onConnectionStateChange: status=$status, newState=$newState, address=${gatt.device?.address}") + when { + status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { + hasConnectedOnce = true + bluetoothGatt = gatt + requestPreferredMtu(gatt) + try { + Log.i(TAG, "onConnectionStateChange: discoverServices") + gatt.discoverServices() + } catch (e: Throwable) { + Log.e(TAG, "onConnectionStateChange: discoverServices failed", e) + postConnectError("服务发现启动失败: ${e.message ?: "unknown"}") + } + } + + newState == BluetoothProfile.STATE_DISCONNECTED -> { + val needCallback = !hasConnectedOnce + Log.w(TAG, "onConnectionStateChange: disconnected, needCallback=$needCallback") + safeCloseGatt(gatt) + bluetoothGatt = null + + if (needCallback) { + postConnectError("BLE 连接失败,status=$status") + } else { + clearConnectCallbacks() + } + } + + status != BluetoothGatt.GATT_SUCCESS -> { + Log.e(TAG, "onConnectionStateChange: gatt error status=$status") + safeCloseGatt(gatt) + bluetoothGatt = null + postConnectError("BLE 连接失败,status=$status") + } + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + debug("服务发现完成: status=$status, count=${gatt.services?.size ?: 0}") + Log.i(TAG, "onServicesDiscovered: status=$status, serviceCount=${gatt.services?.size ?: 0}") + if (status == BluetoothGatt.GATT_SUCCESS) { + gatt.services?.forEach { service -> + debug("服务: ${service.uuid}") + Log.i(TAG, "service: uuid=${service.uuid}, characteristicCount=${service.characteristics?.size ?: 0}") + service.characteristics?.forEach { characteristic -> + debug("特征: ${characteristic.uuid}, properties=${characteristic.properties}") + Log.i(TAG, "characteristic: uuid=${characteristic.uuid}, properties=${characteristic.properties}") + } + } + cacheReadableTargets(gatt) + enableCharacteristicNotification(gatt) + } else { + Log.e(TAG, "onServicesDiscovered: failed status=$status") + } + } + + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + notifyEventCount += 1 + val data = characteristic.value + debug("notify回调#${notifyEventCount}: uuid=${characteristic.uuid}, size=${data?.size ?: 0}") + if (data != null && data.isNotEmpty()) { + val hexString = data.joinToString("") { "%02X".format(it) } + val stringData = String(data, Charsets.UTF_8) + debug("收到 notify#${notifyEventCount}: uuid=${characteristic.uuid}, hex=$hexString") + Log.i(TAG, "onCharacteristicChanged: uuid=${characteristic.uuid}, hex=$hexString, text=$stringData") + postDataReceived(stringData, hexString) + } else { + debug("收到 notify#${notifyEventCount} 但数据为空: ${characteristic.uuid}") + Log.w(TAG, "onCharacteristicChanged: empty payload, uuid=${characteristic.uuid}") + } + } + + override fun onDescriptorWrite( + gatt: BluetoothGatt, + descriptor: BluetoothGattDescriptor, + status: Int + ) { + debug("通知描述符写入: status=$status") + Log.i(TAG, "onDescriptorWrite: uuid=${descriptor.uuid}, status=$status") + if (pendingNotifyDescriptorWrites > 0) { + pendingNotifyDescriptorWrites -= 1 + } + if (status == BluetoothGatt.GATT_SUCCESS) { + hasNotifyDescriptorSuccess = true + } + + if (pendingNotifyDescriptorWrites == 0) { + if (hasNotifyDescriptorSuccess) { + postConnectSuccess() + } else { + postConnectError("启用蓝牙通知失败,status=$status") + } + } + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + debug("写入特征完成: status=$status") + Log.i(TAG, "onCharacteristicWrite: uuid=${characteristic.uuid}, status=$status") + } + + override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + debug("MTU 设置结果: status=$status, mtu=$mtu") + Log.i(TAG, "onMtuChanged: status=$status, mtu=$mtu") + } + + override fun onCharacteristicRead( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + status: Int + ) { + readEventCount += 1 + debug("读取回调#${readEventCount}: status=$status, uuid=${characteristic.uuid}") + Log.i(TAG, "onCharacteristicRead: uuid=${characteristic.uuid}, status=$status") + if (status == BluetoothGatt.GATT_SUCCESS) { + val data = characteristic.value + if (data != null && data.isNotEmpty()) { + val hexString = data.joinToString("") { "%02X".format(it) } + val stringData = String(data, Charsets.UTF_8) + debug("读取到数据#${readEventCount}: uuid=${characteristic.uuid}, hex=$hexString") + postDataReceived(stringData, hexString) + } else { + debug("读取成功但数据为空#${readEventCount}: ${characteristic.uuid}") + } + } else { + debug("读取失败#${readEventCount}: status=$status, uuid=${characteristic.uuid}") + } + } + + } + + try { + bluetoothGatt = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + device.connectGatt(context, false, callback, BluetoothDevice.TRANSPORT_LE) + } else { + device.connectGatt(context, false, callback) + } + Log.i(TAG, "connectBluetoothDevice: connectGatt invoked") + } catch (e: SecurityException) { + Log.e(TAG, "connectBluetoothDevice: permission denied", e) + postConnectError(onError, "BLE 连接权限不足: ${e.message ?: "unknown"}") + } catch (e: Throwable) { + Log.e(TAG, "connectBluetoothDevice: start failed", e) + postConnectError(onError, "BLE 连接启动失败: ${e.message ?: "unknown"}") + } + } + + fun disconnectBluetoothDevice() { + Log.i(TAG, "disconnectBluetoothDevice: disconnect requested") + try { + bluetoothGatt?.disconnect() + } catch (_: Throwable) { + } + + try { + bluetoothGatt?.close() + } catch (_: Throwable) { + } + + bluetoothGatt = null + hasConnectedOnce = false + notifyEventCount = 0 + readEventCount = 0 + pendingNotifyDescriptorWrites = 0 + hasNotifyDescriptorSuccess = false + readCursor = 0 + mtuRequestDone = false + readableTargets.clear() + selectedNotifyServiceUuid = null + selectedNotifyCharacteristicUuid = null + selectedWriteServiceUuid = null + selectedWriteCharacteristicUuid = null + pendingDataPackets.clear() + pendingDebugMessages.clear() + clearConnectCallbacks() + } + + private fun getBluetoothAdapter(context: Context): BluetoothAdapter? { + val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager + return manager?.adapter + } + + private fun checkScanEnvironment(context: Context): String? { + val missingPermissions = getMissingScanPermissions(context) + if (missingPermissions.isNotEmpty()) { + return "缺少扫描权限: ${missingPermissions.joinToString(",")}" + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && !isLocationEnabled(context)) { + return "Android 12 以下扫描 BLE 需要开启系统定位服务" + } + + return null + } + + private fun checkConnectEnvironment(context: Context): String? { + val missingPermissions = getMissingConnectPermissions(context) + if (missingPermissions.isNotEmpty()) { + return "缺少连接权限: ${missingPermissions.joinToString(",")}" + } + return null + } + + private fun getMissingScanPermissions(context: Context): List { + val permissions = mutableListOf() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + permissions.add(Manifest.permission.ACCESS_FINE_LOCATION) + } + + return permissions.filter { !hasPermission(context, it) } + } + + private fun getMissingConnectPermissions(context: Context): List { + val permissions = mutableListOf() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + } + + return permissions.filter { !hasPermission(context, it) } + } + + private fun hasPermission(context: Context, permission: String): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + + private fun isLocationEnabled(context: Context): Boolean { + val locationManager = + context.getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return false + + return try { + locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } catch (_: Throwable) { + false + } + } + + @SuppressLint("MissingPermission") + private fun resolveDeviceName(result: ScanResult): String { + return try { + result.device?.name + ?: result.scanRecord?.deviceName + ?: "" + } catch (_: SecurityException) { + result.scanRecord?.deviceName ?: "" + } catch (_: Throwable) { + "" + } + } + + private fun safeCloseGatt(gatt: BluetoothGatt?) { + Log.i(TAG, "safeCloseGatt: address=${gatt?.device?.address}") + try { + gatt?.disconnect() + } catch (_: Throwable) { + } + + try { + gatt?.close() + } catch (_: Throwable) { + } + } + + private fun clearConnectCallbacks() { + Log.i(TAG, "clearConnectCallbacks") + onConnectSuccessCallback = null + onConnectErrorCallback = null + } + + private fun postDeviceFound(deviceId: String, name: String) { + val callback = onDeviceFoundCallback ?: return + mainHandler.post { + callback(deviceId, name) + } + } + + private fun postScanError(message: String) { + val callback = onScanErrorCallback ?: return + mainHandler.post { + Log.e(TAG, "postScanError: $message") + callback(message) + } + } + + private fun postScanError(callback: (String) -> Unit, message: String) { + mainHandler.post { + Log.e(TAG, "postScanError: $message") + callback(message) + } + } + + private fun postConnectSuccess() { + val callback = onConnectSuccessCallback ?: return + mainHandler.post { + debug("设备连接完成,通知已开启") + Log.i(TAG, "postConnectSuccess") + callback() + } + } + + private fun postConnectError(message: String) { + val callback = onConnectErrorCallback ?: return + mainHandler.post { + Log.e(TAG, "postConnectError: $message") + callback(message) + clearConnectCallbacks() + } + } + + private fun postConnectError(callback: (String) -> Unit, message: String) { + mainHandler.post { + Log.e(TAG, "postConnectError: $message") + callback(message) + } + } + + private fun enableCharacteristicNotification(gatt: BluetoothGatt) { + try { + val notifyTargets = findNotifyTargets(gatt) + if (notifyTargets.isNotEmpty()) { + val firstTarget = notifyTargets[0] + selectedNotifyServiceUuid = firstTarget.first.uuid.toString() + selectedNotifyCharacteristicUuid = firstTarget.second.uuid.toString() + debug("已找到通知特征数量: ${notifyTargets.size}") + debug("当前订阅服务: ${selectedNotifyServiceUuid ?: ""}") + debug("当前订阅特征: ${selectedNotifyCharacteristicUuid ?: ""}") + + val writeTarget = findWriteTarget(gatt) + if (writeTarget != null) { + selectedWriteServiceUuid = writeTarget.first.uuid.toString() + selectedWriteCharacteristicUuid = writeTarget.second.uuid.toString() + debug("选择写入服务: ${writeTarget.first.uuid}") + debug("选择写入特征: ${writeTarget.second.uuid}") + Log.i(TAG, "enableCharacteristicNotification: selected write service=${writeTarget.first.uuid}, characteristic=${writeTarget.second.uuid}") + } else { + selectedWriteServiceUuid = selectedNotifyServiceUuid + selectedWriteCharacteristicUuid = selectedNotifyCharacteristicUuid + debug("未找到独立写入特征,回退为首个通知特征") + Log.w(TAG, "enableCharacteristicNotification: write characteristic not found, fallback to notify characteristic") + } + + pendingNotifyDescriptorWrites = 0 + hasNotifyDescriptorSuccess = false + + for (target in notifyTargets) { + val service = target.first + val characteristic = target.second + debug("订阅通知服务: ${service.uuid}") + debug("订阅通知特征: ${characteristic.uuid}") + Log.i(TAG, "enableCharacteristicNotification: subscribe service=${service.uuid}, characteristic=${characteristic.uuid}") + + val notifyResult = gatt.setCharacteristicNotification(characteristic, true) + debug("开启通知结果: $notifyResult") + Log.i(TAG, "enableCharacteristicNotification: characteristic=${characteristic.uuid}, notifyResult=$notifyResult") + if (!notifyResult) { + continue + } + + val descriptor = characteristic.getDescriptor(java.util.UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG_UUID)) + if (descriptor == null) { + Log.e(TAG, "enableCharacteristicNotification: CCCD descriptor missing -> ${characteristic.uuid}") + continue + } + + val properties = characteristic.properties + val enableValue = if ((properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) { + BluetoothGattDescriptor.ENABLE_INDICATION_VALUE + } else { + BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + } + descriptor.value = enableValue + pendingNotifyDescriptorWrites += 1 + val writeResult = gatt.writeDescriptor(descriptor) + debug("写入通知描述符结果: $writeResult") + Log.i(TAG, "enableCharacteristicNotification: descriptor=${descriptor.uuid}, writeResult=$writeResult") + if (!writeResult) { + pendingNotifyDescriptorWrites -= 1 + } + } + + if (pendingNotifyDescriptorWrites == 0) { + postConnectError("没有成功开启任何通知特征") + } + } else { + Log.e(TAG, "enableCharacteristicNotification: no notify characteristic found") + postConnectError("未找到可接收数据的通知特征值") + } + } catch (e: Throwable) { + Log.e(TAG, "enableCharacteristicNotification: failed", e) + postConnectError("开启蓝牙通知失败: ${e.message ?: "unknown"}") + } + } + + fun setOnDataReceivedCallback(callback: (String, String) -> Unit) { + Log.i(TAG, "setOnDataReceivedCallback") + onDataReceivedCallback = callback + flushPendingDataPackets() + } + + fun setOnDebugMessageCallback(callback: (String) -> Unit) { + onDebugMessageCallback = callback + flushPendingDebugMessages() + } + + fun getDebugSnapshot(): String { + return lastDebugMessage + } + + fun readBluetoothDataOnce() { + val gatt = bluetoothGatt + if (gatt == null) { + debug("读取一次失败:设备未连接") + return + } + + if (readableTargets.isEmpty()) { + cacheReadableTargets(gatt) + } + + if (readableTargets.isEmpty()) { + debug("读取一次失败:未找到可读特征") + return + } + + val preferredTarget = findReadTarget(gatt) + if (preferredTarget != null) { + val characteristic = preferredTarget.second + try { + debug("开始手动读取 FFF1: ${characteristic.uuid}") + gatt.readCharacteristic(characteristic) + } catch (e: Throwable) { + Log.e(TAG, "readBluetoothDataOnce: readCharacteristic failed", e) + } + } + + if (readableTargets.isEmpty()) { + return + } + + if (readCursor >= readableTargets.size) { + readCursor = 0 + } + + val fallbackTarget = readableTargets[readCursor] + readCursor += 1 + debug("定位模式:额外读取候选特征 ${readCursor}/${readableTargets.size}") + val fallbackCharacteristic = fallbackTarget.second + try { + debug("开始手动读取候选特征: ${fallbackCharacteristic.uuid}") + gatt.readCharacteristic(fallbackCharacteristic) + } catch (e: Throwable) { + Log.e(TAG, "readBluetoothDataOnce: read fallback characteristic failed", e) + } + } + + fun refreshBluetoothNotification() { + val gatt = bluetoothGatt + if (gatt == null) { + debug("重订阅失败:设备未连接") + return + } + + debug("准备重新开启通知订阅") + enableCharacteristicNotification(gatt) + } + + fun writeBluetoothData(data: String) { + val gatt = bluetoothGatt ?: return + try { + val target = findWriteTarget(gatt) + if (target != null) { + val service = target.first + val characteristic = target.second + selectedWriteServiceUuid = service.uuid.toString() + selectedWriteCharacteristicUuid = characteristic.uuid.toString() + val normalizedData = data.replace("\r\n", "\n").replace("\n", "\r\n") + val payload = ByteArray(normalizedData.length) + for (index in normalizedData.indices) { + payload[index] = normalizedData[index].code.toByte() + } + characteristic.value = payload + if ((characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) { + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE + } else { + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + } + val writeResult = gatt.writeCharacteristic(characteristic) + val payloadHex = payload.joinToString("") { "%02X".format(it) } + debug("写入命令: ${normalizedData.replace("\r", "\\r").replace("\n", "\\n")} [$payloadHex]") + Log.i(TAG, "writeBluetoothData: service=${service.uuid}, characteristic=${characteristic.uuid}, payload=$normalizedData, payloadHex=$payloadHex, writeResult=$writeResult") + } else { + Log.e(TAG, "writeBluetoothData: no writable characteristic found") + } + } catch (e: Throwable) { + Log.e(TAG, "writeBluetoothData: failed", e) + } + } + + private fun findNotifyTargets(gatt: BluetoothGatt): List> { + val services = gatt.services ?: return emptyList() + val preferredServiceUuid = SERVICE_UUID.lowercase() + val preferredCharacteristicUuid = READ_CHARACTERISTIC_UUID.lowercase() + val preferredTargets = ArrayList>() + val fallbackTargets = ArrayList>() + + for (service in services) { + val serviceUuid = service.uuid.toString().lowercase() + for (characteristic in service.characteristics ?: emptyList()) { + val characteristicUuid = characteristic.uuid.toString().lowercase() + val properties = characteristic.properties + val supportsNotify = (properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 + val supportsIndicate = (properties and BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0 + if (!supportsNotify && !supportsIndicate) { + continue + } + + if (serviceUuid == preferredServiceUuid && characteristicUuid == preferredCharacteristicUuid) { + Log.i(TAG, "findNotifyTarget: matched preferred uuid") + preferredTargets.add(Pair(service, characteristic)) + } + + fallbackTargets.add(Pair(service, characteristic)) + } + } + + if (preferredTargets.isNotEmpty()) { + return preferredTargets + fallbackTargets.filter { fallback -> + preferredTargets.none { preferred -> + preferred.first.uuid == fallback.first.uuid && preferred.second.uuid == fallback.second.uuid + } + } + } + + return fallbackTargets + } + + private fun findWriteTarget(gatt: BluetoothGatt): Pair? { + val services = gatt.services ?: return null + val preferredServiceUuid = SERVICE_UUID.lowercase() + val preferredCharacteristicUuid = WRITE_CHARACTERISTIC_UUID.lowercase() + var fallbackTarget: Pair? = null + + for (service in services) { + val serviceUuid = service.uuid.toString().lowercase() + for (characteristic in service.characteristics ?: emptyList()) { + val characteristicUuid = characteristic.uuid.toString().lowercase() + val properties = characteristic.properties + val supportsWrite = (properties and BluetoothGattCharacteristic.PROPERTY_WRITE) != 0 || + (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0 + if (!supportsWrite) { + continue + } + + if (serviceUuid == preferredServiceUuid && characteristicUuid == preferredCharacteristicUuid) { + Log.i(TAG, "findWriteTarget: matched preferred uuid") + return Pair(service, characteristic) + } + + if (fallbackTarget == null) { + fallbackTarget = Pair(service, characteristic) + } + } + } + + if (fallbackTarget != null) { + Log.w(TAG, "findWriteTarget: preferred uuid missing, use fallback service=${fallbackTarget.first.uuid}, characteristic=${fallbackTarget.second.uuid}") + } + return fallbackTarget + } + + private fun findReadTarget(gatt: BluetoothGatt): Pair? { + val services = gatt.services ?: return null + val preferredServiceUuid = SERVICE_UUID.lowercase() + val preferredCharacteristicUuid = READ_CHARACTERISTIC_UUID.lowercase() + var fallbackTarget: Pair? = null + + for (service in services) { + val serviceUuid = service.uuid.toString().lowercase() + for (characteristic in service.characteristics ?: emptyList()) { + val characteristicUuid = characteristic.uuid.toString().lowercase() + val properties = characteristic.properties + val supportsRead = (properties and BluetoothGattCharacteristic.PROPERTY_READ) != 0 + if (!supportsRead) { + continue + } + + if (serviceUuid == preferredServiceUuid && characteristicUuid == preferredCharacteristicUuid) { + Log.i(TAG, "findReadTarget: matched preferred uuid") + return Pair(service, characteristic) + } + + if (fallbackTarget == null) { + fallbackTarget = Pair(service, characteristic) + } + } + } + + if (fallbackTarget != null) { + Log.w(TAG, "findReadTarget: preferred uuid missing, use fallback service=${fallbackTarget.first.uuid}, characteristic=${fallbackTarget.second.uuid}") + } + return fallbackTarget + } + + private fun cacheReadableTargets(gatt: BluetoothGatt) { + readableTargets.clear() + + val services = gatt.services ?: return + val preferredServiceUuid = SERVICE_UUID.lowercase() + val preferredCharacteristicUuid = READ_CHARACTERISTIC_UUID.lowercase() + val fallbackTargets = ArrayList>() + for (service in services) { + val serviceUuid = service.uuid.toString().lowercase() + for (characteristic in service.characteristics ?: emptyList()) { + val characteristicUuid = characteristic.uuid.toString().lowercase() + val properties = characteristic.properties + val supportsRead = (properties and BluetoothGattCharacteristic.PROPERTY_READ) != 0 + if (!supportsRead) { + continue + } + + val target = Pair(service, characteristic) + if (serviceUuid == preferredServiceUuid && characteristicUuid == preferredCharacteristicUuid) { + readableTargets.add(target) + } else { + fallbackTargets.add(target) + } + } + } + + readableTargets.addAll(fallbackTargets) + readCursor = 0 + + debug("已找到可读特征数量: ${readableTargets.size}") + } + + private fun requestPreferredMtu(gatt: BluetoothGatt) { + if (mtuRequestDone) { + return + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + debug("当前系统不支持设置 MTU") + return + } + + mtuRequestDone = true + try { + val requested = gatt.requestMtu(TARGET_MTU) + debug("请求 MTU 250: $requested") + } catch (e: Throwable) { + Log.e(TAG, "requestPreferredMtu: failed", e) + debug("请求 MTU 250 失败") + } + } + + private fun flushPendingDataPackets() { + val callback = onDataReceivedCallback ?: return + val packets = ArrayList>() + synchronized(pendingDataPackets) { + if (pendingDataPackets.isEmpty()) { + return + } + packets.addAll(pendingDataPackets) + pendingDataPackets.clear() + } + + mainHandler.post { + for (packet in packets) { + Log.i(TAG, "flushPendingDataPackets: hex=${packet.second}, text=${packet.first}") + callback(packet.first, packet.second) + } + } + } + + private fun postDataReceived(data: String, hex: String) { + val callback = onDataReceivedCallback + if (callback == null) { + synchronized(pendingDataPackets) { + pendingDataPackets.add(Pair(data, hex)) + } + Log.w(TAG, "postDataReceived: callback missing, packet cached") + return + } + + mainHandler.post { + debug("收到数据: $hex") + Log.i(TAG, "postDataReceived: hex=$hex, text=$data") + callback(data, hex) + } + } + + private fun debug(message: String) { + lastDebugMessage = message + val callback = onDebugMessageCallback + if (callback == null) { + synchronized(pendingDebugMessages) { + pendingDebugMessages.add(message) + } + return + } + + mainHandler.post { + callback(message) + } + } + + private fun flushPendingDebugMessages() { + val callback = onDebugMessageCallback ?: return + val messages = ArrayList() + synchronized(pendingDebugMessages) { + if (pendingDebugMessages.isEmpty()) { + return + } + messages.addAll(pendingDebugMessages) + pendingDebugMessages.clear() + } + + mainHandler.post { + for (message in messages) { + callback(message) + } + } + } +} diff --git a/src/uni_modules/sp2-bluetooth/utssdk/app-android/index.uts b/src/uni_modules/sp2-bluetooth/utssdk/app-android/index.uts new file mode 100644 index 0000000..29b85f1 --- /dev/null +++ b/src/uni_modules/sp2-bluetooth/utssdk/app-android/index.uts @@ -0,0 +1,101 @@ +import Build from 'android.os.Build' +import { BluetoothNative } from 'uts.sdk.modules.sp2Bluetooth' +import { type BluetoothDeviceItem, type BluetoothDeviceFoundCallback, type BluetoothErrorCallback, type BluetoothSuccessCallback, type BluetoothDataCallback, type BluetoothDebugCallback } from '../interface.uts' + +function getBluetoothPermissions(): string[] { + const permissions: string[] = [] + if (Build.VERSION.SDK_INT >= 31) { + permissions.push('android.permission.BLUETOOTH_SCAN') + permissions.push('android.permission.BLUETOOTH_CONNECT') + } else if (Build.VERSION.SDK_INT >= 23) { + permissions.push('android.permission.ACCESS_FINE_LOCATION') + } + return permissions +} + +function ensureBluetoothPermissions(onSuccess: BluetoothSuccessCallback, onError: BluetoothErrorCallback): void { + const activity = UTSAndroid.getUniActivity() + if (activity == null) { + onError('无法获取当前 Activity') + return + } + + const permissions = getBluetoothPermissions() + if (permissions.length === 0) { + onSuccess() + return + } + + UTSAndroid.requestSystemPermission(activity, permissions, (allRight: boolean, grantedList: string[]) => { + if (allRight || grantedList.length === permissions.length) { + onSuccess() + return + } + onError('蓝牙权限未完全授予') + }, (doNotAskAgain: boolean, grantedList: string[]) => { + if (doNotAskAgain) { + onError('蓝牙权限被永久拒绝,请到系统设置中开启') + return + } + if (grantedList.length > 0) { + onError('蓝牙权限未完全授予') + return + } + onError('蓝牙权限申请失败') + }) +} + +export function startBluetoothScan(onDeviceFound: BluetoothDeviceFoundCallback, onError: BluetoothErrorCallback): void { + ensureBluetoothPermissions(() => { + BluetoothNative.startBluetoothScan((deviceId: string, name: string) => { + const device: BluetoothDeviceItem = { + deviceId: deviceId, + name: name + } + onDeviceFound(device) + }, onError) + }, onError) +} + +export function stopBluetoothScan(): void { + BluetoothNative.stopBluetoothScan() +} + +export function connectBluetoothDevice(deviceId: string, onSuccess: BluetoothSuccessCallback, onError: BluetoothErrorCallback): void { + stopBluetoothScan() + disconnectBluetoothDevice() + ensureBluetoothPermissions(() => { + // 对齐 ecBLE.js 的“先清理旧连接,再建立新连接”思路,避免复用脏的 GATT 状态。 + setTimeout(() => { + BluetoothNative.connectBluetoothDevice(deviceId, onSuccess, onError) + }, 300) + }, onError) +} + +export function disconnectBluetoothDevice(): void { + BluetoothNative.disconnectBluetoothDevice() +} + +export function onBluetoothDataReceived(callback: BluetoothDataCallback): void { + BluetoothNative.setOnDataReceivedCallback(callback) +} + +export function onBluetoothDebugMessage(callback: BluetoothDebugCallback): void { + BluetoothNative.setOnDebugMessageCallback(callback) +} + +export function getBluetoothDebugSnapshot(): string { + return BluetoothNative.getDebugSnapshot() +} + +export function readBluetoothDataOnce(): void { + BluetoothNative.readBluetoothDataOnce() +} + +export function refreshBluetoothNotification(): void { + BluetoothNative.refreshBluetoothNotification() +} + +export function writeBluetoothData(data: string): void { + BluetoothNative.writeBluetoothData(data) +} diff --git a/src/uni_modules/sp2-bluetooth/utssdk/interface.uts b/src/uni_modules/sp2-bluetooth/utssdk/interface.uts new file mode 100644 index 0000000..e6df17f --- /dev/null +++ b/src/uni_modules/sp2-bluetooth/utssdk/interface.uts @@ -0,0 +1,21 @@ +export type BluetoothDeviceItem = { + deviceId: string, + name: string +} + +export type BluetoothDeviceFoundCallback = (device: BluetoothDeviceItem) => void +export type BluetoothErrorCallback = (message: string) => void +export type BluetoothSuccessCallback = () => void +export type BluetoothDataCallback = (data: string, hex: string) => void +export type BluetoothDebugCallback = (message: string) => void + +export declare function startBluetoothScan(onDeviceFound: BluetoothDeviceFoundCallback, onError: BluetoothErrorCallback): void +export declare function stopBluetoothScan(): void +export declare function connectBluetoothDevice(deviceId: string, onSuccess: BluetoothSuccessCallback, onError: BluetoothErrorCallback): void +export declare function disconnectBluetoothDevice(): void +export declare function onBluetoothDataReceived(callback: BluetoothDataCallback): void +export declare function onBluetoothDebugMessage(callback: BluetoothDebugCallback): void +export declare function getBluetoothDebugSnapshot(): string +export declare function readBluetoothDataOnce(): void +export declare function refreshBluetoothNotification(): void +export declare function writeBluetoothData(data: string): void diff --git a/src/utils/ecBLE.js b/src/utils/ecBLE.js new file mode 100644 index 0000000..7e493fe --- /dev/null +++ b/src/utils/ecBLE.js @@ -0,0 +1,388 @@ +const regeneratorRuntime = require('./regenerator/runtime.js') + +const logEnable = true + +let deviceList = [] + +let ecDeviceId = "" +let ecServerId = '' +let ecWriteCharacteristicId = '' +let ecReadCharacteristicId = '' + +let ecServerIdOption1 = "0000FFF0-0000-1000-8000-00805F9B34FB" +let ecServerIdOption2 = "FFF0" +let ecWriteCharacteristicIdOption1 = "0000FFF2-0000-1000-8000-00805F9B34FB" +let ecWriteCharacteristicIdOption2 = "FFF2" +let ecReadCharacteristicIdOption1 = "0000FFF1-0000-1000-8000-00805F9B34FB" +let ecReadCharacteristicIdOption2 = "FFF1" + +const log = (data) => { + if (logEnable) { + console.log(data) + } +} +const wait = (i) => { + return new Promise(function (resolve, reject) { + setTimeout(() => { + resolve() + }, i); + }) +} +const openBluetoothAdapter = () => { + return new Promise(function (resolve, reject) { + wx.openBluetoothAdapter({ + success(res) { + resolve({ ok: true, errCode: 0, errMsg: "" }) + }, + fail(res) { + log(res) + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} +const closeBluetoothAdapter = () => { + return new Promise(function (resolve, reject) { + wx.closeBluetoothAdapter({ + success(res) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} +const getBluetoothAdapterState = () => { + return new Promise(function (resolve, reject) { + wx.getBluetoothAdapterState({ + success(res) { + if (res.available) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + } else { + //蓝牙适配器不可用,打印失败信息 + log(res) + resolve({ ok: false, errCode: 20000, errMsg: '蓝牙适配器关闭' }) + } + }, + fail(res) { + //打印失败信息 + log(res) + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +const startBluetoothDevicesDiscovery = (cb) => { + deviceList = [] + wx.onBluetoothDeviceFound((res) => { + let name = res.devices[0].name ? res.devices[0].name : res.devices[0].localName + if (!name) { return } + // log(res) + for (const item of deviceList) { + if (item.name === name) { + item.rssi = res.devices[0].RSSI + cb(name, item.rssi) + return + } + } + deviceList.push({ name, rssi: res.devices[0].RSSI, deviceId: res.devices[0].deviceId }) + cb(name, res.devices[0].RSSI) + }) + //开始搜索 + wx.startBluetoothDevicesDiscovery({ + //services: [ecServerId], + allowDuplicatesKey: true, + success(res) { + }, + fail(res) { + } + }) +} + +//结束搜索 +const stopBluetoothDevicesDiscovery = () => { + return new Promise(function (resolve, reject) { + //停止扫描 + wx.stopBluetoothDevicesDiscovery({ + success(res) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +//和设备建立连接 +const createBLEConnection = (name) => { + return new Promise(function (resolve, reject) { + let isExist = false + for (const item of deviceList) { + if (item.name === name) { + isExist = true + ecDeviceId = item.deviceId + break + } + } + if (!isExist) { + resolve({ ok: false, errCode: -1, errMsg: "Name error,Device does not exist" }) + return + } + //开始连接 + wx.createBLEConnection({ + deviceId: ecDeviceId, + success(res) { + log(res) + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + //连接失败 + log("connect fail") + log(res) + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +//关闭当前连接 +const closeBLEConnection = () => { + return new Promise(function (resolve, reject) { + wx.closeBLEConnection({ + deviceId: ecDeviceId, + success(res) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +const onBLEConnectionStateChange = (cb) => { + wx.onBLEConnectionStateChange((res) => { + if (!res.connected) cb() + }) +} + +const getBLEDeviceServices = () => { + return new Promise(function (resolve, reject) { + wx.getBLEDeviceServices({ + deviceId: ecDeviceId, + success(res) { + log('device services:') + log(res.services) + for (let i = 0; i < res.services.length; i++) { + let uuid = '' + log(res.services[i].uuid) + uuid = res.services[i].uuid.toUpperCase() + if (uuid === ecServerIdOption1) { + ecServerId = ecServerIdOption1 + return resolve({ ok: true, errCode: 0, errMsg: '' }) + } + if (uuid === ecServerIdOption2) { + ecServerId = ecServerIdOption2 + return resolve({ ok: true, errCode: 0, errMsg: '' }) + } + } + resolve({ ok: false, errCode: 20000, errMsg: '服务未找到' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +//连接特性 +const getBLEDeviceCharacteristics = () => { + return new Promise(function (resolve, reject) { + wx.getBLEDeviceCharacteristics({ + deviceId: ecDeviceId, + serviceId: ecServerId, + success(res) { + log('device getBLEDeviceCharacteristics:') + log(res.characteristics) + if (res.characteristics.length < 2) { + resolve({ ok: false, errCode: 20000, errMsg: '特征值出错' }) + return + } + let uuid1 = '' + let uuid2 = '' + uuid1 = res.characteristics[0].uuid.toUpperCase() + uuid2 = res.characteristics[1].uuid.toUpperCase() + if ((uuid1 === ecWriteCharacteristicIdOption1) + && (uuid2 === ecReadCharacteristicIdOption1)) { + ecWriteCharacteristicId = ecWriteCharacteristicIdOption1 + ecReadCharacteristicId = ecReadCharacteristicIdOption1 + resolve({ ok: true, errCode: 0, errMsg: '' }) + } + else if ((uuid1 === ecReadCharacteristicIdOption1) + && (uuid2 === ecWriteCharacteristicIdOption1)) { + ecWriteCharacteristicId = ecWriteCharacteristicIdOption1 + ecReadCharacteristicId = ecReadCharacteristicIdOption1 + resolve({ ok: true, errCode: 0, errMsg: '' }) + } + else if ((uuid1 === ecWriteCharacteristicIdOption2) + && (uuid2 === ecReadCharacteristicIdOption2)) { + ecWriteCharacteristicId = ecWriteCharacteristicIdOption2 + ecReadCharacteristicId = ecReadCharacteristicIdOption2 + resolve({ ok: true, errCode: 0, errMsg: '' }) + } + else if ((uuid1 === ecReadCharacteristicIdOption2) + && (uuid2 === ecWriteCharacteristicIdOption2)) { + ecWriteCharacteristicId = ecWriteCharacteristicIdOption2 + ecReadCharacteristicId = ecReadCharacteristicIdOption2 + resolve({ ok: true, errCode: 0, errMsg: '' }) + } + else { + resolve({ ok: false, errCode: 20000, errMsg: '特征值出错' }) + } + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +//订阅通知 接收key +const notifyBLECharacteristicValueChange = () => { + return new Promise(function (resolve, reject) { + //开始订阅 + wx.notifyBLECharacteristicValueChange({ + state: true, + deviceId: ecDeviceId, + serviceId: ecServerId, + characteristicId: ecReadCharacteristicId, + success(res) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +const setBLEMTU = (mtu) => { + return new Promise(function (resolve, reject) { + //开始订阅 + wx.setBLEMTU({ + deviceId: ecDeviceId, + mtu, + success(res) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +const easyConnect = async (name, cb) => { + let res = {} + await closeBluetoothAdapter() + await openBluetoothAdapter() + res = await createBLEConnection(name) + if (!res.ok) { + res = { ok: false, errMsg: '蓝牙连接失败|' + res.errCode + '|' + res.errMsg, errCode: 10001 } + cb(res) + return res + } + res = await getBLEDeviceServices() + if (!res.ok) { + closeBLEConnection() + res = { ok: false, errMsg: '获取服务失败|' + res.errCode + '|' + res.errMsg, errCode: 10002 } + cb(res) + return res + } + res = await getBLEDeviceCharacteristics() + if (!res.ok) { + closeBLEConnection() + res = { ok: false, errMsg: '获取特性失败|' + res.errCode + '|' + res.errMsg, errCode: 10003 } + cb(res) + return res + } + res = await notifyBLECharacteristicValueChange() + if (!res.ok) { + closeBLEConnection() + res = { ok: false, errMsg: '订阅失败|' + res.errCode + '|' + res.errMsg, errCode: 10004 } + cb(res) + return res + } + await setBLEMTU(250) + res = { ok: true, errMsg: '', errCode: 0 } + cb(res) + return res +} + +const onBLECharacteristicValueChange = (cb) => { + wx.onBLECharacteristicValueChange((res) => { + let x = new Uint8Array(res.value); + // log(x) + let strHex = "" + let str = "" + for (let i = 0; i < x.length; i++) { + strHex = strHex + x[i].toString(16).padStart(2, "0") + str = str + String.fromCharCode(x[i]) + } + // log(strHex) + // log(str) + cb(str, strHex) + }) +} + +const writeBLECharacteristicValue = (data) => { + return new Promise(function (resolve, reject) { + wx.writeBLECharacteristicValue({ + deviceId: ecDeviceId, + serviceId: ecServerId, + characteristicId: ecWriteCharacteristicId, + value: data, + success(res) { + resolve({ ok: true, errCode: 0, errMsg: '' }) + }, + fail(res) { + resolve({ ok: false, errCode: res.errCode, errMsg: res.errMsg }) + } + }) + }) +} + +const easySendData = async (str, isHex) => { + if (str.length === 0) return + if (isHex) { + const buffer = new ArrayBuffer(str.length / 2); + let x = new Uint8Array(buffer); + for (let i = 0; i < x.length; i++) { + x[i] = parseInt(str.substr(2 * i, 2), 16) + } + return await writeBLECharacteristicValue(buffer) + } else { + const buffer = new ArrayBuffer(str.length); + let x = new Uint8Array(buffer); + for (let i = 0; i < x.length; i++) { + x[i] = str.charCodeAt(i) + } + return await writeBLECharacteristicValue(buffer) + } +} + +module.exports = { + openBluetoothAdapter, + closeBluetoothAdapter, + getBluetoothAdapterState, + startBluetoothDevicesDiscovery, + stopBluetoothDevicesDiscovery, + easyConnect, + closeBLEConnection, + onBLEConnectionStateChange, + onBLECharacteristicValueChange, + easySendData, +} \ No newline at end of file