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

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

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秒左右,具体应该还要看设备)

 

 

 

  • 4
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值