背景
使用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以后需要定位权限,否则无法正常使用
| 12
 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)需要打开‘附近设备’权限
    
| 12
 3
 4
 5
 6
 7
 
 | if (AndroidVersion >= 12) {const { granted } = confirmPermission('ble-scan')
 if (!granted) {
 dispatchEvent('bleStatus', { type: 'ble-scan', status: false })
 return
 }
 }
 
 | 
    // 蓝牙
| 12
 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、监听生命周期的各个节点,和处理方法
| 12
 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、发布事件两个参数(事件名,参数)
| 12
 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、监听蓝牙是否一直连接
| 12
 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、连接蓝牙
| 12
 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.获取服务
| 12
 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.获取特征
| 12
 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.注册、配对、数据交互, 都是获取数据包,发送数据包的过程
| 12
 3
 
 | startRegister() {},pair() {},
 read() {},
 
 | 
  // 7.开启监听设备响应的数据包
| 12
 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.发送数据包
| 12
 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,实现最大限度的控制蓝牙状态
| 12
 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()
 })
 
 | 
| 12
 3
 4
 5
 6
 
 | api.closeWin = () => {
 loading()
 await handleBle()
 close()
 }
 
 | 
  }
使用
| 12
 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('配对失败')
 ...
 
 })
 
 |