背景
使用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] 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) 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('配对失败') ...
})
|