蓝牙多设备连接

前言

经过一个多月的时间蓝牙多设备连接的重构终于告一段落了,这次的重构不止是代码方面的完善,还结合了一些用户的使用场景,另外增加一些离线操作,使手机端对蓝牙的操作更加的便捷,对蓝牙设备的管理更加统一。

场景分析

支持的场景

  • 在饭点,使用同一个手机连接血压计和厨房秤,病人在测量血压,家人在做饭称量菜品—— 多设备连接(一个血压计和一个厨房秤)。
  • 方便下次自动连接——把连接到的设备信息保存到本地。
  • 方便在其他页面获取数据显示UI——把蓝牙返回结果保存本地。
  • 保持连接设备的唯一性,防止找不到连接的设备——同种设备只能连接一个。
  • 对蓝牙设备进行管理,查看自己扫描到的设备,并查看蓝牙的连接状态——可以在蓝牙列表扫描到所支持的设备,并连接、断开和删除蓝牙。
  • 可以快速的使用设备——启动 App 可以自动连接上一次已经连接过的设备。

暂不支持的场景

  • 同时扫描多个同种设备——可能导致连接到了设备,而不知道连接的是哪个设备。
  • 新旧设备替换——不能直接进行替换,需要在设备列表页面删除旧设备,之后关掉旧设备,重新扫描新设备。

场景实现

效果展示

蓝牙多设备连接效果图.gif

多设备连接

现在一台手机可以连接多个设备,例如连接蓝牙耳机,智能手环等。既然手机可以连接多个设备,那么移动应用也是可以连接多个设备的(血压计、心率计等),下面就是移动应用 App 实现多设备连接的思路方法。

实现思路

关于蓝牙连接,主要是 BluetoothGatt 这个类型,每个蓝牙的连接都需要用独立且唯一的 BluetoothGatt 。开始的想法是每个蓝牙都重新创建一个 Service, 在新的 Service 内使用 BluetoothGatt 进行连接,然而这个方法是可以实现多设备连接,但是创建多个 Service 对手机消耗比较大。之后,想到把 BluetoothGatt 保存起来不就可以了么,那用什么保存呢,既可以临时保存多个,又可以按照需要获取相对应的 BluetoothGatt 。在 java 里面有个类型 Map(String, Object) ,它是以 key-value 的形式存储到 Map 中。可以根据当时的 Key 来取相应的 Value 值,而且在关掉进程时相应的变量也就释放了。

代码实现

    private Map<String, BluetoothGatt> mBluetoothGattMap = new HashMap<>(); //临时保存 BluetoothGatt
    private Map<String, BluetoothGattCharacteristic> mGattCharacteristicMap = new HashMap<>();// 临时保存蓝牙的特征值 Characteristic
    private Map<String, BluetoothInfo> mBluetoothInfoMap = new HashMap<>();// 临时保存自己设置的蓝牙信息(deviceName、deviceType、startCMD、stopCMD 等)
    private Map<BluetoothGatt, String> mDeviceTypeMap = new HashMap<>();// 临时存储 deviceType 
    private Map<String, GGBLEDeviceEntity> mConnectModelMap = new HashMap<>();// 临时存储 已连接的设备
//...
/**
     * 连接设备
     *
     * @param deviceType 设备类型
     * @return true 连接成功,false连接失败
     */
    boolean connectBluetooth(Context context, String deviceType, String deviceAddress) {
       //...
        BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddress);//根据 mac 地址获取蓝牙设备
      //...
        BluetoothGatt bluetoothGatt = device.connectGatt(this, false, mGattCallback);// 对蓝牙进行连接操作
      //...
        mBluetoothGattMap.put(deviceType, bluetoothGatt);//把 BluetoothGatt 已 key-value 的形式临时保存起来

        return true;

    }

设备自动连接

要有良好的用户操作体验,我们应该避免对一些无关的操作重复进行。例如我们第一次打开应用连接了蓝牙设备,以后再打开 App 不需要重复操作连接过程,用户就可以少打开一个页面,少点击两次按钮。减少用户重复操作,让用户直接进入正题,提高主功能的使用率。

实现思路

在 Android 中连接蓝牙的方法是

public BluetoothGatt connectGatt(Context context, boolean autoConnect,
                                     BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect,callback, TRANSPORT_AUTO));
    }

其中 BluetoothGatt 是每个连接成功的蓝牙返回唯一的属性。当蓝牙设备连接成功后会返回唯一的 BluetoothGatt ,并用它进行对蓝牙的命令操作;autoConnect 是设置是否为自动连接的一个属性,然而根据我自己的测试,当 autoConnect 的属性设置为 true 时,是有可能自动连接的,但是有时也会失效,所以不采用;BluetoothGattCallback 是蓝牙连接、命令操作,数据返回等成功时的回调。
因为 autoConnect 的设置具有不确定性,所以我们采取另一种方式:当我们第一次连接蓝牙成功的时候,把蓝牙的 Mac 地址存储起来;在第二次启动 App 的时候,先把蓝牙的 Mac 地址从 SharedPreferences 中取出来,用 Mac 地址进行连接,如果连接失败(可能结果是设备不对或者设备没有打开),我们就开启蓝牙扫描功能,进行重新扫描设备,打开设备进行连接。

