直播蓝牙开发的爬坑全过程,快来看看吧

针对蓝牙模组在低端手机上搜索困难、连接不稳定、数据分包混乱等问题,本文提出了一系列解决方案,包括更换蓝牙模组、优化协议处理逻辑、使用RxJava定时器确保数据完整接收等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

直播蓝牙开发的爬坑全过程


2018年3月,我正式接手了公司委派的新项目。在项目中,我负责蓝牙开发部分,需要与硬件对接。
当我开发完毕后,在测试的过程中发现了一些问题:

产生的问题

  • 该蓝牙模组在某些低端手机上很难搜索到,即使搜索出来,连接也不稳定;
  • 在读取一些特性的时候,返回给我的包长度不固定,比如回复的长度为15位,有时会分为2包,有时候会分为3包返给我;
  • 响应的时间较长,有时发送一条协议后,需等待2秒才能收到回复;
  • 有时候连续发送协议后,蓝牙模组会发生堵塞现象,有某些情况下,会把以前堵塞的数据一并返回;
  • 连续读写一定次数后,蓝牙模组卡死,发送任何协议都无响应,只有断电重启才能解决。发送间隔越小,卡死的速度越快;
  • 蓝牙信号不稳定,在10cm之内,还是比较稳定的,10cm之外,信号衰减十分明显,到5~6米的时候,信号衰减为-80左右,接近断开的状态;
  • 通信不稳定,连接后断开的现象十分频繁
  • 通信过程中,数据丢失频有发生;
  • 收到的回复不遵循队列,在客户端分别发送A、B、C三个协议后,有时收到的顺序却是B、C、A,加上分包不明确,在解析的过程中很难一次成功。

项目是公司是与其他公司合作的,硬件的部分协议已经写完了,后面新加的协议亦遵循之前的协议,一下是我们硬件工程师制定的通信协议:
在这里插入图片描述

使用各位的肉眼可以看到,协议中缺失了回复长度,也就是说,在收到回复的时候无法得知该条协议有多长,只能靠自己一次一次去计算校验和。
我戳。。。
由于以上种种原因,项目写得很坎坷。

解决方案

  • 对于协议制定不规范的情况,如果是一次一次的读取,协议里有无长度并无关系;如果业务中需要一次性读取多个值,此时蓝牙的可靠性将变得十分重要,对于数据的处理也要求很严格,此时可以在收到回复的回调函数中;
  • 对于数据丢失的情况,可以在回复的地方将数据保存为全局变量,发送指令后延迟一段时间,读取全局变量的内容,再做进一步解析,若有数据丢失,可重新发起请求再次获取,重复几次直到收到回复为止;
  • 对于蓝牙很难搜到的情况,提供一些建议:
  • 判断客户使用场景,若用户使用的设备为指定设备,且该设备可以轻易搜索到蓝牙,则无须在意
  • 若客户的设备不固定,建议更换蓝牙模块或联系蓝牙厂商查找解决方案
  • 在分包不明确的情况下,(部分厂商的分包机制很烂,代码不可控制且分包不固定)建议使用一个集合来保存所有的数据,待发送所有指令后延迟2-3秒钟(根据蓝牙模块的性能调整),将数据按照HEADER、SUM值进行分割,校验,最后解析全部数据。此种方案可能会遇到协议中Body的值与HEADER的值相同造成分包错误的情况,所以在解析的时候要格外注意;
  • 对于响应的时间较长的情况,建议联系厂家提供帮助;
  • 对于发生堵塞现象、卡死、不遵守队列、丢包的情况,建议直接换模块;
  • 对于连接不稳定,信号差、断开频繁的情况,请检查蓝牙天线与蓝牙模组的兼容问题,如果实在解决不了,建议更换模块。在挑选模块的时候,应选择厂商建议的天线等配件。

由于以上种种原因,客户紧逼,迫于无奈,遂更换了蓝牙模组。

更换模组的时候,我觉得换个蓝牙而已,代码变动应该不是很大,只要把搜索时过滤的蓝牙名字改一下,读写的UUID改一下就好了,但是在更换模组之后,我发现新换的模组UUID的规则与之前的不同:

