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

阅读数:3479 2017 年 1 月 23 日 16:38

背景

公司开发了一款健康类 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 发表评论