- 开发工具:HBuilder X 3.4.7.20220422
- uni-app + Vue3
- 以安卓App的方式运行(iOS和小程序同理)
思路
蓝牙收发数据的逻辑和我们常用的 AJAX 进行的网络请求是有一丢丢不同的。
其中较大的区别是:蓝牙接收数据不是那么的稳定,相比起网络请求,蓝牙更容易出现丢包的情况。
在开发中,AJAX 发起的请求不管成功还是失败,浏览器基本都会给你一个答复。但 uni-app 提供的 api 来看,蓝牙接收数据会显得更加 “异步” 。
大致思路
使用蓝牙进行数据传输的大概思路如下:
- 初始化:打开蓝牙模块
- 搜寻:检测附近存在的设备
- 连接:找到目标设备进行
- 监听:开启监听功能,接收其他设备传过来的数据
- 发送指令:不管发送数据还是读取数据,都可以理解为向外发送指令
初始化阶段
使用蓝牙之前,需要初始化蓝牙模块,这是最最最开始就要做的!
使用 uni.openBluetoothAdapter 这个 api 就可以初始化蓝牙模块。其他
蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。否则 API 会返回错误( errCode=10000 )。
作者:德育处主任
链接:https://juejin.cn/post/7093171532318375950
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<template>
<view>
<button @click="initBlue">初始化蓝牙</button>
</view>
</template>
<script setup>
// 【1】初始化蓝牙
function initBlue() {
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功')
console.log(res)
},
fail(err) {
console.log('初始化蓝牙失败')
console.error(err)
}
})
}
</script>
如果你手机开启了蓝牙,点击页面上的按钮后,控制台就会输出如下内容1
{"errMsg":"openBluetoothAdapter:ok"}
如果手机没开启蓝牙,就会返回如下内容
1 | {"errMsg":"openBluetoothAdapter:fail not available","code":10001} |
10001代表当前蓝牙适配器不可用。
如果你的控制台能打印出 {“errMsg”:”openBluetoothAdapter:ok”} 证明第一步已经成功了。
接下来可以开始搜索附近蓝牙设备。
搜寻附近设备
这一步需要2个 api 配合完成。所以可以分解成以下2步:
- 开启搜寻功能:uni.startBluetoothDevicesDiscovery
- 监听搜寻到新设备:uni.onBluetoothDeviceFound
开发蓝牙相关功能时,操作逻辑更像是推送,所以“开启搜索”和“监听新设备”是分开操作的。
uni.startBluetoothDevicesDiscovery 可以让设备开始搜索附近蓝牙设备,但这个方法比较耗费系统资源,建议在连接到设备之后就使用 uni.stopBluetoothDevicesDiscovery 停止继续搜索。
uni.startBluetoothDevicesDiscovery 方法里可以传入一个对象,该对象接收几个参数,但初学的话我们只关注 success 和 fail。如果你的项目中硬件佬有提供 service 的 uuid 给你的话,你也可以在 services 里传入。其他参数可以查看官方文档的介绍。
在使用 uni.startBluetoothDevicesDiscovery (开始搜索)后,可以使用 uni.onBluetoothDeviceFound 进行监听,这个方法里面接收一个回调函数。
代码示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84<template>
<view>
<scroll-view
scroll-y
class="box"
>
<view class="item" v-for="item in blueDeviceList">
<view>
<text>id: {{ item.deviceId }}</text>
</view>
<view>
<text>name: {{ item.name }}</text>
</view>
</view>
</scroll-view>
<button @click="initBlue">初始化蓝牙</button>
<button @click="discovery">搜索附近蓝牙设备</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 搜索到的蓝牙设备列表
const blueDeviceList = ref([])
// 【1】初始化蓝牙
function initBlue() {
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功')
console.log(res)
},
fail(err) {
console.log('初始化蓝牙失败')
console.error(err)
}
})
}
// 【2】开始搜寻附近设备
function discovery() {
uni.startBluetoothDevicesDiscovery({
success(res) {
console.log('开始搜索')
// 开启监听回调
uni.onBluetoothDeviceFound(found)
},
fail(err) {
console.log('搜索失败')
console.error(err)
}
})
}
// 【3】找到新设备就触发该方法
function found(res) {
console.log(res)
blueDeviceList.value.push(res.devices[0])
}
</script>
<style>
.box {
width: 100%;
height: 400rpx;
box-sizing: border-box;
margin-bottom: 20rpx;
border: 2px solid dodgerblue;
}
.item {
box-sizing: border-box;
padding: 10rpx;
border-bottom: 1px solid #ccc;
}
button {
margin-bottom: 20rpx;
}
</style>
上面代码的逻辑是,如果开启 “寻找附近设备” 功能成功,接着就开启 “监听寻找到新设备的事件” 。
搜索到的设备会返回以下数据:
1 | { |
每监听到一个新的设备,我都会将其添加到 蓝牙设备列表(blueDeviceList) 里,最后讲这个列表的数据渲染到页面上。
连接目标设备
连接目标设备只需要1个 api 就能完成。但根据文档提示,我们连接后还需要关闭 “搜索附近设备” 的功能,这个很好理解,既然找到了,再继续找就是浪费资源。
流程如下:
- 获取设备ID:根据 uni.onBluetoothDeviceFound 回调,拿到设备ID
- 连接设备:使用设备ID进行连接 uni.createBLEConnection
- 停止搜索:uni.stopBluetoothDevicesDiscovery
我给每条搜索到的蓝牙结果添加一个 click 事件,会向目标设备发送连接请求。
我的设备名称是 leihou ,所以我点击了这条。
代码示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121<template>
<view>
<scroll-view
scroll-y
class="box"
>
<view class="item" v-for="item in blueDeviceList" @click="connect(item)">
<view>
<text>id: {{ item.deviceId }}</text>
</view>
<view>
<text>name: {{ item.name }}</text>
</view>
</view>
</scroll-view>
<button @click="initBlue">初始化蓝牙</button>
<button @click="discovery">搜索附近蓝牙设备</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 搜索到的蓝牙设备列表
const blueDeviceList = ref([])
// 【1】初始化蓝牙
function initBlue() {
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功')
console.log(res)
},
fail(err) {
console.log('初始化蓝牙失败')
console.error(err)
}
})
}
// 【2】开始搜寻附近设备
function discovery() {
uni.startBluetoothDevicesDiscovery({
success(res) {
console.log('开始搜索')
// 开启监听回调
uni.onBluetoothDeviceFound(found)
},
fail(err) {
console.log('搜索失败')
console.error(err)
}
})
}
// 【3】找到新设备就触发该方法
function found(res) {
console.log(res)
blueDeviceList.value.push(res.devices[0])
}
// 蓝牙设备的id
const deviceId = ref('')
// 【4】连接设备
function connect(data) {
console.log(data)
deviceId.value = data.deviceId
uni.createBLEConnection({
deviceId: deviceId.value,
success(res) {
console.log('连接成功')
console.log(res)
// 停止搜索
stopDiscovery()
},
fail(err) {
console.log('连接失败')
console.error(err)
}
})
}
// 【5】停止搜索
function stopDiscovery() {
uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log('停止成功')
console.log(res)
},
fail(err) {
console.log('停止失败')
console.error(err)
}
})
}
</script>
<style>
.box {
width: 100%;
height: 400rpx;
box-sizing: border-box;
margin-bottom: 20rpx;
border: 2px solid dodgerblue;
}
.item {
box-sizing: border-box;
padding: 10rpx;
border-bottom: 1px solid #ccc;
}
button {
margin-bottom: 20rpx;
}
</style>
连接成功后在控制台会输出1
{"errMsg":"createBLEConnection:ok"}
在连接成功后就立刻调用 uni.stopBluetoothDevicesDiscovery 方法停止继续搜索附近其他设备,停止成功后会输出
1 | {"errMsg":"stopBluetoothDevicesDiscovery:ok"} |
监听
在连接完设备后,就要先开启监听数据的功能。这样才能接收到发送读写指令后设备给你回调的信息。
要开启监听,首先需要知道蓝牙设备提供了那些服务,然后通过服务获取特征值,特征值会告诉你哪个可读,哪个可写。最后根据特征值进行消息监听。
步骤如下:
- 获取蓝牙设备服务:uni.getBLEDeviceServices
- 获取特征值:uni.getBLEDeviceCharacteristics
- 开启消息监听:uni.notifyBLECharacteristicValueChange
- 接收消息监听传来的数据:uni.onBLECharacteristicValueChange
正常情况下,硬件佬会提前把蓝牙设备的指定服务还有特征值告诉你。
比如我这个设备的蓝牙服务是:0000FFE0-0000-1000-8000-00805F9B34FB
特征值是:0000FFE1-0000-1000-8000-00805F9B34FB
第一步,获取蓝牙服务
1 | <template> |
此时点击按钮,将会获取到已连接设备的所有服务。
我的设备有以下几个服务。你在工作中拿到的 服务uuid 和我的是不一样的,数量也不一定相同。
可以发现,我拿到的结果里有 0000FFE0-0000-1000-8000-00805F9B34FB 这条服务。
1 | { |
第二步,获取指定服务的特征值
获取特征值,需要传 设备ID 和 服务ID。
在上两步我拿到了 设备ID 为 B4:10:7B:C4:83:14,服务ID 为 0000FFE0-0000-1000-8000-00805F9B34FB。
1 | <template> |
最后成功输出
1 | { |
characteristics 字段里保存了该服务的所有特征值,我的设备这个服务只有1个特征值,并且读、写、消息推送都为 true。
你的设备可能不止一条特征值,需要监听那条特征值这需要你和硬件佬协商的(通常也是硬件佬直接和你说要监听哪条)。
第三、四步,开启消息监听 并 接收消息监听传来的数据
根据已经拿到的 设备ID、服务ID、特征值,就可以开启对应的监听功能。
使用 uni.notifyBLECharacteristicValueChange 开启消息监听;
并在 uni.onBLECharacteristicValueChange 方法触发监听到的消息。
1 | <template> |
listenValueChange 方法是用来接收设备传过来的消息。
上面的例子中,res 的结果是
1 | { |
设备传过来的内容就放在 value 字段里,但因为该字段的类型是 ArrayBuffer,所以无法在控制台用肉眼直接观察。于是就通过 ab2hex 方法将该值转成 16进制 ,最后再用 hexCharCodeToStr 方法将 16进制 转成 ASCII码。
我从设备里发送一段字符串过来:leihou
App端收到的数据转成 16进制 后的结果:6c6569686f75
再从 16进制 转成 ASCII码 后的结果:leihou
发送指令
终于到最后一步了。
从 uni-app 和 微信小程序 提供的蓝牙api 来看,发送指令只要有2个方法:
- uni.writeBLECharacteristicValue:向低功耗蓝牙设备特征值中写入二进制数据。
- uni.readBLECharacteristicValue:读取低功耗蓝牙设备的特征值的二进制数据值。
这里需要理清一个概念,本节的内容为 “发送指令” ,也就是说,从你的app或小程序向其他蓝牙设备发送指令,而这个指令分2种情况,一种是你要发送一些数据给蓝牙设备,另一种情况是你叫蓝牙设备给你发点信息。
uni.writeBLECharacteristicValue
这两种情况我们需要分开讨论,先讲uni.writeBLECharacteristicValue 。
uni.writeBLECharacteristicValue 从文档可以看出,这个 api 是可以发送一些数据给蓝牙设备,但发送的值要转成 ArrayBuffer 。
1 | <template> |
此时,如果 uni.writeBLECharacteristicValue 走 success ,证明你已经把数据向外成功发送了,但不代表设备一定就收到了。
通常设备收到你发送过去的信息,会返回一条消息给你,而这个回调消息会在 uni.onBLECharacteristicValueChange 触发,也就是 第【9】步 那里。但这是蓝牙设备那边控制的,你作为前端佬,人家“已读不回”你也拿人家没办法。
uni.readBLECharacteristicValue
在 “监听” 部分,我们使用了 uni.getBLEDeviceCharacteristics 获取设备的特征值,我的设备提供的特征值支持 read ,所以可以使用 uni.readBLECharacteristicValue 向蓝牙设备发送一条 “读取” 指令。然后在 uni.onBLECharacteristicValueChange 里可以接收设备发送过来的数据。
1 | <template> |
使用 “读取” 的方式向设备发送指令,是不需要另外传值的。
此时我的设备返回 00
这个数据是硬件那边设置的。
在日常工作中,uni.readBLECharacteristicValue 的作用主要是读取数据,但使用场景不算很多。
我在工作中遇到的场景是:蓝牙设备提供了几个接口,而且传过来的数据比较大,比如传图片给app这边。我就会先用 uni.writeBLECharacteristicValue 告诉设备我现在需要取什么接口的数据,然后用 uni.readBLECharacteristicValue 发送读取数据的请求,如果数据量比较大,就要重复使用 uni.readBLECharacteristicValue 进行读取。比如上面的例子,我读第一次的时候返回 00 ,读第二次就返回 01 ……
最后再提醒一下,uni.readBLECharacteristicValue 只负责发送读取的请求,并且里面的 success 和 fail 只是返回你本次发送请求的动作是否成功,至于对面的蓝牙设备有没有收到这个指令你是不清楚的。
最后需要通过 uni.getBLEDeviceCharacteristics 监听设备传过来的数据。
完整代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331<template>
<view>
<scroll-view
scroll-y
class="box"
>
<view class="item" v-for="item in blueDeviceList" @click="connect(item)">
<view>
<text>id: {{ item.deviceId }}</text>
</view>
<view>
<text>name: {{ item.name }}</text>
</view>
</view>
</scroll-view>
<button @click="initBlue">1 初始化蓝牙</button>
<button @click="discovery">2 搜索附近蓝牙设备</button>
<button @click="getServices">3 获取蓝牙服务</button>
<button @click="getCharacteristics">4 获取特征值</button>
<button @click="notify">5 开启消息监听</button>
<button @click="send">6 发送数据</button>
<button @click="read">7 读取数据</button>
<view class="msg_x">
<view class="msg_txt">
监听到的内容:{{ message }}
</view>
<view class="msg_hex">
监听到的内容(十六进制):{{ messageHex }}
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 搜索到的蓝牙设备列表
const blueDeviceList = ref([])
// 【1】初始化蓝牙
function initBlue() {
uni.openBluetoothAdapter({
success(res) {
console.log('初始化蓝牙成功')
console.log(res)
},
fail(err) {
console.log('初始化蓝牙失败')
console.error(err)
}
})
}
// 【2】开始搜寻附近设备
function discovery() {
uni.startBluetoothDevicesDiscovery({
success(res) {
console.log('开始搜索')
// 开启监听回调
uni.onBluetoothDeviceFound(found)
},
fail(err) {
console.log('搜索失败')
console.error(err)
}
})
}
// 【3】找到新设备就触发该方法
function found(res) {
console.log(res)
blueDeviceList.value.push(res.devices[0])
}
// 蓝牙设备的id
const deviceId = ref('')
// 【4】连接设备
function connect(data) {
console.log(data)
deviceId.value = data.deviceId // 将获取到的设备ID存起来
uni.createBLEConnection({
deviceId: deviceId.value,
success(res) {
console.log('连接成功')
console.log(res)
// 停止搜索
stopDiscovery()
uni.showToast({
title: '连接成功'
})
},
fail(err) {
console.log('连接失败')
console.error(err)
uni.showToast({
title: '连接成功',
icon: 'error'
})
}
})
}
// 【5】停止搜索
function stopDiscovery() {
uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log('停止成功')
console.log(res)
},
fail(err) {
console.log('停止失败')
console.error(err)
}
})
}
// 【6】获取服务
function getServices() {
// 如果是自动链接的话,uni.getBLEDeviceServices方法建议使用setTimeout延迟1秒后再执行
uni.getBLEDeviceServices({
deviceId: deviceId.value,
success(res) {
console.log(res) // 可以在res里判断有没有硬件佬给你的服务
uni.showToast({
title: '获取服务成功'
})
},
fail(err) {
console.error(err)
uni.showToast({
title: '获取服务失败',
icon: 'error'
})
}
})
}
// 硬件提供的服务id,开发中需要问硬件佬获取该id
const serviceId = ref('0000FFE0-0000-1000-8000-00805F9B34FB')
// 【7】获取特征值
function getCharacteristics() {
// 如果是自动链接的话,uni.getBLEDeviceCharacteristics方法建议使用setTimeout延迟1秒后再执行
uni.getBLEDeviceCharacteristics({
deviceId: deviceId.value,
serviceId: serviceId.value,
success(res) {
console.log(res) // 可以在此判断特征值是否支持读写等操作,特征值其实也需要提前向硬件佬索取的
uni.showToast({
title: '获取特征值成功'
})
},
fail(err) {
console.error(err)
uni.showToast({
title: '获取特征值失败',
icon: 'error'
})
}
})
}
const characteristicId = ref('0000FFE1-0000-1000-8000-00805F9B34FB')
// 【8】开启消息监听
function notify() {
uni.notifyBLECharacteristicValueChange({
deviceId: deviceId.value, // 设备id
serviceId: serviceId.value, // 监听指定的服务
characteristicId: characteristicId.value, // 监听对应的特征值
success(res) {
console.log(res)
listenValueChange()
uni.showToast({
title: '已开启监听'
})
},
fail(err) {
console.error(err)
uni.showToast({
title: '监听失败',
icon: 'error'
})
}
})
}
// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('')
}
// 将16进制的内容转成我们看得懂的字符串内容
function hexCharCodeToStr(hexCharCodeStr) {
var trimedStr = hexCharCodeStr.trim();
var rawStr = trimedStr.substr(0, 2).toLowerCase() === "0x" ? trimedStr.substr(2) : trimedStr;
var len = rawStr.length;
if (len % 2 !== 0) {
alert("存在非法字符!");
return "";
}
var curCharCode;
var resultStr = [];
for (var i = 0; i < len; i = i + 2) {
curCharCode = parseInt(rawStr.substr(i, 2), 16);
resultStr.push(String.fromCharCode(curCharCode));
}
return resultStr.join("");
}
// 监听到的内容
const message = ref('')
const messageHex = ref('') // 十六进制
// 【9】监听消息变化
function listenValueChange() {
uni.onBLECharacteristicValueChange(res => {
console.log(res)
let resHex = ab2hex(res.value)
console.log(resHex)
messageHex.value = resHex
let result = hexCharCodeToStr(resHex)
console.log(String(result))
message.value = String(result)
})
}
// 【10】发送数据
function send() {
// 向蓝牙设备发送一个0x00的16进制数据
let msg = 'hello'
const buffer = new ArrayBuffer(msg.length)
const dataView = new DataView(buffer)
// dataView.setUint8(0, 0)
for (var i = 0; i < msg.length; i++) {
dataView.setUint8(i, msg.charAt(i).charCodeAt())
}
uni.writeBLECharacteristicValue({
deviceId: deviceId.value,
serviceId: serviceId.value,
characteristicId: characteristicId.value,
value: buffer,
success(res) {
console.log('writeBLECharacteristicValue success', res.errMsg)
uni.showToast({
title: 'write指令发送成功'
})
},
fail(err) {
console.error(err)
uni.showToast({
title: 'write指令发送失败',
icon: 'error'
})
}
})
}
// 【11】读取数据
function read() {
uni.readBLECharacteristicValue({
deviceId: deviceId.value,
serviceId: serviceId.value,
characteristicId: characteristicId.value,
success(res) {
console.log(res)
uni.showToast({
title: 'read指令发送成功'
})
},
fail(err) {
console.error(err)
uni.showToast({
title: 'read指令发送失败',
icon: 'error'
})
}
})
}
</script>
<style>
.box {
width: 98%;
height: 400rpx;
box-sizing: border-box;
margin: 0 auto 20rpx;
border: 2px solid dodgerblue;
}
.item {
box-sizing: border-box;
padding: 10rpx;
border-bottom: 1px solid #ccc;
}
button {
margin-bottom: 20rpx;
}
.msg_x {
border: 2px solid seagreen;
width: 98%;
margin: 10rpx auto;
box-sizing: border-box;
padding: 20rpx;
}
.msg_x .msg_txt {
margin-bottom: 20rpx;
}
</style>
作者:德育处主任
链接:https://juejin.cn/post/7093171532318375950
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
本文链接: https://erik.xyz/2025/08/25/luetoot-app/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!