安卓 BLE 开发详解

作者: 七零八落问号 | 来源:发表于2018-04-17 18:10 被阅读169次

    相关概念

    • 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

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

    相关文章

      网友评论

        本文标题:安卓 BLE 开发详解

        本文链接:https://www.haomeiwen.com/subject/azqvhftx.html