背景

使用hybrid框架apicloud,开发物联网领域的蓝牙交互功能时,使用了apicloud的官方ble插件,本文记录了使用逻辑和一些注意点。

可用于第三方蓝牙设备交互,必须要支持蓝牙 4.0。
iOS上:硬件至少是 iphone4s,系统至少是 iOS6。
android上:系统版本至少是 android4.3。

蓝牙 4.0 以低功耗著称,一般也叫 BLE(BluetoothLowEnergy)。目前应用比较多的案例:运动手坏、嵌入式设备、智能家居

蓝牙通讯原理

概述

​ 在蓝牙通讯中有两个主要的部分,Central 和 Peripheral,有一点类似Client Server。
一般手机是客户端, 设备(比如手环)是服务器,因为是手机去连接手环这个服务器。以下称两个部分为手机和设备。

服务和特征

​ 设备可以广播数据、提供服务,手机可以扫描附近的设备,一旦建立连接,就可以交换数据。
​ 特征是与外界交互的最小单位。蓝牙4.0设备通过服务(Service)、特征(Characteristics)和描述符(Descriptor)来形容自己,同一台设备可能包含一个或多个服务,每个服务下面又包含若干个特征,每个特征下面有包含若干个描述符(Descriptor)。比如某台蓝牙4.0设备,用特征A来描述设备信息、用特征B和描述符b来收发数据等。而每个服务、特征和描述符都是用 UUID 来区分和标识的。

封装

这次封装了蓝牙(我们公司的设备)使用的流程,并暴露出生命周期,供处理数据、显示页面等操作,小程序也是同样思路。以下是伪代码,完整代码见git(私有仓库)。
const $ble = {

}

// 1、检测手机蓝牙状态,权限

// 位置, Android 6.0以后需要定位权限,否则无法正常使用

1
2
3
4
5
6
7
8
9
 async init() {
if (isAndroid) {
const { granted } = confirmPermission('location')
if (!granted) {
dispatchEvent('bleStatus', { type: 'location', status: false })
return
}
}
}

​ // 安卓12及以上和鸿蒙(非next)需要打开‘附近设备’权限

1
2
3
4
5
6
7
if (AndroidVersion >= 12) {
const { granted } = confirmPermission('ble-scan')
if (!granted) {
dispatchEvent('bleStatus', { type: 'ble-scan', status: false })
return
}
}

​ // 蓝牙

1
2
3
4
5
6
7
initManager({ "single": true }).then(res => {
if (res.state === "poweredOn") dispatchEvent('bleStatus', { status: true })
else dispatchEvent('bleStatus', { type: 'ble', status: false })
}).catch(() => {
dispatchEvent('bleStatus', { type: 'ble', status: false })
})
}

// 2、监听生命周期的各个节点,和处理方法

1
2
3
4
5
addBleListener(type, handler) {
console.log('监听到', type)
if (!(type in $ble.data.handlers)) $ble.data.handlers[type] = []
$ble.data.handlers[type].push(handler)
},

// 3、发布事件两个参数(事件名,参数)

1
2
3
4
5
6
7
dispatchEvent(type, ...params) {
console.log('触发了', type)
if (!(type in $ble.data.handlers)) return
$ble.data.handlers[type].forEach(handler => {
handler(...params)
})
},

// 4、监听蓝牙是否一直连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
listenConnected() {
$ble.utils.clearTimer($ble.data.listenConnectTimer, 'interval')
$ble.data.listenConnectTimer = setInterval(function () {
$ble.getPrivacy().isConnected({
peripheralUUID: $ble.data.uuid
}, function (ret) {
console.log('--------------listenconnected', ret.status)
if (!ret.status) {
$ble.utils.clearTimer($ble.data.listenConnectTimer, 'interval')
$ble.data.lastStatus = $ble.data.checkStatus
$ble.dispatchEvent('disconnected')
$ble.reset()
}
})
}, 2000)
},