上一个模组的结构是,一个主服务UUID下有两个UUID分别负责读写;而新模组的结构是,读写分离,也就是说,读的UUID上级有一个主服务UUID,写的UUID上级有一个主服务UUID,这样子我代码里改动的地方就比较多了
因此,在我的项目中,在连接蓝牙后,使用全局变量保存读、写、和两个主服务的UUID,不管是有一个主服务还是两个,都保存一次,这样子,即使再更换蓝牙模块,这里也不需要大改。


    在完成这一步骤后,我以为已经完成了我的工作,刚放了一口气,却在调用indicate的时候失败了。我戳。

原来,蓝牙模块并不是indecate和notify都支持。卧槽

  • 关于indecate和notify,这里引用一句理论:indecate和notify的区别就在于,indecate是一定会收到数据,notify有可能会丢失数据。

旧蓝牙模块使用的是indecate,当我改成notify的时候,通知打开正常。
如果我把项目中的indecate改为Notify也不是什么难事,但已经有一部分硬件投入了市场,若我都改成notify,以前旧的模块就无法使用了。
以下是我项目中解决的思路,先将UUID保存起来,然后判断这些UUID支持的是哪种类型的通知:(Android代码,IOS同理)


	List<BluetoothGattService> bgs = gatt.getServices();
        for (int i = 0; i < bgs.size(); i++)
        {
            // 旧版BLE,使用indecate
            if (bgs.get(i).getUuid().toString().toLowerCase().startsWith(BeanBLEConstants.OLDER_BLE_WRITE))
            {
                BluetoothGattService mService = gatt.getServices().get(i);

                BeanBLEConstants.setUuidReadServerStr(mService.getUuid().toString());
                BeanBLEConstants.setUuidReadStr(mService.getCharacteristics().get(0).getUuid().toString());

                BeanBLEConstants.setUuidWriteServerStr(mService.getUuid().toString());
                BeanBLEConstants.setUuidWriteStr(mService.getCharacteristics().get(2).getUuid().toString());

                BeanBLEConstants.setCommType(isNotify(mService.getCharacteristic(UUID.fromString(BeanBLEConstants.getUuidReadStr()))));
                return;
            }
            // 新版BLE,使用notify
            if (bgs.get(i).getUuid().toString().toLowerCase().startsWith(BeanBLEConstants.NEWER_BLE_READ))
            {
                BluetoothGattService mService = gatt.getServices().get(i);
                BeanBLEConstants.setUuidReadServerStr(mService.getUuid().toString());
                BeanBLEConstants.setUuidReadStr(mService.getCharacteristics().get(0).getUuid().toString());
                BeanBLEConstants.setCommType(isNotify(mService.getCharacteristic(UUID.fromString(BeanBLEConstants.getUuidReadStr()))));
                continue;
            }
            if (bgs.get(i).getUuid().toString().toLowerCase().startsWith(BeanBLEConstants.NEWER_BLE_WRITE))
            {
                BluetoothGattService mService = gatt.getServices().get(i);
                BeanBLEConstants.setUuidWriteServerStr(mService.getUuid().toString());
                BluetoothGattCharacteristic characteristic = mService.getCharacteristics().get(0);
                BeanBLEConstants.setUuidWriteStr(characteristic.getUuid().toString());

                return;
            }
        }
    /**
     * 判断该蓝牙模块支持哪些通知,以及决定未来使用何种方式来交互
     *
     * @param bgc
     * @return n为notify,i为indecate
     */
    public String isNotify(BluetoothGattCharacteristic bgc)
    {
        int charaProp = bgc.getProperties();
        if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0)
        {
            return "n";
        } else if ((charaProp & BluetoothGattCharacteristic.PROPERTY_INDICATE) > 0)
        {
            return "i";
        }
        return null;
    }

这样,在使用的时候,判断它支持的是哪种类型,然后再进行交互,这样不管换多少蓝牙模块,代码的变动都会变得很小。


补救

