直播蓝牙开发的爬坑全过程
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次后仍然没有收到回复的情况很多的情况下,那么该考虑换模块了。
最后,感谢某国产 某尔达 的破蓝牙厂商和我们的硬件工程师给我的这一次深刻的教训。
欢迎各位留言讨论,也希望能看到更多解决方案。