如何用AI技术降噪? QCon 广州“音视频架构实践”专场给你答案! 了解详情
写点什么

Android 蓝牙 BLE 集成多设备最佳实践

  • 2017 年 1 月 23 日
  • 本文字数:3899 字

    阅读完需:约 13 分钟

背景

公司开发了一款健康类 APP,用户可以通过 APP 连接外部蓝牙 BLE 设备采集血糖,血压,体重等多个常见健康类指标。因此 APP 需要同时集成多款设备(多个品牌的血糖仪,血压计,体脂秤等)。每个厂家的设备对接协议是不同的,甚至连同一个设备的不同版本,协议都会有差距。在一个 APP 中跟多个设备对接,甚至在同一个界面中需要处理多个设备,界面跟协议混在一起,成为一个比较头疼的问题。本文对如何在 APP 中支持多设备集成,并且在需要对接新设备的时候,容易扩展现有代码提供了一个比较好的实践思路。

原有实现

原来在界面 Activity 类中,集成多个设备时候的代码通常如下所示(为了达到说明目的,代码做了很多简化,实际情况中设备对接的协议代码逻辑要复杂的多):

复制代码
private DeviceType mConnectedDeviceType; // 连接设备类型
/**
* 处理集成设备发过来的数据
* @param uuid
* @param data
*/
private void processDeviceData(UUID uuid,byte[] data){
// 首先判断当前连接设备类型,再分别处理
if (mConnectedDeviceType == DeviceType.A){
// 如果当前连接设备类型为 A, 根据本地硬编码的协议处理 A 类设备
processDeviceA(UUID uuid,byte[] data);
}else if (mConnectedDeviceType == DeviceType.B){
// 如果当前连接设备类型为 B, 处理 B 类设备
processDeviceB(UUID uuid,byte[] data);
}else if (mConnectedDeviceType == DeviceType.C){
// 如果当前连接设备类型为 C, 处理 C 类设备
processDeviceC(UUID uuid,byte[] data);
}
...
}
/**
* 处理 A 类设备的协议交互代码
* @param uuid
* @param data
*/
private void processDeviceA(UUID uuid,byte[] data){
int step = parseCmdA(uuid, data);
if (step == 0x1){
showResultA(data); // 显示测量结果
}else if (step == 0x2){
writeCmd("cmd2a"); // 向连接设备下发数据
}else if (step == 0x3){
writeCmd("cmd3a");
}
}
/**
* 处理 B 类设备的协议交互代码
* @param uuid
* @param data
*/
private void processDeviceB(UUID uuid,byte[] data){
int step = parseCmdB(uuid, data);
if (step == 0x1){
showResultB(data); // 显示测量结果
}else if (step == 0x2){
writeCmd("cmd2b"); // 向连接设备下发数据
}else if (step == 0x3){
writeCmd("cmd3b");
}
}
/**
* 处理 C 类设备的协议交互代码
* @param uuid
* @param data
*/
private void processDeviceC(UUID uuid,byte[] data){
int step = parseCmdC(uuid, data);
if (step == 0x1){
writeCmd("cmd2c"); // 向连接设备下发数据
}else if (step == 0x2){
showResultC(data); // 显示测量结果
}else if (step == 0x3){
writeCmd("cmd3c");
}
}

如何解决

以上代码可以看出界面跟设备协议是强耦合在一起的。如果需要集成更多的设备那怎么办?原有的类代码势必变得更复杂,难以维护。因此我们需要把设备间的协议交互逻辑与界面进行解耦,以保持单一职责的设计原则:界面只进行步骤和测量结果的更新展示,交互逻辑可以放到其他类中。在这个地方我们可以使用 Adapter 作为设备适配器,把设备间的交互封装到 Adapter 里面去,集成不同设备的时候调用不同的 Adapter 处理即可。

我们如何设计 Adapter 呢?虽然每个设备的交互协议不一样,但是其中一些操作却是共性的,比如一开始总是要连接设备,连接成功后设置指定 UUID 的 notification 或者 indication,然后向外部设备写入数据 (下发指令),或者等待外部设备数据变化上报,交互完成后再断开设备。

因此我们可以把这些共性操作抽象成 DeviceAdapter 接口。DeviceAdatper 接口主要包含上述的常用操作:

复制代码
UUID[] notificationUUIDs() // 设置 notification 的 UUID
UUID[] indicatorUUIDs() // 设置 indicator 的 UUID
void connectThenStart(BleDevice bleDevice) // 连接设备并进行协议交互
void disconnect() // 断开设备
void writeCharacteristic(UUID uuid, byte[] data) // 向指定 UUID 的 Characteristic 写入数据
void readCharacteristic(UUID uuid) // 从指定 UUID 的 Characteristic 中读取数据
void executeCmd(int cmd) throws EasyBleException // 执行命令接口
void processData(UUID uuid, byte[] data) // 解析外部设备发过来的数据

