安卓 BLE 开发详解


相关概念

  • BR
    Basic Rate,早期的传统蓝牙技术 V1.1, V1.2 版本,传输速率为748~810kb/s。

  • EDR
    Enhanced Data Rate,传统蓝牙技术 V2.0, V2.1 版本,优化传输速率,减少耗电,速率为1.8M/s~2.1M/s。

  • AMP
    GenericAlternate MAC/PHY,高速蓝牙技术,V3.0版本。
    采用交替射频技术,蓝牙模块仅创建设备间的配对,数据传输通过WIFI射频来完成以达到高速率。
    假如设备某一方没有内建WIFI模块,速率将降至 EDR 速率。

  • BLE
    Bluetooth Low Energy,低耗蓝牙技术,V4.0版本的新规范,通过三个方式实现超低功耗:
    1.大幅度削减扫描信道
    2.极短的链路连接时间
    3.采用长度很短的数据包
    低耗蓝牙的芯片有单模和双模,前者只支持LE技术,后者兼容BR/EDR技术。


1:GATT 协议

  • GATT概述
    GATT(Generic Attributes,通用属性协议),定义了一种面向 BLE设备 的分层数据结构。
    GATT建立在ATT( Attribute Protocol,通用访问协议)之上,ATT使用GATT数据定义两个BLE设备间收发标准消息的方式。
    由于 GATT 是面向 LE 技术的协议,所以在只支持 BR/EDR 技术的设备上无法使用。

  • GATT分层数据结构的层次

    GATT定义了用于BLE设备传输数据的标准数据结构,结构主要包括了如上图所示的:
    1.服务(Service)
    2.特征(Characteristic)
    3.描述符(Descriptor)。

  • 配置文件(Profile)
    配置文件,GATT顶层,该由满足 配置实例 需要的一个或多个服务组成。

  • 服务(Service)
    服务 由 特征 和 其他服务的引用 组成,拥有固定的 UUID 作为标记值。
    设备的功能主要体现在服务上,每种服务都对应着某一种功能。

    可以到官网上查看服务列表 GATT Services
    通过服务列表中的 Assigned Numbers 可以获取服务的UUID。

    Assigned Numbers转换成可用的服务UUID 的方法于文档 Service Discovery
    简单来说,就是:

      "服务的Assigned Numbers"-0000-1000-8000-00805F9B34FB
    
  • 特征(Characteristic)
    特征是BLE通信的主体,是一个服务端和客户端共享的读写空间。
    主机在从机上获取所需的信息,实际就是通过获取对应的特征的内容进行的。

    特征由属性值和描述符组成:

    1. 属性值
      属性值包括声明(Declaration),值(Value),一个属性值最少包括一个声明和一个值,即是属性值是特征必选的条目。
    2. 描述符
      特征可以包括零到若干个描述符,可选条目。

    特征信息列表可以查看官方文档 GATT Characteristics

  • 描述符(Descriptors)
    用于表达 特征 的其他附加信息,如特征值的有效范围,可读性描述等信息。

    其中包含了特殊的 CCCD(Client Characteristic Configuration Descriptor, Assigned Number : 0x2902):
    CCCD 可以设置 服务端 在对应特征值发生变化时,是否对 客户端 进行信息 推送(直接发送信息) 或 提示(发送一个提示并等待回复)。
    当特征包含通知能力时,CCCD为必选项。

    描述符列表可以查看官方文档 GATT Descriptors


