一、低功耗蓝牙的基础知识
1、低功耗蓝牙简介
蓝牙4.0及更高版本被称为蓝牙低功耗,其中蓝牙4.0标准包括传统的蓝牙模块部分和蓝牙低功耗模块部分,这是双模式标准。一般上位机都会有相应的蓝牙API可用,应用程序可以通过这些 API 执行扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。对于低功耗蓝牙,还有很多方面可以去深入,我这边只是对低功耗蓝牙做最简单的操作分享,更深入的不做剖析。本流程文档主要是基于本人参与的蓝牙项目为基础的,所以可能有些问题(欢迎私信或加QQ(443673422)指正),权当参考即可。
2、RSSI信号强度
数值越大,信号越好,当RSSI = 0的时候即代表全部接受(理想状态)。举例 :-81的信号要优于-96的信号。
3、低功耗蓝牙的整个结构简介
蓝牙模块的底层设计结构(大致了解下就行,与上位机开发而言,我们只关注模块的应用层):
应用层 规约定义了三种类型:特性、服务和规范。
1、特性(characteristic)是采用已知格式、以通用唯一识别码(UUID)作为标记的一小块数据。
2、服务(service)是人类可读的一组特征及其相关的行为规范。
3、规范(profile)是描述两个或多个设备的说明,每个设备提供一个或多个服务。
对于低功耗的蓝牙的操作,其实主要就是对低功耗蓝牙特征值的操作。
4、对低功耗蓝牙可以执行的操作(就是对特征值的操作)
(1)Write without response:从机端直接写入数据到特征值中。(一般此类,就是需要打开notify去监听设备的返回数据)
(2)Notify:从机端(BLE透传模块)通过该Characteristic以Notification的方式给主机段发送数据,最大 20 字节。
(3)Write/Read:从机端直接的读写。(这边的写应该是会有返回)
二、uni-app集成低功耗设备全流程(我这边是带低功耗蓝牙的智能水表)
1、低功耗蓝牙集成的步骤概括
(1)初始化蓝牙 uni.openBluetoothAdapter(OBJECT)
(2)开始搜索蓝牙设备 uni.startBluetoothDevicesDiscovery(OBJECT)
(3)发现外围设备 uni.onBluetoothDeviceFound(CALLBACK)
(4)停止搜寻附近的蓝牙外围设备 uni.stopBluetoothDevicesDiscovery(OBJECT)
(5)连接低功耗蓝牙设备 uni.createBLEConnection(OBJECT)
(6)获取蓝牙设备所有服务 uni.getBLEDeviceServices(OBJECT)
(7)获取蓝牙特征 uni.getBLEDeviceCharacteristics(OBJECT)
(8)启用蓝牙设备特征值变化时的 notify 功能 uni.notifyBLECharacteristicValueChange(OBJECT)
(9)监听低功耗蓝牙设备的特征值变化 uni.onBLECharacteristicValueChange(CALLBACK)
(10)对需要操作的特征值进行读、写操作
2、低功耗蓝牙实现的全部代码(uni-app的代码)
蓝牙模块的厂家应该会提供相应的协议文档,本节代码仅供参考,本节代码只是实现了从机端往某一特征值中写入数据,然后再notify中获取设备返回的数据。
(1)界面代码
<!-- 小程序蓝牙集成的整个流程 -->
<!-- 1、初始化蓝牙 uni.openBluetoothAdapter(OBJECT) -->
<!-- 2、开始搜索蓝牙设备 uni.startBluetoothDevicesDiscovery(OBJECT) -->
<!-- 3、发现外围设备 uni.onBluetoothDeviceFound(CALLBACK) -->
<!-- 4、停止搜寻附近的蓝牙外围设备 uni.stopBluetoothDevicesDiscovery(OBJECT) -->
<!-- 5、连接低功耗蓝牙设备 uni.createBLEConnection(OBJECT) -->
<!-- 6、获取蓝牙设备所有服务 uni.getBLEDeviceServices(OBJECT) -->
<!-- 7、获取蓝牙特征 uni.getBLEDeviceCharacteristics(OBJECT) -->
<!-- 8、启用蓝牙设备特征值变化时的 notify 功能 uni.notifyBLECharacteristicValueChange(OBJECT) -->
<!-- 9、监听低功耗蓝牙设备的特征值变化 uni.onBLECharacteristicValueChange(CALLBACK) -->
<!-- 10、对需要操作的特征值进行读、写操作 -->
<template>
<view class="content">
<!-- 蓝牙信息显示区域 单击蓝牙可以执行绑定操作-->
<scroll-view class="deviceList" scroll-y="true">
<!-- 循环显示的条目 -->
<view v-for="(obj,index) in mList" class="itemStyle" @click="connectDevice(obj.deviceId)">
蓝牙地址:{{obj.deviceId}}蓝牙名称:{{obj.deviceName}}
</view>
</scroll-view>
<!-- 搜索蓝牙 -->
<button style="width: 100%;" @click="searchDevice">连接指定Ble</button>
<!-- 停止搜索蓝牙 -->
<button style="width: 100%;" @click="closeBlueAdapter">关闭蓝牙适配器</button>
<!-- 关闭低功耗蓝牙连接 -->
<button style="width: 100%;" @click="disconnectBle">断开Ble连接</button>
<!-- 写入数据并读取数据 -->
<button style="width: 100%;" @click="writeMeter">写数据</button>
</view>
</template>
<script>
import blueTool from '../../common/dataFormatUtil.js';
import uTool from '../../common/buletoothUtil.js'
//import {Meter,concat} from '../../common/meter.js'
let _self;
/* 这三个是需要厂商提供的 服务UUID,特征值UUID*/
let UUID_SERVICE = '0000FF13-0000-1000-8000-00805F9B34FA';
let UUID_CHAR1 = '0000FF01-0000-1000-8000-00805F9B34FA';
let UUID_CHAR2 = '0000FF02-0000-1000-8000-00805F9B34FA';
export default {
data() {
return {
mList:[],//搜索到的设备列表
mDeviceId:0,//连接的设备
targetNumber:'',//要连接的指定设备
}
},
onLoad() {
_self = this;
//console.log("12111111")
//_self.meter = new Meter(1,2,"032202012070")
//_self.meter.writeData()
//console.log(_self.meter)
},
methods: {
/* 延时测试 */
test(){
// 测试当前的数据
},
/* 搜索蓝牙操作 */
searchDevice(){
uni.showLoading({
title:"连接中"
})
/* 1、初始化蓝牙 uni.openBluetoothAdapter(OBJECT) */
uni.openBluetoothAdapter({
success: (res) => {
/* 初始化蓝牙成功 */
/* 2、开始搜索蓝牙设备 uni.startBluetoothDevicesDiscovery(OBJECT */
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey:false,//是否允许同一设备多次上报,但RSSI值会有所不同
success: (res) => {
console.log("搜索成功:" + res)
},
fail: (res) => {
console.log("搜索失败:" + res)
}
})
},
fail: (res) => {
console.log("初始化蓝牙失败:" + res.code)
uni.showToast({
duration:5000,
title:"请手动打开手机蓝牙",
icon:'none',
})
}
}),
/* 搜索蓝牙设备回调 */
uni.onBluetoothDeviceFound(function(CALLBACK){
/* 我这边是直接每次遍历指定设备,
一旦搜索到指定设备就结束搜索,
需要显示的就每次都在这边把搜索到的设备添加到数组中,然后界面去显示*/
/* 搜索过程中如果搜索到需要的设备 */
CALLBACK.devices.forEach(device => {
/* 筛选需要的deviceName */
if(device.name === _self.targetNumber){
//找到需要的设备之后,停止搜索
console.log("搜索到指定设备")
/* 停止搜索 */
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
uni.getBluetoothDevices({
success: (devices) => {
}
})
console.log("停止搜索成功")
},
fail: (res) => {
console.log("停止失败")
uni.hideLoading();
}
})
/* 执行连接 */
_self.connectDevice(device.deviceId);
}
})
}),
/* 监听蓝牙适配器状态变化事件 可以检测当前设备的蓝牙的断开或者连接的情况*/
uni.onBluetoothAdapterStateChange(function(CALLBACK){
//CALLBACK.available 蓝牙适配器是否可用
//CALLBACK.discovering 蓝牙适配器是否处于搜索状态
console.log('适配器状态变化', CALLBACK)
})
},
/* 关闭蓝牙适配器*/
closeBlueAdapter(){
uni.closeBluetoothAdapter({
success() {
console.log("关闭蓝牙适配器成功")
}
})
},
/* 断开低功耗蓝牙的连接 */
disconnectBle(){
uni.closeBLEConnection({
deviceId:_self.mDeviceId,
success: (res) => {
uni.showToast({
title:"断开成功"
});
console.log("断开成功" + res)
}
})
},
/* 连接低功耗蓝牙 */
connectDevice(deviceId){
console.log("执行到这里了")
uni.createBLEConnection({
deviceId,//当前点击的DeviceId
timeout:5000,
success: (res) => {
uni.hideLoading();//需要先隐藏才行,不然不会显示弹框
console.log("连接成功")
_self.mDeviceId = deviceId;
/* 这边获取全部的服务,并筛选出当前需要的*/
_self.getBLEDeviceServices(deviceId)
},
fail: (error) => {
/* 连接失败 */
uni.hideLoading();
console.log("连接失败")
console.log(error);
}
})
/* 监听蓝牙设备的连接状态 */
uni.onBLEConnectionStateChange(function(CALLBACK){
console.log(CALLBACK.deviceId + "连接状态:" + CALLBACK.connected)
})
},
/* 获取所有的服务(目前理解来说应该是进入服务) */
getBLEDeviceServices(deviceId){
console.log("开始获取服务")
//连接成功之后需要延时,继续操作才不会出问题,延时时间短一点都没关系,我这边用了1秒
setTimeout(()=>{
uni.getBLEDeviceServices({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success:(res)=>{
console.log('device services:', res)
res.services.forEach((item)=>{
let serviceId = item.uuid;
console.log(UUID_SERVICE)
if(serviceId == UUID_SERVICE){
console.log('serverId:',serviceId)
/* 进入特征值 */
_self.getBLEDeviceCharacteristics(deviceId,serviceId);
}
})
},
fail: (error) => {
console.log("获取服务失败")
console.log(error)
uni.hideLoading();
}
})
},1000)
},
/* 获取所有特征值 */
getBLEDeviceCharacteristics(deviceId,serviceId){
console.log("开始获取特征")
setTimeout(()=>{
uni.getBLEDeviceCharacteristics({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
success:(res)=>{
console.log(res)
//let characteristics = res.characteristics
res.characteristics.forEach((item)=>{
let characterId = item.uuid;
/* 只要用到的唤醒即可 */
if(characterId == UUID_CHAR2){
console.log('characteristicId:',characterId)
_self.notifyBLECharacteristicValueChange(deviceId,serviceId,characterId)
}
})
},
fail:(res)=>{
uni.hideLoading();
console.log(res)
}
})
},1000)
},
notifyBLECharacteristicValueChange(deviceId,serviceId,characteristicId){
console.log("开始唤醒")
uni.notifyBLECharacteristicValueChange({
state: true, // 启用 notify 功能
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
success:(res)=> {
uni.hideLoading();
_self.meter.tunnel=_self.setTokenTest;
/* 连接成功 */
uni.showToast({
duration:3000,
title:"连接成功",
icon:'success'
});
console.log('notifyBLECharacteristicValueChange success', res.errMsg)
},
fail:(res)=> {
uni.hideLoading();
console.log('notifyBLECharacteristicValueChange success', res.errMsg)
}
})
},
/* 连接成功以后,执行设置的操作,并且打开返回值的监听开关并监听 */
writeMeter(){
/* 获取所有的服务 */
/* 每次设置钱都将notify打开(设备回复开关) */
let tokenArr = '0000FEFE687020010222036814000301000000060a512233191974345957215cdd3a16';
let arrayBuffer = blueTool.string2buffer(tokenArr)
console.log(arrayBuffer)
//console.log(_self.mDeviceId)
/* 测试发现如果不做分包发送的话,还是会有问题,所以这边还是需要做分包发送 */
_self.writeDevice(arrayBufferss);
/* 这边对特征值2进行数据的监听 */
let resultAll;
uni.onBLECharacteristicValueChange(function(CALLBACK){
let result = blueTool.ab2hex(CALLBACK.value);
resultAll += result;
console.log("将两次收到的数据值拼合一下",resultAll)
})
},
/* 执行演示分包操作 */
writeDevice(_Buffer){
let Num = 0;
let ByteLength = _Buffer.byteLength;
let i = 1;
while (ByteLength > 0) {
i++;
let TmpBuffer;
console.log("TmpBuffer",TmpBuffer)
console.log("Num",Num)
console.log("ByteLength",ByteLength)
if(ByteLength > 20)
{
TmpBuffer = _Buffer.slice(Num, Num + 20);
Num += 20;
ByteLength -= 20;
console.log('执行常规循环')
uTool.delayed(500).then(()=>{
uni.writeBLECharacteristicValue({
deviceId:_self.mDeviceId,
serviceId:UUID_SERVICE,
characteristicId:UUID_CHAR1,
value: TmpBuffer,
success: (res) => {
/* 发送成功 */
console.log('前面的循环成功', res)
},
fail: (error) => {
/* 发送失败 */
console.log('前面的循环失败',error)
}
})
})
}else{
console.log('执行最后一次')
TmpBuffer = _Buffer.slice(Num, Num + ByteLength)
Num += ByteLength
ByteLength -= ByteLength
/* 当长度不满20的时候执行最后一次发送即可 */
uTool.delayed(500).then(()=>{
uni.writeBLECharacteristicValue({
deviceId:_self.mDeviceId,
serviceId:UUID_SERVICE,
characteristicId:UUID_CHAR1,
value: TmpBuffer,
success: (res) => {
/* 发送成功 */
console.log('最后一次写入成功', res)
},
fail: (error) => {
/* 发送失败 */
console.log('最后一次写入失败',error)
}
})
})
}
}
}
},
}
</script>
<style>
.itemStyle{
width: 100%;
height: 200rpx;
border: 2rpx solid #007AFF;
}
.content{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.deviceList{
border: 2rpx solid #007AFF;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 80rpx;
}
</style>
(2)工具代码
export default{
/* 蓝牙通信工具 */
getError(errorCode){
if(errorCode == 0) return "OK";
if(errorCode == 10000) return "未初始化蓝牙适配器";
if(errorCode == 10001) return "当前蓝牙适配器不可用";
if(errorCode == 10002) return "没有找到指定设备";
if(errorCode == 10003) return "连接失败";
if(errorCode == 10004) return "没有找到指定服务";
if(errorCode == 10005) return "没有找到指定特征值";
if(errorCode == 10006) return "当前连接已断开";
if(errorCode == 10007) return "当前特征值不支持此操作";
if(errorCode == 10008) return "其余所有系统上报的异常";
if(errorCode == 10009) return "Android 系统特有,系统版本低于 4.3 不支持 BLE";
return "未知异常";
},
/* 异步执行,延时函数 */
delayed(ms,res){
return new Promise((resolve,reject) =>{
setTimeout(function(){
resolve(res);
},ms)
});
}
}
/* 数据格式的转换工具类 */
export default{
/* 字符串转arraybuffer */
string2buffer: function(str) {
// 首先将字符串转为16进制
let val = str
/* for (let i = 0; i < str.length; i++) {
if (val === '') {
val = str.charCodeAt(i).toString(16)
} else {
val += ',' + str.charCodeAt(i).toString(16)
}
} */
console.log(val)
// 将16进制转化为ArrayBuffer
return new Uint8Array(val.match(/[\da-f]{2}/gi).map(function(h) {
return parseInt(h, 16)
})).buffer
},
ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function(bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('')
},
/* arraybuffer 转字符串 */
bufferString: function(str) {
// ArrayBuffer转16进度字符串示例
function ab2hex(buffer) {
const hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function(bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('')
}
//16进制
let systemStr = ab2hex(str)
// console.log(hexCharCodeToStr(systemStr),99)
function hexCharCodeToStr(hexCharCodeStr) {
var trimedStr = hexCharCodeStr.trim();
var rawStr = trimedStr.substr(0, 2).toLowerCase() === "0x" ?trimedStr.substr(2):trimedStr;
var len = rawStr.length;
if (len % 2 !== 0) {
alert("Illegal Format ASCII Code!");
return "";
}
var curCharCode;
var resultStr = [];
for (var i = 0; i < len; i = i + 2) {
curCharCode = parseInt(rawStr.substr(i, 2), 16); // ASCII Code Value
let str5 = String.fromCharCode(curCharCode)
if (str5.startsWith('\n') == false) {
resultStr.push(String.fromCharCode(curCharCode));
}
}
return resultStr.join("");
}
// console.log(hexCharCodeToStr(systemStr),888)
return hexCharCodeToStr(systemStr)
},
}
三、采坑总结
1、蓝牙的分包机制
蓝牙协议的限制,每次通信传输都不能超过20字节。分包的方法参见上述代码。
2、需要在连接蓝牙的时候打开notify()
每次在连接蓝牙之后,都可需要直接将监听打开。
3、流程上的简化
对于蓝牙模块特征值已经限定的模块来说,在连接低功耗蓝牙并打开notify监听之后,直接根据指定的特征值uuid进行读写即可,不需要再执行获取服务->获取特征值的操作。
后测试发现,在苹果手机上,如果直接执行简化的流程,会发现一直无法成功唤醒notify,所以在IOS上,还是需要执行getBLEDeviceServices、getBLEDeviceCharacteristics后再执行notify()
4、低功耗蓝牙休眠
低功耗蓝牙在一段时间不操作之后,是会自动断连休眠的
5、数据格式问题
uni-app中发送的数据参数必须要是ArrayBuffer,不然会出错,在监听中设备返回的数据也是ArrayBuffer,所以都需要转换,详见上面工具代码
6、延时问题
在连接蓝牙成功之后的操作都是需要加一下延时(setTimeout)。
7、在IOS设备上,连接蓝牙的是时候,传入的deiviceId需要用LocalName,我这边的模块是使用LocalName,Android和IOS就都不会出问题了。
8、对于同一个蓝牙设备,必须要规范使用,连接一次,用完断开,如果重复连接,可能会造成多个实例连接同一个蓝牙设备。(蓝牙设备自动断开连接的时间大概20秒左右,具体应该还要看设备)