前言
在android中,BLE的特征一次读写最大长度20字节。对于长时间连续发送的大于20字节的帧结构,如果安卓终端进行接收的话,就需要我们进行重新组帧(即如何处理粘包和丢包问题)。对于如何处理这个问题,首先需要在帧结构上做好设计。一个比较完整的帧,应包含帧头,帧长度,帧序号以及帧尾,通过这些信息来做判断是否丢帧和重新组帧。
就拿我们最近做的一个蓝牙接收项目来说,我们设计每帧是128字节。其具体定义为:
/**
* Created by sks on 2017/12/1.
* /**
* @author sks
* * 定义帧的结构
*
*
* 帧头:4个字节 , 0xbebebebe
* 帧计数:1个字节
* 帧长度:1个字节 128
* sessionId: 4个字节
* heart_array: 心电数组 100字节
* breath_array:呼吸数组 10字节
* temperature_value:体温 2字节
* heart_value: 心率 1字节
* breath_value: 呼吸率 1字节
* power_level:电池标志位 1字节
* reserved_array: 保留字节数组 1字节
* 帧尾: 2字节 0xaaaa
*
*
*/
public class EcgMsg {
/**
*
*/
public EcgMsg() {
super();
}
private byte[] frameHeader;
private int frameNum; //一个字节
private int frameLength;
private byte[] sessionID;
private byte[] heart_array;
private byte[] breath_array;
private float temperature_value;
private int heart_value;
private int breath_value;
private int power_level;
private byte[] reserved_array;
public EcgMsg(byte[] frameHeader, int frameNum, int frameLength, byte[] sessionID, byte[] heart_array, byte[] breath_array, float temperature_value, int heart_value, int breath_value, int power_level, byte[] reserved_array) {
this.frameHeader = frameHeader;
this.frameNum = frameNum;
this.frameLength = frameLength;
this.sessionID = sessionID;
this.heart_array = heart_array;
this.breath_array = breath_array;
this.temperature_value = temperature_value;
this.heart_value = heart_value;
this.breath_value = breath_value;
this.power_level = power_level;
this.reserved_array = reserved_array;
}
public byte[] getFrameHeader() {
return frameHeader;
}
public void setFrameHeader(byte[] frameHeader) {
this.frameHeader = frameHeader;
}
public int getFrameNum() {
return frameNum;
}
public void setFrameNum(int frameNum) {
this.frameNum = frameNum;
}
public int getFrameLength() {
return frameLength;
}
public void setFrameLength(int frameLength) {
this.frameLength = frameLength;
}
public byte[] getSessionID() {
return sessionID;
}
public void setSessionID(byte[] sessionID) {
this.sessionID = sessionID;
}
public byte[] getHeart_array() {
return heart_array;
}
public void setHeart_array(byte[] heart_array) {
this.heart_array = heart_array;
}
public byte[] getBreath_array() {
return breath_array;
}
public void setBreath_array(byte[] breath_array) {
this.breath_array = breath_array;
}
public float getTemperature_value() {
return temperature_value;
}
public void setTemperature_value(float temperature_value) {
this.temperature_value = temperature_value;
}
public int getHeart_value() {
return heart_value;
}
public void setHeart_value(int heart_value) {
this.heart_value = heart_value;
}
public int getBreath_value() {
return breath_value;
}
public void setBreath_value(int breath_value) {
this.breath_value = breath_value;
}
public int getPower_level() {
return power_level;
}
public void setPower_level(int power_level) {
this.power_level = power_level;
}
public byte[] getReserved_array() {
return reserved_array;
}
public void setReserved_array(byte[] reserved_array) {
this.reserved_array = reserved_array;
}
@Override
public String toString() {
return "EcgMsg{" +
"frameHeader=" + Arrays.toString(frameHeader) +
", frameNum=" + frameNum +
", frameLength=" + frameLength +
", sessionID=" + Arrays.toString(sessionID) +
", heart_array=" + Arrays.toString(heart_array) +
", breath_array=" + Arrays.toString(breath_array) +
", temperature_value=" + temperature_value +
", heart_value=" + heart_value +
", breath_value=" + breath_value +
", power_level=" + power_level +
", reserved_array=" + Arrays.toString(reserved_array) +
'}';
}
}
硬件前端采用了串口转蓝牙模块,通过安卓蓝牙接收,可以发现接收方式是128个字节分为7次接收,即20*6+8。传输过程中,这7次接收都有可能丢失某一次,因此我们受到的数有可能是
- 找不到帧头,即7次接收中第一次发的数丢了;
- 找不到帧尾,即最后一次接收8字节失败
- 中间某次接收失败,这种情况是帧头帧尾在,但中间数据丢失
- 以上几种情况组合。
对于上述情况,我们在接收后重新组帧,都必须考虑。
在java中,tcp框架常采用netty库,其通过封装好的方法,可以自动为我们进行tcp的组包处理(处理粘包)。其核心是定义了核心类:缓冲区 ByteBuf,其比java原生NIO的bytebuffer更强大。 因此,我们可以考虑借用netty的ByteBuf缓冲区来为我们的蓝牙接收进行组帧处理。
关于Netty的Bytebuf类介绍
ByteBuf都是维护一个byte数组的,它的内部格式是:
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* | | (CONTENT) | |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
与原生态的ByteBuffer相比,它维护了两个指针,一个是读指针,一个是写指针,而原生态的ByteBuffer只维护了一个指针,你需要调用flip方法来切换读写的状态,不易用户管理维护
读的时候,可读的区域是下标区间是[readerIndex,writeIndex),可写区间的是[writerIndex,capacity-1],但是discardable这段区间就会变得相对无用,既不能读,也不能写
所以我们可以使用discardReadBytes的方法进行内存空间的回收,回收之后是这样的:
* +------------------+--------------------------------------+
* | readable bytes | writable bytes (got more space) |
* +------------------+--------------------------------------+
* | | |
* readerIndex (0) <= writerIndex (decreased) <= capacity
bytebuf还有个重要功能是标记Mark和重置reset。
private int markedReaderIndex;//标记读索引
private int markedWriterIndex;//标记写索引
通过重置方法,可以在读指针回到标记读索引位置
实现
首先定义Bytebuf变量,初始化缓冲区大小
private ByteBuf buffer= Unpooled.buffer(1024*1000);
假设在蓝牙接收数据的回调方法中,每次收到的数通过下面代码获得:
final byte[] data = characteristic.getValue();
对于源源不断的data数组,需要送进缓冲区中,然后进行组帧处理
if (data != null && data.length > 0) {
buffer.writeBytes(data); //将蓝牙收到的byte数组放入bytebuf缓冲区中
// Log.i("tag", "缓冲区大小" + buffer.readableBytes() + "");
//重新组帧
while (buffer.readableBytes() > MyApplication.FRAME_LENGTH) { //判断缓冲区大小大于一帧长度,就
可以进行解帧
ByteBuf bufTemp = buffer.readBytes(1); //先取第一个字节,判断是不是帧头的第一个字节0xbe
byte[] bytesTemp = new byte[1];
bufTemp.readBytes(bytesTemp);
if (bytesTemp[0] == (byte) 0xbe) { //判断第一个字节是不是0xbe,如果不是,直接丢弃,如果是,则进入if判断
buffer.markReaderIndex(); //取后三个数时候,考虑如果第1个是0xbe,但是后面三个不是0xbe,这个时候需要进行回滚操作
ByteBuf bufTemp1 = buffer.readBytes(3); //如果第一个字节是0xbe,就再取后三个字节,继续对帧头进行判断
byte[] bytesTemp1 = new byte[3];
bufTemp1.readBytes(bytesTemp1);
if (bytesTemp1[0] == (byte) 0xbe && bytesTemp1[1] == (byte) 0xbe && bytesTemp1[2] == (byte) 0xbe) { //如果后三位是0xbebebe,说明找到了帧头,就需要取一帧的后续部分(帧长-帧头)=128-4=124个字节
ByteBuf bufTemp2 = buffer.readBytes(MyApplication.FRAME_LENGTH - 4);
byte[] bytesTemp2 = new byte[MyApplication.FRAME_LENGTH - 4];
bufTemp2.readBytes(bytesTemp2);
//取出帧的后续部分,还需要判断帧尾是不是0xaa,0xaa;如果不是0xaaaa,说明这个帧不完整,即需要重新进入第二个字节搜索帧头
if (bytesTemp2[bytesTemp2.length-2]!=(byte)0xaa&&bytesTemp2[bytesTemp2.length-1]!=(byte)0xaa){
buffer.resetReaderIndex(); //指针回滚,回滚到只是取出第一个数
continue; //重新进入while循环
}
// 查看帧计数
new_frame_index=bytesTemp2[0]&0xff;
if (new_frame_index!=(old_frame_index+1)||((new_frame_index==0)&&(old_frame_index!=255))){
error_Frame_count++;
log2Flie.log2FileFun("log","帧计数错误"+"error_Frame_count-->"+error_Frame_count+";old_frame-->"+old_frame_index+";new_frame-->"+new_frame_index);
}
old_frame_index=new_frame_index;
//重新组帧
byte[] bytesTemp3 = new byte[MyApplication.FRAME_LENGTH];
for (int i = 0; i < 4; i++) {
bytesTemp3[i] = (byte) 0xbe;
}
System.arraycopy(bytesTemp2, 0, bytesTemp3, 4, bytesTemp2.length);
//如果是,则放入list
synchronized (byteListInbuf) {
byteListInbuf.add(bytesTemp3); //放入byteListInbuf链表的帧shiw
}
buffer.discardReadBytes(); //将取出来的这一帧数据在buffer的内存进行清除,释放内存
}else{ //第一个字节是0xbe,后三个字节不是0xbebebe情况
buffer.resetReaderIndex(); //指针回滚,回滚到只是取出第一个数
continue;
}
}
}
}
改变BLE默认的单次发包、收包的最大长度
定义方法
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean requestMtu(int mtu){
if (mBluetoothGatt != null) {
Log.d("requestMtu", "requestMtud调用成功");
boolean ret=mBluetoothGatt.requestMtu(mtu);
Log.d("requestMtu","requestMtud调用情况--》"+ret);
return ret;
}else{
Log.d("requestMtu","requestMtud调用不成功");
}
return false;
}
然后,在 BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver()方法中,调用此方法
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) { //接口回调会是两次??????
final String action = intent.getAction();
if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { //连接成功
Log.e("tag", "Only gatt, just wait");
} else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { //断开连接
mConnected = false;
}else if(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) //可以开始干活了
{
Toast.makeText(activity, "连接成功,现在可以正常通信!", Toast.LENGTH_SHORT).show();
mConnected = true;
int sdkInt = Build.VERSION.SDK_INT;
System.out.println("sdkInt------------>" + sdkInt);
if (sdkInt>=21){
//设置最大发包、收包的长度为310个字节
if(mBluetoothLeService.requestMtu(310)){
// Toast.makeText(this,getString(R.string.transmittal_length,"512"),Toast.LENGTH_LONG).show();
}
}
}else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { //收到数据
Log.e("tag", "RECV DATA");
// String data = intent.getStringExtra(BluetoothLeService.EXTRA_DATA);
byte [] data=intent.getByteArrayExtra(BluetoothLeService.EXTRA_DATA);
Log.e("tag", "RECV DATA,data'length is ->"+data.length+"&&&"+new String(data));
if (data != null) {
// Toast.makeText(activity, data[0], Toast.LENGTH_SHORT).show();
}
}
}
};