2:Android BLE 相关 API

  • BluetoothAdapter
    蓝牙适配器:

    本地设备蓝牙适配器,提供基本蓝牙功能的工具,例如开启蓝牙发现,查询配对设备,实例化蓝牙设备链接,监听连接请求,扫描设备等。
    基本上说,蓝牙适配器是进行蓝牙操作的起点。

    获取BluetoothAdapter实例,在 API 18 及以上的设备,使用:

    BluetoothManager.getAdapter
    

    在API18以下设备使用以下API获取:

    BluetoothAdapter.getDefaultAdapte
    

    本类线程安全。涉及到的权限为:

    <uses-permission android:name="android.permission.BLUETOOTH"/> 
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    
  • BluetoothDevice
    远程蓝牙设备:

    提供了远程蓝牙设备的基本信息,如名称,地址,类别,绑定状态等。
    本质上只是对蓝牙硬件地址的简单包装。该类的实例不可修改。

    一般来说,通过扫描设备的扫描结果回调中获取。
    也可以直接通过以下方式获取:

    /* 使用已知的物理地址作为参数进行连接 */
    BluetoothAdapter.getRemoteDevice(address);
    /* 获取已适配的蓝牙记录列表 */
    BluetoothAdapter.getBondedDevices();
    
  • BluetoothGatt
    GATT客户端,GATT协议的公共API,提供了GATT的基本功能,如实现蓝牙设备的通信。
    通过扫描支持LE技术的蓝牙设备,获取到 BluetoothDevice,然后通过:

    /* GATT连接操作的回调 */
    BluetoothGattCallback mCallback;
    BluetoothDevice.connectGatt(content, autoConnect, mCallback);
    

    通过设置 BluetoothGattCallback 回调,可以从回调中得到 BluetoothGatt 实例。

  • BluetoothGattCallback
    GATT状态回调,大部分GATT操作的结果都会通过该类实例回调,包括:

    /* 连接状态回调,包括连接到服务器 / 从服务器断开连接 */
    onConnectionStateChange();
    /* 远程设备发现新服务 */
    onServicesDiscovered();
    /* 特征相关操作的回调 */
    onCharacteristicRead();
    onCharacteristicWrite();
    onCharacteristicChanged();
    

    同时,扫描设备 和 停止扫描 的操作,都需要用到该类的实例。

  • BluetoothGattService
    GATT服务,根据服务的 UUID,尝试获取服务实例。

    /* 如果对应的设备支持该服务,则返回一个服务的实例,否则返回空 */
    BluetoothGatt.getService(uuid); 
    
  • BluetoothGattCharacteristic
    GATT特征,实际通信中的数据信息主体。通过以下方法获取:

    /* 获取对应UUID的特征 */
    BluetoothGattService.getCharacteristic(uuid);
    /* 获取服务的特征列表 */
    BluetoothGattService.getCharacteristics();
    

