2026-06-23T11:27:35

This commit is contained in:
yuntang 2026-06-23 11:27:35 +08:00
commit 3b2571d150
20 changed files with 2802 additions and 0 deletions

22
push.sh Normal file
View File

@ -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

4
src/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
unpackage/
.hbuilderx/
.DS_Store

51
src/App.uvue Normal file
View File

@ -0,0 +1,51 @@
<script setup lang="uts">
// #ifdef APP-ANDROID || APP-HARMONY
let firstBackTime = 0
// #endif
onLaunch(() => {
console.log('App Launch')
})
onAppShow(() => {
console.log('App Show')
})
onAppHide(() => {
console.log('App Hide')
})
// #ifdef APP-ANDROID || APP-HARMONY
onLastPageBackPress(() => {
console.log('App LastPageBackPress')
if (firstBackTime == 0) {
uni.showToast({
title: '再按一次退出应用',
position: 'bottom',
})
firstBackTime = Date.now()
setTimeout(() => {
firstBackTime = 0
}, 2000)
} else if (Date.now() - firstBackTime < 2000) {
firstBackTime = Date.now()
uni.exit()
}
})
onExit(() => {
console.log('App Exit')
})
// #endif
</script>
<style>
/*每个页面公共css */
.uni-row {
flex-direction: row;
}
.uni-column {
flex-direction: column;
}
</style>

109
src/android-logcat.log Normal file
View File

@ -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.06uni-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"}]

20
src/index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main"></script>
</body>
</html>

119
src/launch-android.log Normal file
View File

@ -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.06uni-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 已停止运行...

9
src/main.uts Normal file
View File

@ -0,0 +1,9 @@
import App from './App.uvue'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}

64
src/manifest.json Normal file
View File

@ -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": {}
}
}
}

23
src/pages.json Normal file
View File

@ -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": {}
}

468
src/pages/data/data.uvue Normal file
View File