经过进一步的调研我们发现,设备的连接,断开连接,设置 notification/indication,写入数据,读取数据,这些操作本身都是完全一样的,不同的是我们对协议数据本身的解析。所以这些操作我们可以用一个默认的抽象类 DefaultAdapter 来实现,DefaultAdapter 实现 DeviceAdapter 接口,把对数据解析的功能延迟到子类去进行。针对 A 设备创建 DeviceAdapterA 继承于 DefaultAdapter,B 设备创建 DeviceAdapterB 继承于 DefaultAdapter,不同的设备用不同的 Adapter 去处理。

如图所示:

(点击放大图像)

解耦关键

Adapter 设计完成后,那调用模块(Client)又是如何知道针对 A 设备,用 DeviceAdapterA 处理;针对 B 设备用 DeviceAdapterB 处理的呢?

我们需要做到两点:

  1. Adapter 需要告诉客户:它能处理哪些设备。
  2. 需要把 adapter 管理起来,连接设备后需要能找到相匹配的 adapter 去处理设备。

解决第 1 点很简单,我们只需要在 DeviceAdapter 增加一个方法用来标识它能处理哪些设备,方法如下:

String[] supportedNames() // 返回的 String 数组代表它能处理的设备名组合第 2 点解决起来要复杂些,我们需要增加一个管理类 BleCenterManager(门面模式),BleCenterManager 的主要职责为:管理维护 adapter,并对不同设备找到相匹配的 adapter 进行处理,主要包含如下方法:

复制代码
public void startScan() // 开始蓝牙扫描
public void stopScan() // 停止蓝牙扫描
public void connectThenStart(BleDevice device) // 连接并处理设备
public void addDeviceAdapterFactory(DeviceAdapter.Factory factory) // 增加 Adapter 相应的 Factory

Adapter 创建

在深入讲解 BleCenterManager 之前,我们可以先谈谈 adapter 的创建。adapter 主要由客户代码根据交互协议创建,初始化的过程可能各不相同。因此 BleCenterManager 最好不直接创建 adapter,委托相应的 Factory 进行,也就是通常所说的工厂方法模式。客户提供 adapter 的时候,需提供与之对应的 Factory,BleCenterManager 负责管理这些 factories,创建 adapter 的时候只需要调用 factory.buildDeviceAdapter() 方法即可。Factory 针对抽象编程,设计为抽象类,核心代码如下:

复制代码
abstract class Factory{
protected BleCenterManager mBleCenterManager;
public Factory(BleCenterManager bleCenterManager) {
mBleCenterManager = bleCenterManager;
}
public abstract DeviceAdapter buildDeviceAdapter();
@Override
public String toString() {
return "Factory{}"+getClass().getName();
}
}

Factory 与 adapter 之间关系如下:

(点击放大图像)

查找Adapter 进行处理

Adapter 和 Factory 设计完后,通过 bleCenterManager.addDeviceAdapterFactory() 方法添加到 BleCenterManager 内部的 factory 列表,添加 factory 的同时,factory 创建对应的 adapter 并加入到 adapter 列表。添加完之后,BleCenterManager 是如何找到 device 相匹配的 adapter 进行处理的呢?答案很简单,逐一遍历 adapter 列表,查找 adapter 的 supportedNames() 方法返回的 String 列表是否包含设备名。查找到第一个就返回,如果列表遍历后查找不到就抛出异常。核心代码如下:

查找 Adapter```

private DeviceAdapter findAppropriateDeviceAdapter(BleDevice bleDevice) throws EasyBleException {
// 先判断 factory 是否为空
if (mDeviceAdapterFactories == null || mDeviceAdapterFactories.isEmpty()){
throw new EasyBleException(“Device adapter factories empty!”);
}
// 遍历 adapter 列表
for (DeviceAdapter adapter:mDeviceAdapters){
String[] nameList = adapter.supportedNames();
if (nameList != null && nameList.length > 0){
for (String name:nameList){
// 查找到名字符合的就返回
if (bleDevice.getDeviceName().equalsIgnoreCase(name)){
return adapter;
}
}
}
String[] nameRegExpList = adapter.supportedNameRegExps();
if (nameRegExpList != null && nameRegExpList.length >0){
for (String nameRegExp:nameRegExpList){
if (Pattern.matches(nameRegExp,bleDevice.getDeviceName())){
return adapter;
}
}
}
}
throw new EasyBleUnsupportedDeviceException(bleDevice);
}

复制代码
查找到 adapter 后,调用 adapter.connectThenStart() 方法进行后续协议交互处理。
看完 Adapter 这部分,很多人都会觉得有些熟悉,这个设计跟 Retrofit 的 CallAdapter 很类似。对的,好的设计都是相通的,只是换了个形式,都是常用设计模式: 适配器,工厂,单例等的组合。
## 结束语
通过 Adapter 与 BleCenterManager 的结合实现了协议逻辑与 APP 界面的解耦。上文中的代码只是基本核心示例代码,完整代码已经开源到 Github:<https://github.com/nziyouren/EasyBle> , 欢迎大家 contribute。目前库还处于初级阶段,后续逐步会加一些功能,比如从网络加载 adapter,如何在 APP 不升级版本的情况下,动态扩展集成能力。
- - - - - -
感谢 [徐川](http://www.infoq.com/cn/author/%E5%BE%90%E5%B7%9D) 对本文的审校。
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 [editors@cn.infoq.com](mailto:editors@cn.infoq.com)。也欢迎大家通过新浪微博([@InfoQ](http://www.weibo.com/infoqchina),[@丁晓昀](http://weibo.com/u/1451714913)),微信(微信号:[InfoQChina](http://www.geekbang.org/ivtw))关注我们。
2017 年 1 月 23 日 16:384067

评论

发布
暂无评论
发现更多内容

IDC发布中国区块链BaaS市场份额报告,腾讯云稳居TOP2

科技热闻

企业数字化转型,低代码是“趋势”还是“陷阱”?

云智慧AIOps社区

大前端 低代码 云开发

张平安:加快云上数字创新,共建产业智慧生态

华为云开发者联盟

云计算 后端 SaaS 华为云

【Unity】升级版·Excel数据解析,自动创建对应C#类,自动创建ScriptableObject生成类,自动序列化Asset文件

萧然🐳

游戏开发 Unity 7月月更 Excel工具

(1)长安链学习笔记-启动长安链

体验Python剪辑视频以及相关问题解决,一劳永逸!

迷彩

Python Moviepy视频剪辑处理 7月月更

长安链学习笔记-证书研究之证书模式

长安链

MetaForce原力元宇宙开发搭建丨佛萨奇2.0系统开发

开发微hkkf5566

1500万员工轻松管理,云原生数据库GaussDB让HR办公更高效

华为云开发者联盟

数据库 后端

Flutter3.0了,小程序不止于移动应用跨端运行

Speedoooo

flutter 小程序 移动开发 小程序容器 跨端运行

小程序能运行在自有App中,且实现直播和连麦?

Speedoooo

小程序 直播 移动开发 小程序容器 连麦

深度解读 RocketMQ 存储机制

阿里巴巴中间件

阿里云 RocketMQ 云原生 中间件 消息队列

从0开始创建小程序

小恺

7月月更

华为小米互“抄作业”

科技新知

阿里云易立:云原生如何破解企业降本提效难题?

阿里巴巴中间件

阿里云 架构 云原生

“去虚向实”大潮下,百度智能云向实而生

科技新知

面试题:AOF重写机制,redis面试必问!!!

知识浅谈

redis 底层原理

🚩🚩🚩建议收藏!!Flutter状态管理插件哪家强?请看岛上码农的排行榜!

岛上码农

flutter ios 安卓 移动端开发 7月月更

云原生混部最后一道防线:节点水位线设计

阿里巴巴中间件

阿里云 云原生 中间件 混部

解密函数计算异步任务能力之「任务的状态及生命周期管理」

阿里巴巴中间件

阿里云 中间件 异步 函数计算

新一代云原生消息队列(一)

技术小生

云原生 消息队列 7月月更

Web开发小妙招:巧用ThreadLocal规避层层传值

华为云开发者联盟

Java 前端 web开发

【C语言】 题集 of Ⅸ

謓泽

7月月更

低代码平台中的数据连接方式(上)

百度智能云

前端 低代码 数据格式 数据通信 爱速搭

TiFlash 源码阅读(四)TiFlash DDL 模块设计及实现分析

PingCAP

企业中台建设新路径——低代码平台

力软低代码开发平台

HAVE FUN | “飞船计划”活动最新进展

SOFAStack

微服务架构 开源软件 新手引导

SchedulX V1.4.0及SaaS版发布,免费体验降本增效高级功能!

星汉未来

DevOps 运维 k8s IT FinOps

java零基础入门-Scanner类

喵手

Java’ 7月月更

从解析HTML开始,破解页面渲染时间长难题

华为云开发者联盟

html 前端 web开发 网页

阿里云中间件开源往事

阿里巴巴中间件

阿里云 开源 中间件

「云智公开课」百度沧海·存储

「云智公开课」百度沧海·存储

Android蓝牙BLE集成多设备最佳实践_移动_章星星_InfoQ精选文章