代码实现

/**
  *保存 mac 地址到 SharedPreferences
  */
public void saveMac(Context context, String macAddress){
if (null != context) {
            SharedPreferences sharedPreferences = context.getSharedPreferences(BLUETOOTH_MAC_TABLE, Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sharedPreferences.edit();
            editor.putString(deviceType, bluetoothAddress);
            editor.apply();
            GGLog.i(TAG, "save success");
        }
}

void autoConnect(Context context, final String deviceType) {
        /*防止蓝牙 adapter 为空,程序崩溃*/
        if (null == mBluetoothAdapter) {
            GGLog.e(TAG, "method discoveryBluetooth \n mBluetoothAdapter is null");
            return;
        }
        //根据 deviceType 获取 蓝牙 mac 地址
        SharedPreferences sharedPreferences = context.getSharedPreferences(BLUETOOTH_MAC_TABLE, Context.MODE_PRIVATE);
        String deviceAddress = sharedPreferences.getString(deviceType, "");
        if (!"".equals(deviceAddress)) {
            if (!deviceAddressList.contains(deviceAddress)) {
                deviceAddressList.add(deviceAddress);
            }
            connectBluetooth(context, deviceType, deviceAddress);// 连接设备
        }
    }

void connectBluetooth(Context context, String deviceType, String deviceAddress){
//...
// 连接失败 开启扫描功能
if (!bluetoothGatt.connect() || bluetoothGatt.getServices().size() == 0) {
            startScan();
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    stopScan();//十秒后停止扫描
                }
            }, 10000);
            return false;
        }
}

不同页面共享结果数据

在移动端我们很多时候,同一份数据要在多个页面上显示,又不想多次调用接口,我们只好把数据保存到本地,来达到数据共享的目的。

实现思路