// 5、连接蓝牙

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
// 连接整体控制  
connectBle() {
$ble.connect()
$ble.utils.clearTimer($ble.data.connectTimer, 'interval')
$ble.data.connectTimer = setInterval(() => {
$ble.connect()
}, 3500)
$ble.utils.clearTimer($ble.data.connectOutTimer)
$ble.data.connectOutTimer = setTimeout(() => {
if (!$ble.data.isConnected) {
$ble.data.isConnectOut = true
$ble.bleCheckFail('连接超时', '14')
}
}, 30 * 1000)
},

// 调用连接
connect() {
$ble.data.connectTimes++
if ($ble.data.isConnected) return
console.log('第' + $ble.data.connectTimes + '次连接', $ble.data.uuid)
$ble.getPrivacy().connect({
peripheralUUID: $ble.data.uuid
}, function (ret) {
if (ret.status) {
if ($ble.data.isConnectOut) { // 超时之后连接成功
return false
}
$ble.dispatchEvent('bleProcessNode', { name: 'connecting', status: 'success' })
$ble.dispatchEvent('connectSuccess')
$ble.data.isConnected = true
$ble.utils.clearTimer($ble.data.connectOutTimer)
$ble.listenConnected()
// 连接成功后获取服务
$ble.getService()
$ble.utils.clearTimer($ble.data.connectTimer, 'interval')
$ble.data.connectTimes = 0
}
})
if ($ble.data.connectTimes > 4) {
$ble.utils.clearTimer($ble.data.connectTimer, 'interval')
$ble.data.connectTimes = 0
}
}

// 获取服务
// 4.获取服务

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
getService() {
console.log('当前状态-------------->', 'serviceGetting')
$ble.data.checkStatus = 'serviceGetting'
$ble.utils.clearTimer($ble.data.getServiceTimer)
$ble.data.getServiceTimer = setTimeout(() => {
if (!$ble.data.notifyServiceId) {
$ble.bleCheckFail('获取服务超时', '29')
}
}, 5 * 1000)
$ble.getPrivacy().discoverService({
peripheralUUID: $ble.data.uuid
}, function (ret, err) {
if (ret.status) {
let service = ret["services"]
$ble.data.notifyServiceId = $ble.data.writeServiceId = ''
for (let i = 0; i < service.length; i++) {
let UUID_slice = service[i].length > 4 ? service[i].slice(4, 8) : service[i] //截取4到8位
/* 判断是否是我们需要的服务*/
if (UUID_slice.toUpperCase() == $ble.data.notify) $ble.data.notifyServiceId = service[i]
if (UUID_slice.toUpperCase() == $ble.data.write) $ble.data.writeServiceId = service[i]
}
if ($ble.data.notifyServiceId) {
$ble.dispatchEvent('getServiceSuccess')
// 获取特征
$ble.getNotifyChara()
}
} else {
$ble.bleCheckFail('获取服务失败', '21')
}
})
},

// 5.获取特征

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
getNotifyChara() {
console.log('当前状态-------------->', 'charaGetting')
$ble.data.checkStatus = 'charaGetting'
$ble.utils.clearTimer($ble.data.getCharaTimer)
$ble.data.getCharaTimer = setTimeout(() => {
if (!$ble.data.notifyCharacterId) {
$ble.bleCheckFail('获取特征超时')
}
}, 5 * 1000)
$ble.getPrivacy().discoverCharacteristics({
serviceUUID: $ble.data.notifyServiceId,
peripheralUUID: $ble.data.uuid
}, function (ret) {
if (ret.status) {
$ble.dispatchEvent('getNotifyCharaSuccess')
let characteristic = ret["characteristics"]
$ble.data.notifyCharacterId = characteristic[1] ? characteristic[1].uuid : ''
$ble.data.writeCharacterId = characteristic[0] ? characteristic[0].uuid : ''
if ($ble.data.notifyCharacterId && (!$ble.data.ywBleTab || $ble.data.ywBleTab==='reading')) {
$ble.startRegister()
}
} else {
$ble.bleCheckFail('获取特征失败', '31')
}
});
},

// 6.注册、配对、数据交互, 都是获取数据包,发送数据包的过程

1
2
3
startRegister() {},
pair() {},
read() {},

// 7.开启监听设备响应的数据包

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
notify(ret) {
if (ret.status) {
var msg = ret["characteristic"]["value"]
if (msg != $ble.data.not) {
$ble.data.isNotified = true
$ble.utils.clearTimer($ble.data.sendTimer)
// 数据是否是我们想要的
let isVaild = $ble.data.deviceType=='2' ? true : $ble.utils.vaildReading(msg)
if (isVaild) {
if ($ble.data.showLog) $ble.dispatchEvent('rspLog', { msg, status: true, isVaild: true })
$ble.data.sendTimes = 0
console.log('--------------监听回调--------------', $ble.data.checkStatus)
// 非多k的直接上传
ajax("bleUpload", { device: $ble.data.systemBleId, "bytes": msg }, null, function (res) {
if($ble.data.checkStatus === "register" && res.type === 'readRsp') {
// 注册-----------------------------------------------------------------
handle()
}else if($ble.data.checkStatus === "assign" && (res.type === 'randomRsp' || res.type === 'pairRsp')) {
// 配对-----------------------------------------------------------------
handle()
}else if ($ble.data.checkStatus == "reading" && res.type === 'reading') {
handle()
}
}, function (desc) {
$ble.data.packList = []
console.log('uploadble失败')
const d = desc || '失败!'
$ble.bleCheckFail(d, $ble.data.errCode[$ble.data.checkStatus])
}, function (desc) {
$ble.data.packList = []
console.log('uploadble失败2')
const d = desc || '失败!'
$ble.bleCheckFail(d, $ble.data.errCode[$ble.data.checkStatus])
}, 1)
} else {
if ($ble.data.showLog) $ble.dispatchEvent('rspLog', { msg, status: true, isVaild: false })
$ble.reSend()
}
}
} else {
$ble.reSend()
}
},

// 8.发送数据包

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
reSend() {
$ble.data.isNotified = false
$ble.data.sendTimes++
console.log('sendTimes', $ble.data.sendTimes)
if ($ble.data.sendTimes > 3) {
$ble.utils.clearTimer($ble.data.sendTimer)
$ble.bleCheckFail('失败')
return
}
if (!$ble.data.isConnected) {
$ble.utils.clearTimer($ble.data.sendTimer)
$ble.bleCheckFail('失败')
return
}
const timeout = (wait = 8000) => {
$ble.utils.clearTimer($ble.data.sendTimer)
$ble.data.sendTimer = setTimeout(() => {
if (!$ble.data.isNotified) {
// 表具未反馈指令!
const isSwitch = $ble.data.sendTimes > 2 ? true : false
$ble.dispatchEvent('rspLog', { msg: '', status: false, isSwitch: isSwitch, name: $ble.data.checkStatus })
$ble.reSend()
}
}, wait)
}
if ($ble.data.checkStatus == "reading") {
timeout()
$ble.sendMsg($ble.data.readingPack)
} else {
timeout()
$ble.sendMsg($ble.data.sendPack)
}
},

// 9.重写关闭窗口的页面,由于关闭窗口的时候无法监听,从而去关闭蓝牙设备的连接,所以重写api.closeWin,实现最大限度的控制蓝牙状态

1
2
3
4
5
6
7
8
9
10
11
reWriteClose() {
// 左滑返回处理
api.addEventListener({
name: 'keyback'
}, function (ret, err) {
cn++
if (cn == 1)
toast("再滑一次退出蓝牙")
else if (cn == 2)
api.closeWin()
})
1
2
3
4
5
6
// 直接关闭重写, 关闭时将蓝牙状态重置
api.closeWin = () => {
loading()
await handleBle()
close()
}

}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ble.init()

$ble.addBleListener('bleStatus', res => {

res === 'success' && alert('蓝牙正常使用')

res === 'fail' && alert('蓝牙权限未开启')

})

$ble.addBleListener('connected', res => {

res === 'success' && alert('连接成功')
res === 'fail' && alert('连接失败')

})

$ble.addBleListener('pair', res => {

res === 'success' && alert('配对成功')
res === 'fail' && alert('配对失败')
...

})