3:Android BLE 开发示例

  • 声明权限

    一个声明和两个基本权限:

    <uses-feature android:name"android.permission.BLUETOOTH_ADMIN"/>
    
    <uses-permission android:name="android.permission.BLUETOOTH"/> 
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 
    

    执行搜索BLE设备的时候,需要使用定位权限。
    而在5.0及以上的版本,需要手动声明GPS硬件模块功能的权限:

    <uses-feature android:name="android.hardware.location.gps"/>
    

    而在6.0及以上版本,扫描设备还需要 动态申请 以下权限:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
  • 检查设备支持性
    如果设备不支持BLE,可以跳过BLE相关操作了。

    boolean checkSupport() {
        return getPackageManager()
                  .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
    }
    
  • 初始化BluetoothAdapter

    private BluetoothAdapter mAdapter;
    
    BluetoothManager bluetoothManager = 
        (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mAdapter = bluetoothManager.getAdapter();
    

    然后检查蓝牙的支持性,及是否已打开蓝牙。

    if (mAdapter == null) {
        return;
    }
    
    ... private final static int REQUEST_ENABLE_BT = 1;
    
    if (!mAdapter.isEnabled()) {
        Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(intent, REQUEST_ENABLE_BT);
    }
    
  • 启动设备扫描

    创建LeScanCallback实例:
    首先需要实现一个 LeScanCallback 实例,扫描结果会通过实例的 onLeScan 方法返回:

    LeScanCallback mCallBack = new LeScanCallback (){ 
         @Override
         public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {}
    }
    

    启动扫描与停止扫描:

    ··· static int SCAN_TIME = 5_000;
    ··· Handler mHandler = new Handler();
    
    /* 开始扫描:
       由于扫描消耗电量,所以不能一直处于扫描状态,
       设置扫描一段时间后关闭扫描 */
    mAdapter.startLeScan(mCallBack);
    mHandler.postDelay(()->{
    
       /* 关闭扫描:
        * 注意需要传入启动扫描时的 callback对象,否则无效 */
       mAdapter.stopLeScan(mCallBack);
    }, SCAN_TIME);
    

    API 21 及以上时,扫描操作应使用 BluetoothLeScanner

    final ScanCallback callback = new ScanCallback() {};
    final BluetoothLeScanner scanner = mAdapter.getBluetoothLeScanner();
    
    scanner.startScan(new ScanCallback(){});
    mHandler.postDelay(()->{
       scanner.stopScan(scanCallback);
    }, SCAN_TIME);
    
  • 获取扫描结果
    以 LeScanCallback 的回调方法 onLeScan 分析:

     /**
      * @param device:    识别到的远程设备
      *
      * @param rssi:      信号强度指示,计数为dB。可以通过:
                           d = 10^((abs(RSSI) - A) / (10 * n)) 
                           计算出距离。A和n根据环境改变,需经实验测出,
                           给出两个网上的经验值:
                           <1>  A: 50 n: 2.5    <2>  A: 59 n: 2.0
      *
      * @param scanRecord:广播数据和扫描应答数据数据
                           BLE设备在对外广播中,广播中会携带一些有用的信息。
                           其中包含了 广播数据 和 扫描应答数据,
                           两者有效荷载最大都为 31字节(蓝牙4),
                           以十六进制格式存储,可通过 bytesToHex 转换成可用的字符串。
      */
     void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {}
    

    注意相同的 BluetoothDevice 会重复出现在回调中,所以如果要记录蓝牙列表,需要自行 过滤 重复出现的设备,或更新对应重复出现的设备的信息。
    bytesToHex 参考

  • 连接外围设备
    通过 BluetoothDevice 的 connectGatt 方法获取一个 BluetoothGatt 实例。
    connectGatt 有多个重载方法,这里介绍其中最复杂的重载方法:

     /**
      * 以客户端的身份连接到该设备托管的GATT服务器
      *
      * @param autoConnect:自动连接,设备不可用时会不断尝试重连。
      *
      * @param callback:   BluetoothGattCallback实例,用于接收异步回调
      *
      * @param transport:  GATT连接到双模设备的首选传输模式:
      *                     1:TRANSPORT_AUTO   自动选择 (默认值)
      *                     2:TRANSPORT_BREDR  BR/EDR 传统蓝牙
      *                     3:TRANSPORT_LE     LE 低耗蓝牙
      *
      * @param phy:        PHY物理层的模式选择:
      *                     1:PHY_LE_1M_MASK:
      *                        默认值,LE设备强制要求支持的模式,
      *                        符号速率为1M/s,未编码。
      *                     2:PHY_LE_2M_MASK:
      *                        符号速率为2M/s,未编码,
      *                        用于 蓝牙5 的 "2x speed" 2倍速率。
      *                     3:PHY_LE_CODED_MASK:
      *                        在数据包中增加纠错编码以实现更远的传输范围,
      *                        以实现 蓝牙5 的 "4x range" 4倍范围。
      *                        使用FEC编码,根据方案又分为:
      *                        LE Coded S=2:2个编码位代替原来一个数据位,
      *                                      速率降为 500K/s,传输范围增大2倍;
      *                        LE Coded S=8:8个编码位代替原来一个数据位,
      *                                      速率降为 125K/s,传输范围增大4倍;
      *                     设置 autoConnect 自动连接时,该项无效
      *
      * @param handler:    传入一个Handler,以指定回调发生的线程,
      *                     传入null时,回调将会在一个未指定的后台线程上进行。
      */
     BluetoothGatt connectGatt(Context context,
                               boolean autoConnect,
                               BluetoothGattCallback callback, 
                               int transport, int phy,
                               Handler handler) { ··· }
    

    一般情况下使用默认值既可,
    注意必须传入非空的callback,否则会抛出 IllegalArgumentException

    BluetoothDevice.connectGatt(content, autoConnect, callback);
    

    当连接成功时,会回调 callback 的 onConnectionStateChange 方法

    /**
     * GATT客户端的连接状态回调
     *
     * @param gatt:    GATT客户端。
     * @param status:  连接或断开操作的执行结果, 成功返回 GATT_SUCCESS
     * @param newState:当前的连接状态:STATE_CONNECTED / STATE_DISCONNECTED
     */
    void onConnectionStateChange(BluetoothGatt gatt, int status, int newState);
    

    status 表示连接操作的结果,只有status为 GATT_SUCCESS 时,newState才是有效值。

    注意一台安卓设备最多同时连接6个左右的蓝牙设备,超出时可能出现:
    status == 133 连接错误,
    所以需要注意调用 BluetoothGatt.close() 方法进行资源释放。
    可参考:Android中BLE连接出现“BluetoothGatt status 133”的解决方法

    当 status == GATT_SUCCESS,且 newState == STATE_CONNECTED 时,表示已成功连接设备,可以进行下一步操作。

  • 发现服务
    在建立连接之后,就可以通过 BluetoothGatt实例 进行发现服务操作,查找设备支持的服务。

    /**
     * 异步操作,发现服务完成时,会回调onServicesDiscovered()方法。
     * 假如发现服务已在启动状态中,则返回true
     */
    boolean discoverService();
    

    等待 BluetoothGattCallback 的 onServicesDiscovered() 被回调:

    /**
     * @param gatt:   执行发现服务后的GATT客户端。
     * @param status: 发现服务的执行结果, 成功返回 GATT_SUCCESS
     */
    void onServicesDiscovered(BluetoothGatt gatt, int status) ;
    

    当 status 返回GATT_SUCCESS,表示与外部设备成功建立 可通信连接
    意味着可以执行如:写入数据,读取蓝牙设备的数据等 蓝牙通信操作了。
    先把获取到的 BluetoothGatt实例 记录为 mGatt:

    ··· BluetoothGatt mGatt;
    
    void onServicesDiscovered(BluetoothGatt gatt, int status) {
        mGatt = gatt;
    }
    
  • 获取服务
    发现服务成功之后,可以通过以下的方法尝试获取 BluetoothGattService 实例:

    /* 获取远程设备提供的服务列表,
     * 如果未执行发现服务,会返回一个空列表 */
    mGatt.getServices();
    
    /* 通过服务的UUID,获取指定的服务,
     * 如果远程设备不支持给定UUID的服务,返回null,
     * 如果远程设备存在多个给定UUID的服务实例,则返回第一个实例 */
    mGatt.getService(UUID);
    

    获取到 BluetoothGattService 之后,就可以通过获取服务的特征进行读写。

  • 特征的读写数据
    前面介绍了,通信主体实际上是 特征,要进行读写操作,其实就是在操作特征里的属性词条,所以要先通过 服务 获取 特征:

    /* 假设 service 是从上一步获取到的一个 BluetoothGattService 实例*/
    ··· BluetoothGattService service;
    
    /* 获取该服务的特征列表 */
    service.getCharacteristics();
    
    /* 通过特征的UUID,获取指定的特征,
     * 如果没有找到给定UUID的特征,返回null,
     * 如果服务中存在多个给定UUID的特征,则返回第一个实例 */
    service.getCharacteristic(UUID);
    

    获取到了特征之后,就可以通过上面获取到的 mGatt 读写信息:

    /* 上一步获取的 BluetoothGattCharacteristic 实例 */
    ··· BluetoothGattCharacteristic characteristic;
    
    /* 从关联的远程设备读取请求的特征,
     * 异步操作,请求发起成功则返回true,读取完成会回调:
     * BluetoothGattCallback.onCharacteristicRead() */
    mGatt.readCharacteristic(characteristic);
    
    /* 将给定的特征及其值写入关联的远程设备,
     * 异步操作,请求发起成功则返回true,写入完成会回调:
     * BluetoothGattCallback.onCharacteristicWrite() */
    mGatt.writeCharacteristic(characteristic);
    

    读写操作都是异步操作,方法返回的是请求是否成功,请求结果都会回调 BluetoothGattCallback 的方法:

    /**
     * 读操作的回调
     * @param characteristic:  读取后的特征
     * @param status:          读取结果,成功为 GATT_SUCCESS
     */
    void onCharacteristicRead(BluetoothGatt gatt, 
                              BluetoothGattCharacteristic characteristic,
                              int status) { ··· }
    
    /**
     * 写操作的回调
     * @param characteristic:  写入后的特征
     *                          注意:这里返回的特征,为设备当前的特征, 
     *                          应该在该回调中,应对比该特征的内容是否符合期望值,
     *                          如果与期望值不同,应该选择重发或终止写入。                    
     * 
     * @param status:          写入结果,成功为 GATT_SUCCESS
     */
    void onCharacteristicWrite(BluetoothGatt gatt, 
                              BluetoothGattCharacteristic characteristic,
                              int status) { ··· }
    

    写数据的时候要注意,需要对比返回的特征和写入的特征,判断是否写入成功或者产生了异常,选择继续写入或者重写,或者放弃操作。

  • 描述符的读写数据
    读写方式与 特征 的 读写方式基本一致,不再过多描述 :

    /* 获取描述符 */
    ··· BluetoothGattCharacteristic characteristic;
    characteristic.getDescriptors();
    characteristic.getDescriptor(UUID);
    
    /* 通过 mGatt 读写数据
     * 同样,写操作需要做写入结果校验 */
    ··· BluetoothGattDescriptor descriptor;
    mGatt.readDescriptor(descriptor);
    mGatt.writeDescriptor(descriptor);
    
    /* 结果回调 */
    void onDescriptorRead(BluetoothGatt gatt,
                           BluetoothGattDescriptor descriptor,
                           int status) { ··· }
    void onDescriptorWrite(BluetoothGatt gatt,
                           BluetoothGattDescriptor descriptor,
                           int status) { ··· }
    
  • 读写数据需要注意的问题

    写入数据量:
    每次写操作的时候,无论是 特征 或者 描述符,一般来说最大只能设置 20个字节 的数据。
    这是因为ATT协议中,最大传输单元MTU的默认大小为23字节,其中3字节用于ATT协议的控制数据,所以GATT可用的数据大小默认为剩余的20字节。

    ATT的MTU最大值为512,在API 21及以上的安卓平台,可以通过以下方法尝试改变MTU的大小:

    ··· int mMtu;
    
    /* 请求变更MTU的大小 */
    BluetoothGatt.requestMtu(mMtu);
    
    /* 请求结果通过 BluetoothGattCallback 回调 
     * 当statue返回为 GATT_SUCCESS 时,表示变更成功
     * 变更成功后,可以使用(mMtu - 3)的大小传输数据*/
    public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {}
    

    无法改变的时候,超过20字节的数据,进行分包发送(BLE服务端需要支持)。

    读写间隔:
    读写操作都是队列操作,需要等待操作结果返回后,才能进行下次操作,若当次操作未完成,下次操作调用时,将直接返回操作启用失败。

    写入操作时,需等待服务器的确认信息,即写入回调,再进行下次写入操作。
    当写入类型设置为 不需要接收服务器确认信息(PROPERTY_WRITE_NO_RESPONSE)以加快传输速度时,两次操作之间应保留 80ms ~ 100ms 或以上的延时。

  • 数据变更通知
    前面说到ATT支持通知,一些特征在值发生变化时,可以主动向申请了监听数据变化的客户端推送通知或指示(不带数据)。
    开启特征的监听,需要进行两步操作:

    设置特征信息推送

    /**
     * 启用或禁用给定特征的通知或指示
     * @param characteristic:  需要进行操作的特征
     * @param enable :         开启或关闭
     */
    BluetoothGatt.setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
                                                boolean enable);
    

    写入CCCD
    虽然开启了特征的信息推送,但假如特征本身禁用了通知和指示,则不会有更新推送。
    前面提到了一个特殊的标识符CCCD,用于控制特征的消息推送。需要对特征的CCCD描述符进行操作,将其值置为 1 / 2,才能开启对应的 通知 / 指示 功能。

    /* 设置特征信息推送 */
    ··· BluetoothGattCharacteristic characteristic;
        mGatt.setCharacteristicNotification(characteristic,true);
    
    /* CCCD 的UUID */
    private UUID ID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");  
    
    /* 获取CCCD */
    BluetoothGattDescriptor cccd = characteristic.getDescriptor(ID_CCCD);
    
    /* 设置推送通知,参考值为:
     * BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE:   通知
     * BluetoothGattDescriptor.ENABLE_INDICATION_VALUE:     指示
     * BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE:  关闭
     */  
    cccd.setValue(参考值);
    /* 写入CCCD */
    mGatt.writeDescriptor(descriptor);
    

    以上操作完成后,即开启对应特征的更新推送了。

    接收推送
    更新推送会回调BluetoothGattCallback的onCharacteristicChanged()方法:

    /**
     * 特征变更推送触发的回调
     * @param gatt:            特征 关联的 BluetoothGatt 实例
     * @param characteristic:  更新后的 特征
     */
    void onCharacteristicChanged(BluetoothGatt gatt,
                                 BluetoothGattCharacteristic characteristic)
    
  • 关闭客户端
    用完的东西总是要收拾好。

    断开连接:

    /* 断开当前连接,如果正在连接中,则取消连接操作 */
    BluetoothGatt.disconnect();
    

    断开连接操作后,结果回调 onConnectionStateChange() 方法,应该通过回调返回的结果 status 和 newState 判断是否成功断开。

    关闭Gatt客户端:
    成功断开连接之后(甚至是断开失败),应该调用 BluetoothGatt 的close() 方法关闭客户端释放资源。
    安卓同时连接远程设备的资源极其有限,在所以任何情况不再需要连接远程设备时,都要使用BluetoothGatt 的 close() 方法释放资源。


参考文章:
蓝牙技术基础知识学习
蓝牙核心技术概述
GATT协议及蓝牙核心系统结构
Android BLE的总结
Android BLE 蓝牙开发入门

更具体的蓝牙技术说明请查看官方网站
Bluetooth Technology Website

欢迎留言,欢迎关注,会持续更新 安卓开发 中遇到的问题和技术上的一些自我总结。
如有错误,欢迎指出。

推荐阅读更多精彩内容