@ -0,0 +1,468 @@
<template>
<view class="container">
<!-- #ifdef APP -->
<scroll-view class="page-scroll" :scroll-y="true">
<!-- #endif -->
<view class="page-content">
<view class="header">
<text class="title">设备数据</text>
<text class="back-btn" @click="goBack">返回</text>
</view>
<view class="device-info">
<text class="device-name">{{ deviceName }}</text>
<text class="device-id">{{ deviceId }}</text>
</view>
<view class="status-bar">
<text class="status-indicator" :class="{ 'connected': isConnected }"></text>
<text class="status-text">{{ isConnected ? '已连接' : '未连接' }}</text>
</view>
<!-- <view class="debug-panel">
<text class="debug-title">蓝牙调试状态</text>
<text class="debug-content">{{ debugMessage }}</text>
<text class="debug-tip">观察是否出现“当前订阅特征: ...”与“收到 notify: ...”</text>
<view class="debug-log-list">
<text class="debug-log-item" v-for="(item, index) in debugLogList" :key="index">{{ item }}</text>
</view>
<view class="debug-actions">
<text class="debug-action-btn secondary" @click="runMeasureCommand">执行测量</text>
<text class="debug-action-btn secondary" @click="runCalibrationCommand">执行校准</text>
<text class="debug-action-btn" @click="readDataOnce">读取一次</text>
</view>
</view> -->
<view class="data-section">
<view class="section-title">
<text class="section-title-text">接收到的数据</text>
<text class="update-time">{{ currentTime }}</text>
</view>
<view class="received-data">
<text class="data-label">接收到的字符串:</text>
<view class="received-text-box">
<text class="data-content">{{ receivedText }}</text>
</view>
</view>
</view>
<view class="data-section">
<view class="section-title">
<text class="section-title-text">数据记录</text>
</view>
<view class="record-list">
<view class="record-item" v-for="(record, index) in recordList" :key="index">
<text class="record-time">{{ record.time }}</text>
<text class="record-data">{{ record.data }}</text>
</view>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</view>
</template>
<script>
import { onBluetoothDataReceived, onBluetoothDebugMessage, getBluetoothDebugSnapshot, readBluetoothDataOnce, writeBluetoothData, disconnectBluetoothDevice } from '@/uni_modules/sp2-bluetooth'
type DeviceRecordItem = {
time: string,
data: string
}
function formatTimePart(value: number): string {
return value.toString().padStart(2, '0')
}
const MEASURE_COMMAND = '#RunMeas\n'
const CALIBRATION_COMMAND = '#RunCal\n'
export default {
data() {
const recordList: DeviceRecordItem[] = []
const debugLogList: string[] = []
return {
deviceId: '',
deviceName: '',
currentTime: '--:--:--',
receivedText: '等待接收数据...',
debugMessage: '等待蓝牙调试状态...',
debugLogList: debugLogList,
isConnected: false,
recordList: recordList
}
},
onLoad(options : UTSJSONObject) {
const incomingDeviceId = options['deviceId'] as string | null
if (incomingDeviceId != null && incomingDeviceId.length > 0) {
this.deviceId = incomingDeviceId
}
const incomingDeviceName = options['deviceName'] as string | null
if (incomingDeviceName != null && incomingDeviceName.length > 0) {
const decodedDeviceName = decodeURIComponent(incomingDeviceName)
if (decodedDeviceName != null && decodedDeviceName.length > 0) {
this.deviceName = decodedDeviceName
} else {
this.deviceName = incomingDeviceName
}
} else {
this.deviceName = '未知设备'
}
this.isConnected = true
this.refreshCurrentTime()
const debugSnapshot = getBluetoothDebugSnapshot()
this.debugMessage = `${this.currentTime} ${debugSnapshot}`
this.pushDebugLog(this.debugMessage)
this.setupDataListener()
},
onUnload() {
},
methods: {
goBack() {
disconnectBluetoothDevice()
uni.navigateBack()
},
refreshCurrentTime() {
const now = new Date()
const hours = formatTimePart(now.getHours())
const minutes = formatTimePart(now.getMinutes())
const seconds = formatTimePart(now.getSeconds())
this.currentTime = `${hours}:${minutes}:${seconds}`
},
setupDataListener() {
const currentDebugMessage = this.debugMessage
if (currentDebugMessage.length === 0 || currentDebugMessage.includes('等待蓝牙调试状态')) {
this.refreshCurrentTime()
this.debugMessage = `${this.currentTime} 已注册蓝牙调试监听`
this.pushDebugLog(this.debugMessage)
}
onBluetoothDebugMessage((message: string) => {
this.refreshCurrentTime()
this.debugMessage = `${this.currentTime} ${message}`
this.pushDebugLog(this.debugMessage)
})
onBluetoothDataReceived((data: string, hex: string) => {
this.refreshCurrentTime()
const normalizedData = this.normalizeReceivedText(data)
const timeStr = this.currentTime
const dataStr = normalizedData
if (this.receivedText === '等待接收数据...') {
this.receivedText = normalizedData
} else {
this.receivedText = `${this.receivedText}\n${normalizedData}`
}
if (this.recordList.length >= 50) {
this.recordList.pop()
}
const recordItem: DeviceRecordItem = { time: timeStr, data: dataStr }
this.recordList.unshift(recordItem)
})
},
pushDebugLog(message: string) {
if (this.debugLogList.length >= 12) {
this.debugLogList.pop()
}
this.debugLogList.unshift(message)
},
normalizeReceivedText(data: string): string {
const trimmedData = data.replace(/\r/g, '').replace(/\n/g, '').trim()
if (trimmedData.endsWith(';')) {
return trimmedData.substring(0, trimmedData.length - 1)
}
return trimmedData
},
readDataOnce() {
this.refreshCurrentTime()
this.debugMessage = `${this.currentTime} 正在手动读取一次`
this.pushDebugLog(this.debugMessage)
readBluetoothDataOnce()
},
triggerCommand(command: string, label: string) {
this.refreshCurrentTime()
this.debugMessage = `${this.currentTime} 正在发送${label}命令`
this.pushDebugLog(this.debugMessage)
writeBluetoothData(command)
this.refreshCurrentTime()
this.debugMessage = `${this.currentTime} ${label}命令已发送,等待 FFF1 通知数据`
this.pushDebugLog(this.debugMessage)
},
runMeasureCommand() {
this.triggerCommand(MEASURE_COMMAND, '测量')
},
runCalibrationCommand() {
this.triggerCommand(CALIBRATION_COMMAND, '校准')
},
sendData() {
const testData = "Hello BLE"
writeBluetoothData(testData)
}
}
}
</script>
<style scoped>
.container {
width: 100%;
flex: 1;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.page-scroll {
width: 100%;
flex: 1;
}
.page-content {
width: 100%;
display: flex;
flex-direction: column;
}
.header {
width: 100%;
height: 44px;
background: #007aff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-sizing: border-box;
}
.title {
color: #ffffff;
font-size: 18px;
font-weight: 500;
}
.back-btn {
color: #ffffff;
font-size: 14px;
padding: 5px 10px;
border: 1px solid #ffffff;
border-radius: 5px;
}
.device-info {
width: 100%;
padding: 15px 20px;
background-color: #ffffff;
display: flex;
flex-direction: column;
border-bottom: 1px solid #e5e5e5;
}
.device-name {
font-size: 16px;
font-weight: 500;
color: #333333;
margin-bottom: 5px;
}
.device-id {
font-size: 12px;
color: #999999;
}
.status-bar {
width: 100%;
padding: 10px 20px;
background-color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #e5e5e5;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #999999;
margin-right: 8px;
}
.connected {
background-color: #4cd964;
}
.status-text {
font-size: 14px;
color: #666666;
}
.debug-panel {
width: 100%;
padding: 15px 20px;
background-color: #fff8e8;
margin-top: 10px;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.debug-title {
font-size: 14px;
font-weight: 500;
color: #8c5a00;
margin-bottom: 8px;
}
.debug-content {
font-size: 12px;
color: #8c5a00;
}
.debug-tip {
font-size: 11px;
color: #a7771f;
margin-top: 6px;
}
.debug-log-list {
width: 100%;
display: flex;
flex-direction: column;
margin-top: 8px;
}
.debug-log-item {
font-size: 11px;
color: #8c5a00;
margin-top: 4px;
}
.debug-actions {
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 10px;
}
.debug-action-btn {
padding: 6px 12px;
border-radius: 999px;
background-color: #8c5a00;
color: #ffffff;
font-size: 12px;
margin-left: 8px;
}
.secondary {
background-color: #007aff;
}
.data-section {
width: 100%;
padding: 15px 20px;
background-color: #ffffff;
margin-top: 10px;
display: flex;
flex-direction: column;
}
.section-title {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-title-text {
font-size: 16px;
font-weight: 500;
color: #333333;
}
.update-time {
font-size: 12px;
font-weight: normal;
color: #999999;
}
.received-data {
width: 100%;
margin-bottom: 15px;
padding: 12px;
background-color: #f8f8f8;
border-radius: 8px;
}
.received-text-box {
width: 100%;
min-height: 96px;
padding: 14px 16px;
background-color: #ffffff;
border: 1px solid #d9e2ef;
border-radius: 10px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
}
.data-label {
width: 100%;
font-size: 12px;
color: #999999;
margin-bottom: 8px;
}
.data-content {
width: 100%;
font-size: 14px;
color: #333333;
}
.record-list {
width: 100%;
display: flex;
flex-direction: column;
max-height: 300px;
}
.record-item {
width: 100%;
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.record-time {
font-size: 12px;
color: #999999;
}
.record-data {
font-size: 12px;
color: #666666;
}
</style>

274
src/pages/index/index.uvue Normal file
View File

@ -0,0 +1,274 @@
<template>
<view class="container">
<!-- #ifdef APP -->
<scroll-view class="page-scroll" :scroll-y="true">
<!-- #endif -->
<view class="content">
<view class="header">
<text class="title">连接设备</text>
<text class="right" @click="reloadDevices">重新扫描</text>
</view>
<view class="tip-container">
<text class="tip-text">请将手机放置到设备附近,并确保蓝牙与必要权限已开启</text>
</view>
<view class="loading-area">
<view class="bluetooth-icon">
<text class="bluetooth-symbol">BLE</text>
</view>
<text class="loading-text">{{ statusText }}</text>
</view>
<view class="found-tip">
<text class="found-text">{{ deviceList.length > 0 ? `发现 ${deviceList.length} 台可用设备,点击连接` : emptyText }}</text>
</view>
<view class="device-list">
<view class="device-card" :class="{ 'device-card-connecting': connectingDeviceId.length > 0 }" v-for="device in deviceList" :key="device.deviceId" @click="connectDevice(device)">
<text class="device-name">{{ device.name.length > 0 ? device.name : '未知设备' }}</text>
<text class="device-mac">{{ device.deviceId }}</text>
</view>
</view>
</view>
<!-- #ifdef APP -->
</scroll-view>
<!-- #endif -->
</view>
</template>
<script>
import { startBluetoothScan, stopBluetoothScan, connectBluetoothDevice, disconnectBluetoothDevice, type BluetoothDeviceItem } from '@/uni_modules/sp2-bluetooth'
export default {
data() {
return {
deviceList: [] as BluetoothDeviceItem[],
statusText: '正在扫描附近 BLE 设备',
emptyText: '暂未发现设备,请确认设备已开机并靠近手机',
connectingDeviceId: ''
}
},
onLoad() {
this.initBluetooth()
},
onShow() {
this.connectingDeviceId = ''
},
onUnload() {
stopBluetoothScan()
disconnectBluetoothDevice()
},
methods: {
initBluetooth() {
this.connectingDeviceId = ''
this.deviceList = []
this.statusText = '正在扫描附近 BLE 设备'
this.emptyText = '暂未发现设备,请确认设备已开机并靠近手机'
disconnectBluetoothDevice()
stopBluetoothScan()
startBluetoothScan((device : BluetoothDeviceItem) => {
if (!device.name.startsWith('NB-')) {
return
}
let exists = false
for (let i = 0; i < this.deviceList.length; i++) {
if (this.deviceList[i].deviceId === device.deviceId) {
exists = true
break
}
}
if (!exists) {
this.deviceList.push(device)
this.statusText = '已发现 BLE 设备,继续扫描中'
}
}, (message : string) => {
this.statusText = '扫描失败'
this.emptyText = message
uni.showToast({
title: message,
icon: 'none'
})
})
},
reloadDevices() {
if (this.connectingDeviceId.length > 0) {
return
}
this.initBluetooth()
},
connectDevice(device : BluetoothDeviceItem) {
if (this.connectingDeviceId.length > 0) {
return
}
this.connectingDeviceId = device.deviceId
this.statusText = '正在连接设备'
stopBluetoothScan()
connectBluetoothDevice(device.deviceId, () => {
this.statusText = '设备连接成功'
setTimeout(() => {
this.connectingDeviceId = ''
uni.navigateTo({
url: `/pages/data/data?deviceId=${device.deviceId}&deviceName=${encodeURIComponent(device.name)}`
})
}, 500)
}, (message : string) => {
this.statusText = '连接失败'
this.connectingDeviceId = ''
uni.showToast({
title: message,
icon: 'none'
})
})
}
}
};
</script>
<style scoped>
.container {
width: 100%;
flex: 1;
background-color: #ffffff;
display: flex;
flex-direction: column;
}
.page-scroll {
width: 100%;
flex: 1;
}
.content {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 0 20px;
}
.header {
width: 100%;
height: 44px;
background: #007aff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-sizing: border-box;
}
.title {
color: #ffffff;
font-size: 18px;
font-weight: 500;
}
.right {
color: #ffffff;
font-size: 14px;
padding: 5px 10px;
border: 1px solid #ffffff;
border-radius: 5px;
}
.tip-container {
width: 100%;
padding: 20px 0;
}
.tip-text {
width: 100%;
text-align: center;
font-size: 15px;
color: #333333;
line-height: 1.5;
}
.loading-area {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
}
.bluetooth-icon {
width: 120px;
height: 120px;
border-radius: 60px;
background-color: #e8f2ff;
border: 2px solid #007aff;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.bluetooth-symbol {
font-size: 28px;
font-weight: 700;
letter-spacing: 2px;
color: #007aff;
}
.loading-text {
margin-top: 15px;
font-size: 16px;
color: #007aff;
}
.found-tip {
width: 100%;
margin: 10px 0;
}
.found-text {
width: 100%;
text-align: center;
font-size: 14px;
color: #666666;
}
.device-list {
width: 100%;
display: flex;
flex-direction: column;
padding-bottom: 24px;
box-sizing: border-box;
}
.device-card {
width: 100%;
padding: 20px;
background-color: #f0f0f0;
border-radius: 12px;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin-top: 12px;
}
.device-card-connecting {
opacity: 0.6;
}
.device-name {
font-size: 18px;
color: #333333;
font-weight: 500;
}
.device-mac {
margin-top: 8px;
font-size: 14px;
color: #999999;
}
</style>

7
src/platformConfig.json Normal file
View File

@ -0,0 +1,7 @@
// https://doc.dcloud.net.cn/uni-app-x/tutorial/ls-plugin.html#setting
{
"targets": [
"APP-ANDROID"
]
}

BIN
src/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

76
src/uni.scss Normal file
View File

@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

View File

@ -0,0 +1,50 @@
{
"id": "sp2-bluetooth",
"displayName": "sp2 蓝牙插件",
"version": "1.0.0",
"description": "用于 uni-app x Android BLE 扫描与连接的 UTS 插件",
"keywords": [
"bluetooth",
"ble",
"uts"
],
"engines": {
"HBuilderX": "^5.0.0",
"uni-app-x": "^5.0.0"
},
"dcloudext": {
"type": "uts",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "扫描附近蓝牙设备并建立 BLE 连接",
"permissions": "蓝牙与定位权限"
}
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"client": {
"uni-app-x": {
"app": {
"android": {
"extVersion": "1.0.0",
"minVersion": "21"
}
}
}
}
}
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.dcloud.uni_modules.sp2Bluetooth">
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT" />
</manifest>

View File

@ -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<Pair<BluetoothGattService, BluetoothGattCharacteristic>>())
private val pendingDebugMessages = Collections.synchronizedList(ArrayList<String>())
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<Pair<String, String>>())
private var hasConnectedOnce = false
private val discoveredDeviceIds = Collections.synchronizedSet(HashSet<String>())
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<ScanResult>) {
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<String> {
val permissions = mutableListOf<String>()
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<String> {
val permissions = mutableListOf<String>()
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<Pair<BluetoothGattService, BluetoothGattCharacteristic>> {
val services = gatt.services ?: return emptyList()
val preferredServiceUuid = SERVICE_UUID.lowercase()
val preferredCharacteristicUuid = READ_CHARACTERISTIC_UUID.lowercase()
val preferredTargets = ArrayList<Pair<BluetoothGattService, BluetoothGattCharacteristic>>()
val fallbackTargets = ArrayList<Pair<BluetoothGattService, BluetoothGattCharacteristic>>()
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<BluetoothGattService, BluetoothGattCharacteristic>? {
val services = gatt.services ?: return null
val preferredServiceUuid = SERVICE_UUID.lowercase()
val preferredCharacteristicUuid = WRITE_CHARACTERISTIC_UUID.lowercase()
var fallbackTarget: Pair<BluetoothGattService, BluetoothGattCharacteristic>? = 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<BluetoothGattService, BluetoothGattCharacteristic>? {
val services = gatt.services ?: return null
val preferredServiceUuid = SERVICE_UUID.lowercase()
val preferredCharacteristicUuid = READ_CHARACTERISTIC_UUID.lowercase()
var fallbackTarget: Pair<BluetoothGattService, BluetoothGattCharacteristic>? = 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<Pair<BluetoothGattService, BluetoothGattCharacteristic>>()
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<Pair<String, String>>()
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<String>()
synchronized(pendingDebugMessages) {
if (pendingDebugMessages.isEmpty()) {
return
}
messages.addAll(pendingDebugMessages)
pendingDebugMessages.clear()
}
mainHandler.post {
for (message in messages) {
callback(message)
}
}
}
}

View File

@ -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)
}

View File

@ -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

388
src/utils/ecBLE.js Normal file
View File

@ -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,
}