android中ble蓝牙分包接收的粘包处理以及ble单次发送字节丢帧问题

前言

在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();
                }
            }
        }
    };
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容