在我这蓝牙的开发中,是使用 SQL 对数据进行保存。蓝牙测量中的数据因为是实时更新,我们不需要进行缓存,只要把测量的数据结果和蓝牙的信息存储下来就可以了。
使用 SQL 有一点不好的是使用 ContentValues 以 key-value 的形式进行存储,那么带来一个问题就是 key 我们手写很容易写错,所以可能一不小心就会万劫不复。在 Git 上有个轻量级的数据库第三方 Litepal (https://github.com/LitePalFramework/LitePal) ,它操作简单,不需要手写 key ,对一些常用的增删改查都进行了封装,而且还支持手写 SQL 语句,在 CSDN 上博客专家郭霖有详细的介绍 Litepal 的说明和使用方法。在这里我使用的是原生的 SQL ,防止与库外的第三方冲突。
蓝牙设备信息的保存,我存储了以下信息:

存储字段 类型 功能说明
deviceType String 设备类型,更具设备类型进行蓝牙操作
deviceAddress String 蓝牙的 mac 地址,用来连接蓝牙
deviceName String 蓝牙名称,扫描蓝牙时,进行校验
connectStatus int 蓝牙连接状态

对测量结果的存储内容是:

存储字段 类型 功能说明
deviceType String 设备类型,更具设备类型进行蓝牙操作
measuredData String 测量数据,把测量完成的数据进行保存,又 int[] 转成 String 进行保存
measuredTime long 测量时间
isUpdate boolean 上传状态,判断是否上传服务区,如果没有,则在有网络的情况下自动上传

代码实现

/**
     * 插入一条测量结果。因为数据库是封装在蓝牙库里面,我们获取不到 Application的 Context 所以传递一个 context
     */
    void insertComplete(Context context, CompleteModel entity) {
        SQLiteTemplate sqLiteTemplate = SQLiteTemplate.getInstance(context, instance.mBLEDBManager, false);
        ContentValues values = new ContentValues();
        values.put("deviceType", entity.getDeviceType()); //保存蓝牙设备类型
        values.put("measuredData", Arrays.toString(entity.getMeasuredData()));//保存测量结果
        values.put("measuredTime", entity.getMeasuredTime());// 保存测量时间
        values.put("isUpdate", entity.isUpdate());//保存是否已经上传完毕
        sqLiteTemplate.insert("ble_complete_table", values);// 数据插入表中
    }

这些都是移动端对数据库的简单操作,剩下的 更新、删除、查找的方法就不都一一列举了。对于蓝牙设备信息的存储与保存测量结果相似,也不列举了。

蓝牙搜索、连接、删除

既然我们有单独蓝牙列表页面,那么就要有对蓝牙的一些基本的操作。在列表页面我们可以对蓝牙进行搜索,发现周围打开的蓝牙设备;点击链接,连接我们需要使用的血压计、厨房秤等;长安断开链接并删除相应的蓝牙设备,当我们设备不再使用或者更换新设备的时候,我们可以删除多余的设备,使页面看起来更加简洁。

实现思路

首先,我们在BluetoothService里面对扫描到的蓝牙进行区分,检查是否是我们设定的的蓝牙设备(extends BaseBluetoothAdapter的类),根据设定的蓝牙名称(deviceName)筛选扫描到的设备,之后把设备通过 Listener 监听传递到 Activity 中,并添加到列表里显示出来。设备管理的勒图结构是:

设备灌流.png

代码实现

以下是 BluetoothService 中蓝牙扫描结果的处理

private void scanResult(BluetoothDevice device, int type) {
        if (null != device) {
            String name = device.getName();//获取扫描到的设备名称
            if (null != name) {
                //获取我们自己设定的蓝牙详情(deviceType、deviceName等)
                for (BluetoothInfo bluetoothInterface : mInterfaceList) {
                    String deviceName = bluetoothInterface.getDeviceName();
                    //判断设备名称是否与我们自己设定的名称相同
                    if (!deviceName.equals(name)) {
                        continue;
                    }
                    //判断设备是否已经添加到列表中
                    if (deviceNameList.contains(deviceName)) {
                        continue;
                    }
                    // 把设备名称添加到列表中
                    if (!deviceNameList.contains(deviceName)) {
                        deviceNameList.add(deviceName);
                    }
                    //如果扫描到的设备是上次连接过的设备,则自动连接。
                    if (null != deviceAddressList && deviceAddressList.size() > 0) {
                        for (String address : deviceAddressList) {
                            if (address.equals(device.getAddress())) {
                                connectBluetooth(null, bluetoothInterface.getDeviceType(), address);
                            }
                        }
                    }
                    GGBLEDeviceEntity entity = BLEDeviceToGGBLEEntity(bluetoothInterface.getDeviceType(), device);//把bluetoothDevice 转换成我们自定义的BLEEntity。
                    if (null != mResultListener) {
                        mResultListener.onScanResult(entity);//扫描结果监听赋值
                    } else {
                        setError(bluetoothInterface.getDeviceType(), HHCBluetoothProfile.ERROR_NULL, "mResultListener is null");
                    }
                }
            }
        }
    }

其他

  • 在多设备连接中,需要时刻获取到连接的设备是什么,才能获取到相应的数据。通过 onBluetoothConnectStatus 接口方法只能在连接状态发生改变的时候才会回调,不能在已进入页面的时候就获取到设备的连接状态,因此我们在 BluetoothService 中提供两个方法:
//根据设备类型获取连接的蓝牙设备
GGBLEDeviceEntity getConnectedDevice(String deviceType) {
        return mConnectModelMap.get(deviceType);
    }
//获取所有的连接设备
List<GGBLEDeviceEntity> getAllConnectedDevice() {
        List<GGBLEDeviceEntity> connectModelList = new ArrayList<>();
        for (String type : mConnectModelMap.keySet()) {
            connectModelList.add(mConnectModelMap.get(type));
        }
        return connectModelList;
    }
  • 在 service 中不能持久化 Context ,如果定义了 Context 会发生内存溢出,所以在我们所提供的方法中都会传入 context 用于承接上下文。
  • 我把 service 内对外开放的方法都集中在一个类里,方便调用和管理。
  • 蓝牙扫描的方法有三种:
    1、在5.0之前使用的是:mBluetoothAdapter.startLeScan(mLeScanCallback);;
    对应的停止扫描为:mBluetoothAdapter.stopLeScan(mLeScanCallback);
    2、在 Android 5.0之后 使用的是:mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);;
    对应的停止扫描为:mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
    3、一种通用的扫描方法:mBluetoothAdapter.startDiscovery();
    对应的停止扫描为:mBluetoothAdapter.cancelDiscovery();

改进之处

  • 在这次的开发中,主要面临的问题是场景方面的考虑,开始只是单一的想要进行多个设备的连接,没有考虑现实中的场景,做出的第一个版本,只能在同一个 Activity 中进行多设备连接,而不能各自在不同的页面进行操作,数据也不能共享;之后和 leader 多次讨论,想到很多场景,结合这些场景,罗列出开发功能,并调整代码结构,让使用者使用起来更加方便。总之,一切的功能开发都是需要场景来支撑。

  • 在数据存储这方面还有待改进,手写的 key-svalue 形式很容易出现书写错误,即使写错也不容易找到错误出处。改进方向是数据存储直接存储对象而不是一个一个的 key-value 进行存储。

  • demo 地址:https://github.com/wdmxzfG/MultiBluetooths

小结

此篇文章主要讲述了在一些场景的支撑下对蓝牙设备的开发,描述多台不同类型的设备同时运行,并能够在启动 App 时可以自动连接;提供蓝牙列表页面对设备进行管理;对蓝牙设备信息和测量结果进行持久化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 一、同时连接多个设备,并读取信号强度(RSSI) iOS蓝牙支持一个中心连接多个蓝牙设备,最多7个。根据CoreB...
    半月迎风阅读 12,719评论 13 22
  • Guide to BluetoothSecurity原文 本出版物可免费从以下网址获得:https://doi.o...
    公子小水阅读 7,882评论 0 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,401评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,587评论 18 139
  • When... founded... in..., he chose the right time.... hel...
    luhu阅读 137评论 0 0