BT

你的观点很重要! 快来参与InfoQ调研吧!

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

| 作者 章星星 关注 0 他的粉丝 发布于 2017年1月24日. 估计阅读时间: 18 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

A note to our readers: As per your request we have developed a set of features that allow you to reduce the noise, while not losing sight of anything that is important. Get email and web notifications by choosing the topics you are interested in.

背景

公司开发了一款健康类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不升级版本的情况下,动态扩展集成能力。


感谢徐川对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT