学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为3675字,预计阅读12分钟
前言
接上篇《Android BlueToothBLE入门(二)——设备的连接和通讯(附Demo源码地址)》最后提到过蓝牙BLE通讯每次默认发送的数据为20字节,如果我们要处理大的数据时,需要修改MTU的值,还有就是分包数据发送,本篇就专门来看看怎么实现的分包数据的发送和接收。
实现效果
代码实现
微卡智享
01
修改MTU值
修改MTU值其实是非常简单的,本身有个函数直接调用就可以申请,直接在BlueToothGatt下面有个requestMtu方法,其中参数size就是要申请的MTU值。
前面说过,BLE通讯默认是20字节,最大也只有512字节,所以既然申请MTU,那就往最大申请即可,代码中还是在当时BlueToothBLEUtil的类中先定义一个mtuSize,用于记录当前的mtu值,后面根据这个值的大小来实现分包处理的。
申请Mtu时我这里放到了发现服务返回后直接再做申请,那就是修改Gatt的回调方法里面onServicesDiscovered
最开始是连接成功后,发现服务并直接申请修改Mtu,在测试过程中有时候会服务没有返回刷不出来,所以改到发现服务后再申请。
02
分包发送数据和接收处理
申请MTU比较简单,现在是这篇文的重点了,分包的方式其实也有多种,我这边采用的是每个数据包中前4个字节来定义总包数和当前包数,后面的是当前包的数据,如下图所示。
上面可以看到,1-2字节是代表总包数,3-4字节是当前包数,5-512字节是当前包的数据。
其实这里主要要说为什么是前4个字节来记录总包数和当前包,1个byte的数字范围是-128到127,总共就256个数字存储,考虑到每个包最大512字节,如果数据量特别大,拆分的包数大于256就有问题了,而正常的int类型存储需要4个byte,总包数和当前包如果都使用int存储就直接减少了8个字节,所以这里我采用的是2个byte存储,最大范围是65535,这个分包数应该就够了。
两个字节和int类型的相互转化函数
接下来是分包和截取数据的相关处理了,通过ByteArray转换为list后,再进行chunked根据每个包实际大小生成list,再进行组包,转成Array<ByteArray>输出。
每个包的数据截取,通过ByteArray中的slice进行获取,截取后再进行转换即可获取总包数和当前包数。
bytearray相关的处理这里新建了一个Class实现的,直接贴上来。
package vac.test.bluetoothbledemo.repository
object BLEByteArrayUtil {
//计算发送的数据库生成数组
fun calcSendbyteArray(byteArray: ByteArray): Array<ByteArray?> {
//根据当前的传输MTU值计算要分的包数
//分包格式前前两个byte是总包数,当前包数,
//为了节省字节,前4个字节为总包数2个,当前包数2个,
//采用short的取值范围65536,分包如果超过这个总包数,就不做传输了
val everybytelen = BlueToothBLEUtil.mtuSize - 4
val totalpkgs =
byteArray.size / everybytelen + if (byteArray.size % everybytelen > 0) 1 else 0
val listbyte = byteArray.toList().chunked(everybytelen)
val arybytes = arrayOfNulls<ByteArray>(totalpkgs)
//定义总包数
val totalbytepkgs = inttobytes2bit(totalpkgs)
for(i in 0 until totalpkgs) {
//转换当前包数
val curbytepkgs = inttobytes2bit(i)
val curbytes = totalbytepkgs.plus(curbytepkgs).plus(listbyte[i])
arybytes[i] = curbytes
}
return arybytes
}
//region 处理接收的数组
//获取当前ByteArray的总包数
fun getTotalpkgs(bytes: ByteArray):Int{
val totalbytes = bytes.slice(0..1)
return bytestoint2bit(totalbytes.toByteArray())
}
//获取当前ByteArray的当前包数
fun getCurpkgs(bytes: ByteArray):Int{
val curbytes = bytes.slice(2..3)
return bytestoint2bit(curbytes.toByteArray())
}
//获取当前ByteArray的实际数据包
fun getByteArray(bytes: ByteArray):ByteArray{
val curdata = bytes.slice(4 until bytes.size)
return curdata.toByteArray()
}
//endregion
//Int类型转ByteArray,范围是65536,只用两个字节
private fun inttobytes2bit(num: Int): ByteArray {
val byteArray = ByteArray(2)
val lowH = ((num shr 8) and 0xff).toByte()
val lowL = (num and 0xff).toByte()
byteArray[0] = lowH
byteArray[1] = lowL
return byteArray
}
//ByteArray类型转Int,范围是65536,只用两个字节
private fun bytestoint2bit(bytes: ByteArray): Int {
val lowH = (bytes[0].toInt() shl 8)
val lowl = bytes[1].toInt()
val resint = if (lowH + lowl < 0) {
65536 + lowH + lowl
} else {
lowH + lowl
}
return resint
}
}
分包发送数据
在原来的BlueToothBLEUtil中再加入分写发送的函数,每个包发送完后间隔50毫秒
接收再组装数据
还是BlueToothBLEUtil中,首先定义了一个HashTable,根据通讯的设备地址为key生成数组。
接收的当前包数据先调用前面写的函数获取到总包数,当前包数和当前包的数据,根据总包数定义总包数的数组,如果hashtable里面有直接获取到后更新对应的当前包数据,因为发送时是按顺序发送的,所以在接收的时候判断当前包数+1是否等于总包数,相等即说明所有的数据包接收完成。
当接收完后从hashtable中获取到Array<ByteArray>数组,然后将数组组合成一个ByteArray返回,并且在hasttable中删除即可。
而数据接收到处理在Server中就写在BluetoothGattServerCallback回调的onCharacteristicWriteRequest中
//特征值写入回调
override fun onCharacteristicWriteRequest(
device: BluetoothDevice, requestId: Int,
characteristic: BluetoothGattCharacteristic, preparedWrite: Boolean,
responseNeeded: Boolean, offset: Int, value: ByteArray
) {
super.onCharacteristicWriteRequest(
device,
requestId,
characteristic,
preparedWrite,
responseNeeded,
offset,
value
)
Log.i("pkg","${requestId} ${value}")
//刷新该特征值
characteristic.value = value
// 响应客户端
if (ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
return
}
// mBluetoothGattServer.sendResponse(
// device, requestId, BluetoothGatt.GATT_SUCCESS,
// offset, value
// )
BlueToothBLEUtil.sendResponse(
device,requestId,offset,value
)
//处理接收的数据
val res = BlueToothBLEUtil.dealRecvByteArray(device.address, value)
//接收完毕后进行数据处理
if(res) {
//获取接收完的数据
val recvByteArray = BlueToothBLEUtil.getRecvByteArray(device.address)
var readstr = String(recvByteArray)
lifecycleScope.launch {
serverViewModel.serverIntent.send(
ServerIntent.Info(
"${device.address} 请求写入特征值: UUID = ${characteristic.uuid} " +
"写入值 = ${readstr}"
)
)
lifecycleScope.async {
//模拟数据处理,延迟100ms
delay(100)
val sb = StringBuilder()
for(i in 1..10){
sb.append("服务端收到了客户端发的消息,这里是返回的消息,第${i}条 ")
}
val readbytearray = sb.toString().toByteArray()
characteristic.value = readbytearray
//回复客户端,让客户端读取该特征新赋予的值,获取由服务端发送的数据
BlueToothBLEUtil.notifyCharacteristicChangedSplit(device, characteristic, readbytearray)
}
}
}
}
相应的Client客户端在BluetoothGattCallback回调的onCharacteristicChanged中
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
super.onCharacteristicChanged(gatt, characteristic, value)
lifecycleScope.launch {
//处理接收的数据
val res = BlueToothBLEUtil.dealRecvByteArray(gatt.device.address, value)
//接收完毕后进行数据处理
if(res) {
//获取接收完的数据
val recvByteArray = BlueToothBLEUtil.getRecvByteArray(gatt.device.address)
val str = "返回消息:${String(recvByteArray)}"
connectViewModel.connectIntent.send(
ConnectIntent.CharacteristicNotify(str, characteristic)
)
}
}
}
这样数据分包的发送和接收就实现了,效果就是文章开头的GIf视频中,源码还是上次的Demo中,已更新至当前版本了。
源码地址
https://github.com/Vaccae/AndroidBLEDemo.git
点击原文链接可以看到“码云”的源码地址
完
往期精彩回顾
Android BlueToothBLE入门(二)——设备的连接和通讯(附Demo源码地址)
Android BlueToothBLE入门(一)——低功耗蓝牙介绍
Android监听消息(二)——电话及短信监听