【蓝牙分包传输】飞书小程序实现通过蓝牙传输文件给其他设备【TS】

场景描述

在飞书小程序中实现下载文件并把文件通过蓝牙传输给其他设备【分包传输】。

  1. 下载文件(这里用的是网上找的一个测试下载url) ;
  2. 文件是ArrayBuffer格式,并把文件缓存到飞书小程序的Storage;

    在飞书中下载文件见:https://hengqu4.github.io/2023/07/18/feishu-download-file/

  3. 调用飞书API,扫描附近的蓝牙设备;
  4. 选择目标设备并连接配对(如果连接需要配对码,需要用户自己知道该设备的配对码);
  5. 成功配对后,开始进行蓝牙传输;由于飞书API的限制,每次只能传输20字节,所以我们要进行分包传输(类似于前端大文件切片上传)。
    传输过程中要进行crc16循环冗余校验

本篇文章重点写的是:
第4步【如何配对蓝牙】
第5步【如何实现蓝牙分包传输,以及如何实现crc16循环冗余校验

飞书蓝牙相关API

飞书官网【蓝牙接入开发流程】:https://open.feishu.cn/document/client-docs/gadget/-web-app-api/device/bluetooth/bluetooth-api-guide

封装蓝牙API

/utils/bluetooth.ts

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
export const getBluetoothAdapterState = () => {
return new Promise<any>((resolve, reject) => {
tt.getBluetoothAdapterState({
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const openBluetoothAdapter = () => {
return new Promise<any>((resolve, reject) => {
tt.openBluetoothAdapter({
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const closeBluetoothAdapter = () => {
return new Promise<any>((resolve, reject) => {
tt.closeBluetoothAdapter({
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const startBluetoothDevicesDiscovery = (options?: { allowDuplicatesKey: boolean }) => {
return new Promise<any>((resolve, reject) => {
tt.startBluetoothDevicesDiscovery({
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) },
...options
})
})
}

export const stopBluetoothDevicesDiscovery = () => {
return new Promise<any>((resolve, reject) => {
tt.stopBluetoothDevicesDiscovery({
success: (res: any) => { resolve(res) },
fail: (err: any) => { resolve(err) }
})
})
}

export const getConnectedBluetoothDevices = (...services: string[]) => {
return new Promise<any>((resolve, reject) => {
tt.getConnectedBluetoothDevices({
services,
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const connectBLEDevice = (deviceId: string) => {
return new Promise<any>((resolve, reject) => {
tt.connectBLEDevice({
deviceId,
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const disconnectBLEDevice = (deviceId: string) => {
return new Promise<any>((resolve, reject) => {
tt.disconnectBLEDevice({
deviceId,
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const getBLEDeviceServices = (deviceId: string) => {
console.log('getBLEDeviceServices====')
return new Promise<any>((resolve, reject) => {
tt.getBLEDeviceServices({
deviceId,
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) }
})
})
}

export const getBLEDeviceCharacteristics = (options: { deviceId: string, serviceId: string }) => {
return new Promise<any>((resolve, reject) => {
tt.getBLEDeviceCharacteristics({
success: (res: any) => {
console.log('获取特征值,success')
resolve(res)
},
fail: (err: any) => {
console.log('获取特征值,fail')
reject(err)
},
...options
})
})
}

export const notifyBLECharacteristicValueChange = (
options: {
deviceId: string,
serviceId: string,
characteristicId: string,
state?: boolean
}) => {
return new Promise<any>((resolve, reject) => {
tt.notifyBLECharacteristicValueChange({
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) },
...options
})
})
}

export const writeBLECharacteristicValue = (
options: {
deviceId: string,
serviceId: string,
characteristicId: string,
value: string
}) => {
return new Promise<any>((resolve, reject) => {
tt.writeBLECharacteristicValue({
success: (res: any) => { resolve(res) },
fail: (err: any) => { reject(err) },
...options
})
})
}

export const showToast = (options: {
title?: string,
icon?: 'success' | 'loading' | 'none' | 'info' | 'error' | 'warning',
duration?: number,
mask?: boolean
}) => {
tt.showToast({
...options
})
}

封装循环校验crc和hex字符串转换等方法

/utils/deviceUtil.ts

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
// CRC-CCITT多项式 16位的CRC校验算法
// 方法1.将存有数据的字节数组进行逐位计算,求得字节形式的CRC
// 方法2.字节查表法
// 这里用的是方法2

// CRC16_CCITT 0x1021
// 字节查表法:CRC 字节余式表
export const crcTab16 = new Uint16Array([
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0])

export const crc16 = (data: Uint8Array) => {
// uint16_t crc
let crc = 0
for (const b of data) {
crc = ((crc << 8) & 0xffff) ^ crcTab16[((crc >> 8) ^ b) & 0x00ff]
}
return crc
}

export const arrayBuffer2Hex = (buffer: ArrayBuffer) => {
const bitArr = new Uint8Array(buffer)
const hexArr: string[] = []
bitArr.forEach((bit) => {
const hex = '00' + String(bit).slice(-2)
hexArr.push(hex)
})
return hexArr.join('')
}

export function buf2hex(buffer: ArrayBuffer): string {
return [...new Uint8Array(buffer)]
.map(x => x.toString(16).padStart(2, '0'))
.join('')
}

export function hex2buf(hex: string): ArrayBuffer {
const bytes:number[] = []
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substring(c, 2), 16))
}
return new Uint8Array(bytes).buffer
}

export function str2buf(str: string): ArrayBuffer {
const buf = new ArrayBuffer(str.length) // 2 bytes for each char
const byteArr = new Uint8Array(buf)
for (let i = 0; i < str.length; i++) {
byteArr[i] = str.charCodeAt(i)
}
return buf
}

export function buf2str(arraybuffer: ArrayBuffer) {
const chunkSize = 512

const byteArr = new Uint8Array(arraybuffer)
const chunkNum = Math.ceil(byteArr.length / chunkSize)
let chunkStart = 0

let outStr = ''
for (let i = 0; i < chunkNum; i++) {
const chunkEnd = Math.min(byteArr.length, chunkStart + chunkSize)
const chunkedStr = String.fromCharCode(...byteArr.slice(chunkStart, chunkEnd))
outStr += chunkedStr
chunkStart += chunkSize
}
return outStr
}

封装分包传输ChunkedArrayBuffer

/utils/ChunkedArrayBuffer.ts

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
export class ChunkedArrayBuffer {
constructor(arraybuffer: ArrayBuffer) {
this.byteArr = new Uint8Array(arraybuffer)
this.chunkNum = Math.ceil(this.byteArr.length / this.chunkSize)
}

private byteArr!: Uint8Array

private chunkSize = 128

private chunkNum!: number

private chunkStart = 0

private chunkIndex = 0

private isPaused = false

get getChunkIndex() {
return this.chunkIndex
}

beforeStart?: (byteLength: number, chunkSize: number, chunkNum: number) => void

extraFunc?: (arraybuffer: ArrayBuffer, chunkIndex: number, chunkNum: number) => void

afterEnd?: (byteLength: number, chunkIndex: number, chunkNum: number) => void

/**
* 读取下一个分片
*/
readNextChunk = (): ArrayBuffer | undefined => {
console.log('发送下一帧 chunkIndex:', this.chunkIndex, 'chunkStart:', this.chunkStart, 'isPaused:', this.isPaused)
if (!this.isPaused) {
if (this.chunkStart < this.byteArr.length) {
if (this.chunkIndex < 0xfffe) this.chunkIndex += 1
else this.chunkIndex = 0
this.chunkStart += this.chunkSize
return this.readChunkedArrayBuffer()
}
}
console.warn(ChunkedArrayBuffer.name, this.readNextChunk.name, 'call next chunk paused')
}

/**
* 读取当前分片
*/
readCurrentChunk = (): ArrayBuffer | undefined => {
return this.readChunkedArrayBuffer()
}

pause = () => {
if (!this.isPaused) this.isPaused = true
}

resume = () => {
if (this.isPaused) this.isPaused = false
}

// 调用设置的extraFunc方法
// bluetooth/index.ts里设置了该方法,在其中调用了tt的接口向设备写入文件数据
private readChunkedArrayBuffer = (): ArrayBuffer | undefined => {
if (this.chunkStart < this.byteArr.length) {
const chunkEnd = Math.min(this.byteArr.length, this.chunkStart + this.chunkSize)
const chunk = this.byteArr.slice(this.chunkStart, chunkEnd)
// eslint-disable-next-line no-useless-call
if (this.extraFunc) this.extraFunc.apply(this, [chunk, this.chunkIndex, this.chunkNum])
return chunk
} else {
console.info(ChunkedArrayBuffer.name, this.readChunkedArrayBuffer.name, 'chunk arraybuffer finished')
// eslint-disable-next-line no-useless-call
if (this.afterEnd) this.afterEnd.apply(this, [this.byteArr.length, this.chunkIndex, this.chunkNum])
}
}
}

封装传输帧FrameUtil

/utils/FrameUtil.ts
传输帧FrameUtil

定义了分包的每个包结构是什么

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
import { crc16 } from '@/utils/deviceUtil'

export const KEY_BIN_STORAGE = 'test-file'

export class FrameUtil {
public static genFrameArrayBuffer = (
frameTypeId: number,
dataBuffer: ArrayBuffer = new ArrayBuffer(0),
frameId = 1
): ArrayBuffer => {
// 预声明常量

// 这些ID的设置与所使用的蓝牙通信协议相关,请根据自己的协议改动
// 通常协议必备项目:帧数ID、数据包长度、数据包、CRC校验


// 0x16进制,以0x开始的数据表示16进制。
// 计算机中每位的权为16,即(16进制)10 = (10进制)1×16。
const frameHead = 0x3d5e
const frameProductId = 0x09
// 计算crc(循环冗余校验)
const frameLen = 2 + 2 + 1 + 1 + 2 + dataBuffer.byteLength + 2
const frameBuffer = new ArrayBuffer(frameLen)
const dataView = new DataView(frameBuffer)
// 起始位置以byte为计数的指定偏移量 0 处储存一个16-bit数(无符号短整型).
dataView.setUint16(0, frameHead)

// 支持从一个消息序列的最高位开始传送的那伙人叫做Big-Endians,
// 支持从最低位开始传送的相对应地叫做Little-Endians

// setUnit:(缓冲区行中列的索引, 赋给缓冲区列的值, True/False)
// true: littleEndian
// false/undefined: bigEndian

// Little-endian
// 偏移量 2 byte处储存一个16-bit数。frameId:传输的第几帧
dataView.setUint16(2, frameId, true)
// 偏移量 4 byte处储存一个8-bit数 [4,5)
dataView.setUint8(4, frameProductId)
// 偏移量 5 byte处储存一个8-bit数 [5,6)
dataView.setUint8(5, frameTypeId)
// Little-endian
// 偏移量 6 byte处储存一个16-bit数 [6,8)
dataView.setUint16(6, dataBuffer.byteLength, true)
// 将dataBuffer写入数据帧
const data8Arr = new Uint8Array(dataBuffer)

for (let i = 0; i < data8Arr.length; i++) {
dataView.setUint8(8 + i, data8Arr[i])
}
const crc8Arr = new Uint8Array(frameBuffer).slice(0, frameLen - 2)
const crc = crc16(crc8Arr)
// 写入crc
// littleEndian
dataView.setUint16(frameLen - 2, crc, true)

return frameBuffer
}
}

页面中实际应用

index.mpx

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
<template>
<view>
<view>
<button bindtap="scanDevices" style="background-color: #55efc4">开始扫描蓝牙设备</button>
<button bindtap="stopScanDevices" style="background-color: #ff7675">停止扫描蓝牙设备</button>
<button bindtap="downloadFile" style="background-color: #74b9ff">下载用于传输的文件</button>
</view>

<view wx:if="{{timer}}">扫描中...</view>

<view wx:if="{{connected}}">
<view>
<text>已连接到 {{ name }}</text>
<view class="operation">
<button size="mini" bindtap="disconnectDevice">断开连接</button>
<button wx:if="{{canWrite}}" size="mini" bindtap="startSendFile">开始传文件</button>
<button wx:if="{{canWrite}}" size="mini" bindtap="sendCurrentFrame">发送当前帧</button>
<button wx:if="{{canWrite}}" size="mini" bindtap="sendNextFrame">发送下一帧</button>
</view>
</view>
</view>

<view>已搜索到的设备数量:{{ deviceList.length }}</view>
<view wx:for="{{ deviceList }}" wx:for-index="index" wx:key="deviceId">
<view bindtap="connectDevice(item)" style="padding: 10px 0; border: 1px solid #b2bec3">
<view style="font-size: 1.2rem">{{ item.name }} </view>
<view style="font-size: 0.8rem"> [UUID] {{ item.deviceId }}</view>
<view style="font-size: 0.8rem">[信号强度] {{ item.RSSI }}dBm</view>
</view>
</view>
</view>
</template>

<script lang="ts" src="./index.ts">
</script>

index.ts

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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import mpx, { createPage } from "@mpxjs/core";
import { buf2hex } from "@/utils/deviceUtil";
import { ChunkedArrayBuffer } from "@/utils/ChunkedArrayBuffer";
import { FrameUtil } from "@/utils/FrameUtil";
import {
openBluetoothAdapter,
closeBluetoothAdapter,
showToast,
stopBluetoothDevicesDiscovery,
connectBLEDevice,
getBLEDeviceServices,
getBLEDeviceCharacteristics,
notifyBLECharacteristicValueChange,
disconnectBLEDevice,
writeBLECharacteristicValue,
getBluetoothAdapterState,
} from "@/utils/bluetooth";

const USE_THIS_SERVICE_UUID = "0000FF03-0000-1000-8000-00805F9B34FB";
const WRITABLE_CHARACTERISTIC_UUID = "0000FF02-0000-1000-8000-00805F9B34FB";
const NOTIFYABLE_CHARACTERISTIC_UUID = "0000FF01-0000-1000-8000-00805F9B34FB";

createPage({
data: {
timer: null as any,
deviceList: [],
// deviceList: [
// { deviceId: "C0:2F:T1:KB:MD:6P", name: "111", RSSI: -43 },
// { deviceId: "H2:G3:D5:K2:H1:E8", name: "222", RSSI: -21 },
// ],
deviceId: "",
name: "",

connected: false,
canWrite: false,
discoveryStarted: false,
selectedDeviceId: "",
selectedServiceId: "",
selectedCharacteristicId: "",
chunkedArrayBuffer: new ChunkedArrayBuffer(new ArrayBuffer(0)),
progress: "0",
},
computed: {
getDeviceList() {
return this.deviceList;
},
},
onReady() {
console.log("bluetooth onReady");
},

// 下载用于传输的文件
// 下载文件,类型为ArrayBuffer,存到Storage
onDownloadStorage() {
const url =
"https://freetestdata.com/wp-content/uploads/2021/09/Free_Test_Data_200KB_CSV-1.csv";
console.log("下载用于传输的文件===");
// 类型为arraybuffer
tt.request({
url,
responseType: "arraybuffer",
success: (res) => {
if (res.data) {
console.log("download file success", res);
const { data: arraybuffer } = res;
const byteArr = new Uint8Array(arraybuffer);
const arr = Array.from(byteArr);
// 保存为Array
tt.setStorage({
key: "test-file",
data: arr,
success: (res) => {
console.log("setStorage success", res);
},
fail: (res) => {
console.error("setStorage fail", res);
},
});
}
},
fail: (err) => {
console.error("download file fail", err);
},
});
},

async onLoad() {
// 连接蓝牙
try {
// 初始化蓝牙模块
await openBluetoothAdapter();
const { discovering } = await getBluetoothAdapterState();
if (discovering) this.discoveryStarted = true;
} catch (err: any) {
console.error(err);
const { errMsg } = err;
showToast({ title: errMsg, icon: "error" });
}
// 监听蓝牙适配器状态变化事件
tt.onBluetoothAdapterStateChange((res: any) => {
const { available, discovering } = res;
console.info(available, discovering);
if (!available) showToast({ title: "蓝牙不可用", icon: "error" });
});
},
onUnload() {
// 停止搜寻附近的蓝牙外围设备。
stopBluetoothDevicesDiscovery();
// 关闭蓝牙模块。
closeBluetoothAdapter();
// 取消监听蓝牙适配器状态变化事件
tt.offBluetoothAdapterStateChange((args: any[]) => {});
// 取消监听寻找到新设备的事件
tt.offBluetoothDeviceFound((args: any[]) => {});
// 取消监听蓝牙低功耗设备的特征值变化事件
tt.offBLECharacteristicValueChange((args: any[]) => {});
},
methods: {
// 获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备。
getBluetoothDevices() {
console.log("所有已发现的蓝牙设备", this.deviceList);
tt.getBluetoothDevices({
success: (res) => {
const devices = res.devices;
this.deviceList = devices;
},
fail: (res) => {
console.log(`getBluetoothDevices fail: ${JSON.stringify(res)}`);
},
});
},

// 刷新已搜索到的设备
async refreshScannedDevice() {
console.log("连接设备 start");
tt.showToast({
title: "刷新设备列表",
duration: 3000,
icon: "success",
mask: false,
});
this.timer = setInterval(() => {
this.getBluetoothDevices();
}, 3000);
},

// 开始扫描蓝牙设备
async scanDevices() {
await tt.startBluetoothDevicesDiscovery({
services: [],
allowDuplicatesKey: true,
interval: 0,
success: (res) => {
console.log("开始搜寻设备", JSON.stringify(res));
tt.showToast({
title: "开始搜寻设备",
duration: 3000,
icon: "success",
mask: false,
});
// 刷新 获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备。
this.refreshScannedDevice();
},
fail: (res) => {
console.log(
`startBluetoothDevicesDiscovery fail: ${JSON.stringify(res)}`
);
},
});
},

// 停止扫描蓝牙设备
async stopScanDevices() {
await tt.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log("停止扫描设备", JSON.stringify(res));
tt.showToast({
title: "停止扫描设备",
duration: 3000,
icon: "success",
mask: false,
});
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
fail: (res) => {
console.log(
`stopBluetoothDevicesDiscovery fail: ${JSON.stringify(res)}`
);
},
});
},

// 点击某个设备,选择连接该设备
async connectDevice(item) {
console.log("连接设备", item);
// 找到某个id的服务->找到某个id的特征值->利用write特征值去写入数据
const device = {
deviceId: item.deviceId,
name: item.name,
};

try {
await connectBLEDevice(device.deviceId);
await stopBluetoothDevicesDiscovery();
console.info("try connected to ", device.name);
tt.showToast({ title: device.name, icon: "success" });
this.name = device.name;
this.selectedDeviceId = device.deviceId;
this.connected = true;

// 获取设备的低功耗蓝牙的服务
const { services } = await getBLEDeviceServices(this.selectedDeviceId);
console.log(
"getBLEDeviceServices",
services instanceof Array && services.length,
services
);

if (services instanceof Array && services.length) {
for (const service of services) {
// 找id为 xxxx FF03 -xxxx ... 的service
console.log("遍历service,找FF03。", service);
if (service.serviceId.toUpperCase() !== USE_THIS_SERVICE_UUID) {
continue;
}
this.selectedServiceId = service.serviceId;
const { characteristics } = await getBLEDeviceCharacteristics({
deviceId: this.selectedDeviceId,
serviceId: this.selectedServiceId,
});
console.log(
"characteristics",
characteristics,
characteristics instanceof Array && characteristics.length
);
// 找到FF01的特征值:通知notify
// 找到FF02的特征值:写入write -> 利用这个特征值向设备写入数据
if (characteristics instanceof Array && characteristics.length) {
for (const characteristic of characteristics) {
console.log(
"遍历characteristic,找FF02和FF01。",
characteristic
);
if (
characteristic.properties.notify &&
characteristic.characteristicId.toUpperCase() ===
NOTIFYABLE_CHARACTERISTIC_UUID
) {
await notifyBLECharacteristicValueChange({
deviceId: this.selectedDeviceId,
serviceId: this.selectedServiceId,
characteristicId: characteristic.characteristicId,
state: true,
});
console.log("notify", characteristic.characteristicId);
}
if (
characteristic.properties.write ||
characteristic.characteristicId.toUpperCase() ===
WRITABLE_CHARACTERISTIC_UUID
) {
this.canWrite = true;
this.selectedCharacteristicId =
characteristic.characteristicId;
console.log("can write", characteristic.characteristicId);
}
}
}
}
}
} catch (err: any) {
console.error(err);
const { errMsg } = err;
showToast({ title: errMsg, icon: "error" });
}
},
},
// 断开已连接的设备
async disconnectDevice() {
try {
await disconnectBLEDevice(this.selectedDeviceId);
// this.chs = []
this.connected = false;
this.canWrite = false;
} catch (err: any) {
console.error(err);
}
},

// 开始发送文件
async startSendFile() {
try {
console.log("开始发送文件==== 'test-file'");
// 从storage中读取文件二进制数组 8bit
const binArr = tt.getStorageSync("test-file");
console.log("binArr", binArr);
if (binArr && binArr instanceof Array) {
// 文件:把数组转为buffer类型
const buffer = new Uint8Array(binArr).buffer;
// 实例化
this.chunkedArrayBuffer = new ChunkedArrayBuffer(buffer);

// 设置extraFunc函数
this.chunkedArrayBuffer.extraFunc = (
arraybuffer: any,
index: number,
totalNum: number
) => {
// 0x48为 FrameTypeID
// 这些ID的设置与所使用的蓝牙通信协议相关,请根据自己的协议改动
// 通常协议必备项目:帧数ID、数据包长度、数据包、CRC校验
const frameBuffer = FrameUtil.genFrameArrayBuffer(
0x48,
arraybuffer,
index + 1
);
// 更新上传进度条
this.progress = (((index + 1) * 100) / totalNum).toFixed(2);
// buffer to hex string,蓝牙传输数据规定要hex编码
const hex = buf2hex(frameBuffer);
console.log("向设备写入文件的某段切片", hex.length, " value:", hex);
// 向设备写入文件的某段切片
writeBLECharacteristicValue({
deviceId: this.selectedDeviceId,
serviceId: this.selectedServiceId,
characteristicId: this.selectedCharacteristicId,
value: hex,
})
.then((res: any) => console.info("写入成功", res))
.catch((err: any) => {
console.error("写入失败", err);
});
};
this.chunkedArrayBuffer.readCurrentChunk();
} else {
console.warn("invalid file", binArr);
}
} catch (err: any) {
console.error("开始发送文件 fail", err);
}
},
// 发送当前帧
sendCurrentFrame() {
this.chunkedArrayBuffer.readCurrentChunk();
},
// 发送下一帧
sendNextFrame() {
this.chunkedArrayBuffer.readNextChunk();
},
});