一、低功耗蓝牙的基础知识

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,//当前点击的DeviceIdtimeout: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.characteristicsres.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 += ByteLengthByteLength -= 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进制转化为ArrayBufferreturn 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 Valuelet 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秒左右,具体应该还要看设备)

【uni-app学习】uni-app低功耗蓝牙采坑记录相关推荐

  1. iOS 微信SDK1.8.6后需要UniversalLink解决方案及采坑记录

    项目最初因审核原因,一直使用iOS原生分享, 最近因项目需求要求, 接入微信分享, 以为和原来的没有区别, 但是接入时才发现改动的地方还是挺多的, 主要是需要配置UniversalLink和提包时的一 ...

  2. 微信支付采坑记录(java后端 一:微信支付调用从哪一步开始)

    普通商户模式微信支付之APP支付统一下单: 前言: 最近项目开发到微信支付的模块,经过一周的不懈努力,虽然微信支付流程跑通了,但当时的那些坑是真的烦,为了避免自己以后犯同样的错误,当然还有各位新接触微 ...

  3. 【SpringBoot DB系列】Jooq批量写入采坑记录

    [SpringBoot DB系列]Jooq批量写入采坑记录 前面介绍了jooq的三种批量插入方式,结果最近发现这里面居然还有一个深坑,我以为的批量插入居然不是一次插入多条数据,而是一条一条的插入-,这 ...

  4. php给微信公众号接入聊天机器人程序+采坑记录

    php给微信公众号接入聊天机器人程序 今天逛了下我的公众号,突然心血来潮,想添加个自动聊天功能,于是-动手-!! 主要用到的api: 图灵机器人api 青云客智能聊天机器人API 茉莉机器人API 至 ...

  5. 联想昭阳E40-80安装win10、ubuntu18.04双系统安装采坑记录

    联想昭阳E40-80安装win10.ubuntu18.04双系统安装采坑记录 1 硬盘格式:MBR 2 rufus的坑:缺少文件ldlinux.sys ldlinux.bss 3 分区的坑:多分了一个 ...

  6. H5拍照、预览、压缩、上传采坑记录

    H5拍照.预览.压缩.上传采坑记录 公司项目前段时间需要实现手机拍照上传的功能,本来以为用createObjectURL和canvas可以很轻松的实现,结果发现问题多多,特此记录下来. DEMO预览( ...

  7. mysql8.0.19.0_分享MySql8.0.19 安装采坑记录

    上篇文章给大家介绍了MySql8.0.19 安装过程,需要的朋友可以点击查看.https://www.jb51.net/article/178988.htm 1.ERROR 1820 (HY000): ...

  8. mysql8.0依赖_分享MySql8.0.19 安装采坑记录

    上篇文章给大家介绍了MySql8.0.19 安装过程,需要的朋友可以点击查看.https://www.jb51.net/article/178988.htm 1.ERROR 1820 (HY000): ...

  9. Ubuntu18安装微信(deepin-wine版本)完整过程以及采坑记录

    Ubuntu18安装微信(deepin-wine版本)完整过程以及采坑记录(亲测运行) git clone https://gitee.com/wszqkzqk/deepin-wine-for-ubu ...

最新文章

  1. IP地址与字符串、整型之间的转换
  2. 专用码计算机英语怎么说,计算机专用英语词汇整理
  3. 深入理解分布式技术 - Dubbo vs Spring Cloud
  4. WebDriver自动化测试工具(3)---PhantomJS的使用
  5. Flutter: 显示/关闭系统叠加层ui
  6. 升级EXCHANGE2010到2013(C)
  7. 读书笔记-泛型有限通配符
  8. 使用Object、param标签在页面显示PDF文件
  9. 模拟CMOS集成电路设计中的gm/id设计方法及用Cadence Virtuoso IC617仿真有关参数曲线
  10. Ubuntu Desktop LTS - 快速显示桌面
  11. 如何在未越狱的ios系统安装ipa文件
  12. Redis第六讲 Redis之List底层数据结构实现
  13. arXiv每日推荐-5.9:语音/音频每日论文速递
  14. Linux LVM(逻辑卷管理)
  15. 学生每日计划表_中学生时间计划表
  16. 静态路由的基本原理讲解
  17. 获取服务器上图片的大小KB
  18. SV学习笔记—随机化约束的概率
  19. 用python画一箭穿心_python turtle画一箭穿心
  20. reduce参数的理解

热门文章

  1. python输出emoji表情符号 学习笔记
  2. 数据库候选关键词怎么求_关系模式中候选关键字的图论求解法
  3. 宝塔面板搭建个人图床Chevereto完整教程
  4. 微信小程序实现store功能
  5. 必不可少的10类MAC装机必备软件,个个万里挑一
  6. CentOS 7.6安装JDK8过程(通过官网下载压缩包方式)
  7. 作也 努力努力在努力
  8. 中荷金生有约养老年金险怎么样?好不好?
  9. 信息系统项目管理师与系统集成项目管理工程师5大区别
  10. HDU 4417 Super Mario(线段树离线处理/主席树)