对于一些蓝牙有丢包的情况,若在无法更换蓝牙模块的情况下,这里提供一个思路来增加成功的几率:


     /**
     * 合成协议包
     * @param isFirst 是否第一次请求
     */
    private void onGenerateReadParm(boolean isFirst)
    {
        read_parm = new AgreementUtils().getReadAll(new AgreementUtils().splitAdjustList()); // 
        replyCount = 0;
        if (isFirst)
        {
            replyCount = 0;// 清空临时缓存
            cacheList.clear();// 清空临时缓存
            tempBytes.clear();// 清空临时缓存
            BeanStaticQHLParm.initAdjust(); // 清空临时缓存
        } else // 非第一次加载
        {
            read_parm.clear();
            read_parm = new AgreementUtils().getReadAll(new AgreementUtils().splitAdjustList());
            if (read_parm.size() == 0)
            {
                mView.bindValue();
                mView.showDialog("", false);
                return;
            } else
            {
                if (reReadCount == 4)
                {
                    mView.showDialog("", false);
                    mView.showDialog("抱歉", "读取数据异常,请重试。如多次发生请断电重启电路板。",
                            "取消", "确定", () -> {
                            }, null, true).show();
                    return;
                }
            }
        }
        for (BeanAgreements beanAgreements : read_parm) // 计算应该回复的所有字节长度
        {
            replyCount = replyCount + beanAgreements.getReplyLength();
        }
        timerRead = Observable.interval(Constants.READ_TIME, Constants.READ_TIME, TimeUnit.MILLISECONDS)
                .compose(mActivity.bindToLifecycle())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .take(read_parm.size());
        reReadCount += 1;
        readParm(read_parm);
    }

这个函数主要是生成协议集合的,getReadAll() 方法可以返回所有协议的集合,splitAdjustList() 可以返回当前没有收到回复的协议,第一行代码会生成当前没有收到数据的所有协议的集合。第一次加载,会清空已经缓存好的数据,然后将所有协议组合起来分别发送;然后使用RxJava提供的定时器每隔150毫秒发送一次。当所有数据发送完毕,延迟2秒,检查缓存里是否已经有值,若没有收到数据或数据异常,则再次调用该函数,传入isFirst为false,再重复读4次,若期间有全部成功的情况,则停止发送。现在我们看一下BeanAgreements的结构:

public class BeanAgreements
{
    private String instructDesc; // 协议请求描述
    private byte[] instructContent; // 协议请求内容
    private int instructLength; // 协议

    private byte[] replyContent; // 协议
    private int replyLength; // 长度
}

由于协议不规范,我只能将协议封装为对象,手动填写长度等数据。现在再看**for()**循环体,你应该了解到,这里是计算每次回复的所有字节的长度,可以在收到的时候检查长度查看是否有丢失包的情况。最后,发送到蓝牙。

现在看一下使用RxJava的定时器发送数据:

    @Override
    public void readParm(List<BeanAgreements> baList)
    {
        timerRead.subscribe(new Observer<Long>()
        {
            @Override
            public void onSubscribe(Disposable disposable)
            {
                mReadDispose = disposable;
            }

            @Override
            public void onNext(Long aLong)
            {
                byte[] mReadParm = baList.get(aLong.intValue()).getInstructContent();
                mReadParm[mReadParm.length - 1] = (byte) new ConvertUtil().getJYH(mReadParm); // 追加校验和
                onWrite(mReadParm);
            }

            @Override
            public void onError(Throwable throwable)
            {
                throwable.printStackTrace();
                mView.showDialog("", false);
                mView.showSnackBar(throwable.getMessage());
            }

            @Override
            public void onComplete()
            {
                if (reReadCount <= 4)
                {
                    new Handler().postDelayed(() -> onGenerateReadParm(false), 2000);
                }
            }
        });
    }

这里讲生成的协议包合并为集合,在onNext() 中取到协议体,并计算出校验和,在 onWrite() 中开始发送到蓝牙;当发送完毕后,延迟2秒钟开启一个线程来查看是否已经缓存到回复,若没有收到回复,则继续调用生成协议的函数。

至此,一个非常兼容垃圾蓝牙模组的代码已经完成,当重复请求5次后仍然没有收到回复的情况很多的情况下,那么该考虑换模块了。


最后,感谢某国产 某尔达 的破蓝牙厂商和我们的硬件工程师给我的这一次深刻的教训。

欢迎各位留言讨论,也希望能看到更多解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值