背景
使用hybrid框架apicloud,2022年12月,开发物联网领域的蓝牙交互功能时,使用了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以后需要定位权限,否则无法正常使用
async init() {
if (isAndroid) {
const { granted } = confirmPermission('location')
if (!granted) {
dispatchEvent('bleStatus', { type: 'location', status: false })
return
}
}
}
// 安卓12及以上和鸿蒙(非next)需要打开‘附近设备’权限
if (AndroidVersion >= 12) {
const { granted } = confirmPermission('ble-scan')
if (!granted) {
dispatchEvent('bleStatus', { type: 'ble-scan', status: false })
return
}
}
// 蓝牙
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、监听生命周期的各个节点,和处理方法
addBleListener(type, handler) {
console.log('监听到', type)
if (!(type in $ble.data.handlers)) $ble.data.handlers[type] = []
$ble.data.handlers[type].push(handler)
},
3、发布事件两个参数(事件名,参数)
dispatchEvent(type, ...params) {
console.log('触发了', type)
if (!(type in $ble.data.handlers)) return
$ble.data.handlers[type].forEach(handler => {
handler(...params)
})
},
4、监听蓝牙是否一直连接
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、连接蓝牙
// 连接整体控制
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
}
}
5、获取服务
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')
}
})
},
6、获取特征
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')
}
});
},
7、注册、配对、数据交互, 都是获取数据包,发送数据包的过程
startRegister() {},
pair() {},
read() {},
8、开启监听设备响应的数据包
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()
}
},
9、发送数据包
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)
}
},
10、重写关闭窗口的页面,由于关闭窗口的时候无法监听,从而去关闭蓝牙设备的连接,所以重写api.closeWin,实现最大限度的控制蓝牙状态
reWriteClose() {
// 左滑返回处理
api.addEventListener({
name: 'keyback'
}, function (ret, err) {
cn++
if (cn == 1)
toast("再滑一次退出蓝牙")
else if (cn == 2)
api.closeWin()
})
// 直接关闭重写, 关闭时将蓝牙状态重置
api.closeWin = () => {
loading()
await handleBle()
close()
}
}
使用
$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('配对失败')
...
})