Android BLE(低功耗蓝牙)技术总结


前言

  去年公司启动了TWS耳机项目的开发,而APP端和耳机端的数据传输和交互恰好就要用到低功耗蓝牙技术(BLE),从零开始入手BLE开发确实需要弥补很多相关的知识点,本篇文章算是对Android BLE开发的入门分享和知识总结,同时希望能对后来者有一些启发


一、蓝牙介绍

1.什么是蓝牙?

  蓝牙( Bluetooth® ):是一种无线技术标准,可实现固定设备、移动设备和楼宇个人域网之间的短距离数据交换(使用2.4—2.485GHz的ISM波段的UHF无线电波)。蓝牙技术最初由电信巨头爱立信公司于1994年创制,当时是作为RS232数据线的替代方案。如今蓝牙由蓝牙技术联盟(Bluetooth Special Interest Group,简称SIG)管理。

2.蓝牙版本介绍

  蓝牙发展至今已经历了11个版本的更新。1.0、1.1、1.2、2.0、2.1、3.0、4.0、4.1、4.2、5.0、5.1。蓝牙规范1.0、2.0,即基本速率(BR)和增强数据速率(EDR),蓝牙技术规范3.0,即高速(HS)技术规范,最大数据速率达到了24Mb/s。具体的蓝牙版本可参考下图
网络图片

二、低功耗蓝牙(BLE)

1.BLE介绍

  蓝牙 4.0 支持单模和双模两种部署方式,其中单模即是我们说的 BLE低功耗蓝牙(Bluetooth Low Energy),而双模指的是 Classic Bluetooth + BLE。

2.经典蓝牙(Classic Bluetooth)与低功耗蓝牙(BLE)的区别

(1)经典蓝牙可以用与数据量比较大的传输,如语音,音乐,较高数据量传输等。

(2)BLE 特点就如其名,功耗更低的同时,对数据包做出了限制。所以适用于实时性要求比较高,但是数据速率比较低的产品,如鼠标,键盘,传感设备的数据发送等。

3.低功耗蓝牙(BLE)基本概念讲解

GATT简介

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

我们可以首先看一下这个分层数据结构图,当我们看懂了这个结构图之后,基本能够对BLE蓝牙的概念有个基本的掌握了。
在这里插入图片描述
  简单描述一下结构图的意思就是:Profile是一组服务(Service)的集合,这个文件包含广播的种类、所使用的连接间隔、所需的安全等级等配置信息,每个Service下面可以包含多个特征(characteristic),每个特征包含属性(properties)和值(value),还可以包含多个描述(descriptor)。

Profile(数据配置文件)

  GATT顶层,一个profile文件可以包含一个或者多个服务,一个profile文件包含需要的服务的信息或者是为对等设备如何交互的配置文件的选项信息。设备的GAP和GATT的角色都可能在数据的交换过程中改变,因此,这个文件应该包含广播的种类、所使用的连接间隔、所需的安全等级等信息。
比如:通过配置文件,我们可以区分出连接的蓝牙设备是 耳机/健康设备/A2DP(蓝牙立体声音频传输),当我们打开蓝牙开始搜索设备的时候,不同的配置文件显示出的logo也不一样


Service(服务)

  一个低功耗蓝牙设备可以定义一个或多个Service, 每一个Service包含了一个或多个特性(characteristic)。
设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是经过官方认证的16位UUID,另外一种是由开发者自己定义的128位的UUID类似于0x0000xxxx-0000-1000-8000-00805F9B34FB,这是蓝牙技术联盟(SIG)定义的一个基本格式,蓝牙模块的开发者通常只要定义xxxx的部分即可,开发时通常由硬件或嵌入式工程师会告诉我们该模块包含哪些Service及其对应的UUID。


Characteristic(特征)

  在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。
简而言之,Characteristic是我们进行数据通信的一个重要载体,我们的数据读、写、通知等都通过这个类来实现,是非常重要的一个类。


Descriptor(描述符)

  用于表达 特征 的其他附加信息,如特征值的有效范围,可读性描述等信息。
其中包含了特殊的 CCCD(Client Characteristic Configuration Descriptor, Assigned Number : 0x2902):
CCCD 可以设置 服务端 在对应特征值发生变化时,是否对 客户端 进行信息 推送(直接发送信息) 或 提示(发送一个提示并等待回复)。
当特征包含通知能力时,CCCD为必选项。通过设置监听通知,客户端就能够收到服务端发送过来的数据。


三、Android BLE API 简介

  Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的属性特征(characteristics)等操作。以下是我们使用Android BLE时需要用到的关键类。

BluetoothAdapter
  BluetoothAdapter 拥有基本的蓝牙操作,例如开启蓝牙扫描,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)实例化一个 BluetoothDevice 用于连接蓝牙设备的操作等等。

BluetoothDevice
  代表一个远程蓝牙设备。这个类可以让你连接所代表的蓝牙设备或者获取一些有关它的信息,例如它的名字,地址和绑定状态等等。

BluetoothGatt
  这个类提供了 Bluetooth GATT 的基本功能。例如重新连接蓝牙设备,发现蓝牙设备的 Service 等等。

BluetoothGattService
  这个类通过 BluetoothGatt#getService 获得,如果当前服务不可见那么将返回一个 null。这个类对应上面说过的 Service。我们可以通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现蓝牙数据的双向传输。

BluetoothGattCharacteristic
  这个类对应上面提到的 Characteristic。通过这个类定义需要往外围设备写入的数据和读取外围设备发送过来的数据。

Android 蓝牙开发示例

第一步:声明所需要的权限

<uses-permission android:name="android.permission.BLUETOOTH"/> 
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 
在 Android 6.0 及以上,还需要打开位置权限。如果应用没有位置权限,蓝牙扫描功能不能使用(其它蓝牙操作例如连接蓝牙设备和写入数据不受影响)。
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

第二步:连接蓝牙前的初始化工作

在建立蓝牙连接之前,需要确认设备支持 BLE。如果支持,再确认蓝牙是否开启。
如果蓝牙没有开启,可以使用 BluetoothAdapter 类来开启蓝牙。
1.获取 BluetoothAdapter

final BluetoothManager blManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter=blManager.getAdapter();

2.如果检测到蓝牙没有开启,尝试开启蓝牙。开启蓝牙方式有两种:

//第一种不需要用户打开,直接帮用户开启手机上的蓝牙:
  if (!mBluetoothAdapter.isEnabled()) {
       mBluetoothAdapter.enable();
  }

//第二种跳转到设置界面,由用户自己开启蓝牙
if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
     Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
     startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

第三步:扫描蓝牙设备

  外围设备开启蓝牙后,会广播出许多的关于该设备的数据信息,例如 mac 地址,uuid 等等。通过这些数据我们可以筛选出需要的设备。在 BluetoothAdapter 中,我们可以看到有两个扫描蓝牙的方法

//第一种:可以指定只扫描含有特定 UUID Service 的蓝牙设备
boolean startLeScan(UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback)

//第二种:扫描全部蓝牙设备
boolean startLeScan(BluetoothAdapter.LeScanCallback callback)

//开启蓝牙扫描
private List<BluetoothDevice> bluetoothDeviceArrayList = new ArraryList<>();
final BluetoothAdapter.LeScanCallback callback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
        bluetoothDeviceArrayList.add(device);
        Log.d(TAG, "run: scanning...");
    }
};
mBluetoothAdapter.startLeScan(callback);

/**onLeScan回调方法解析
第一个参数是代表蓝牙设备的类,可以通过这个类建立蓝牙连接获取关于这一个设备的一系列详细的参数,例如名字,MAC 地址等等;
第二个参数是蓝牙的信号强弱指标,通过蓝牙的信号指标,我们可以大概计算出蓝牙设备离手机的距离。
第三个参数是蓝牙广播数据。
当执行上面的代码之后,一旦发现蓝牙设备,LeScanCallback 就会被回调,直到 stopLeScan 被调用。出现在回调中的设备会重复出现,所以如果我们需要通过 BluetoothDevice 获取外围设备的地址手动过滤掉已经发现的外围设备。*/

//停止蓝牙扫描 通过调用 BluetoothAdapter#stopLeScan 可以停止正在进行的蓝牙扫描,这里需要注意的是,传入的回调必须是开启蓝牙扫描时传入的回调,否则蓝牙扫描不会停止。
mBluetoothAdapter.stopLeScan(callback)

  由于蓝牙扫描的操作比较消耗手机的能量。所以我们不能一直开着蓝牙,必须设置一段时间之后关闭蓝牙扫描。示例代码如下:

private void scanLeDevice(final boolean enable) {
    if (enable) {
        // Stops scanning after a pre-defined scan period.
        // 预先定义停止蓝牙扫描的时间(因为蓝牙扫描需要消耗较多的电量)
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mScanning = false;
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            }
        }, SCAN_PERIOD);
        mScanning = true;


        // 定义一个回调接口供扫描结束处理
        mBluetoothAdapter.startLeScan(mLeScanCallback);
    } else {
        mScanning = false;
        mBluetoothAdapter.stopLeScan(mLeScanCallback);
    }
}

第四步:连接蓝牙设备

  通过扫描能够获得设备 BluetoothDevice,包含地址和名字。
  通过设备连接可获取 BluetoothGatt,后面通过 BluetoothGatt 的实例来进行client的操作,如使用该实例去发现服务,获取读、写、通知等属性

//连接蓝牙设备可以通过 BluetoothDevice#ConnectGatt 方法连接,也可以通过 BluetoothGatt#connect 方法进行重新连接。
//Tips:这里的BluetoothDevice就是上面我们的onLeScan回调中的device
BluetoothDevice#connectGatt

/**
第一个参数代表当前的上下文。
第二个参数表示是否需要自动连接。如果设置为 true, 表示如果设备断开了,会不断的尝试自动连接。设置为 false 表示只进行一次连接尝试。
第三个参数是连接后进行的一系列操作的回调,例如连接和断开连接的回调,发现服务的回调,成功写入数据,成功读取数据的回调等等。*/
BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)


//当调用蓝牙的连接方法之后,蓝牙会异步执行蓝牙连接的操作,如果连接成功会回调方法。
//Tips:这里的BluetoothGattCallback 就是我们调用ble连接时传入的回调
BluetoothGattCallback#onConnectionStateChange

//第一个就蓝牙设备的 Gatt 服务连接类。
//第二个参数代表是否成功执行了连接操作,如果为 BluetoothGatt.GATT_SUCCESS 表示成功执行连接操作.
//第三个参数代表当前设备的连接状态.
onConnectionStateChange(BluetoothGatt gatt, int status, int newState)

第五步:发现服务

  在成功连接到蓝牙设备之后才能进行这一个步骤,在 BluetoothGattCalbackl#onConnectionStateChang 方法被成功回调且表示成功连接—> BluetoothGatt#discoverService 这一个方法
  当这一个方法被调用之后,系统会异步执行发现服务的过程,直到 BluetoothGattCallback#onServicesDiscovered 被系统回调之后,手机设备和蓝牙设备才算是真正建立了可通信的连接。参考代码如下:

//这里做一个回调实现方便理解,该对象的实例应该在BluetoothGatt#connectGatt 方法中传入
public class BluetoothGattCallbackImp extends BluetoothGattCallback {
    
     /**
     * Callback indicating when GATT client has connected/disconnected to/from a remote
     * GATT server.
     * @param gatt GATT client
     * @param status Status of the connect or disconnect operation. {@link
     * BluetoothGatt#GATT_SUCCESS} if the operation succeeds.
     * @param newState Returns the new connection state. Can be one of {@link
     * BluetoothProfile#STATE_DISCONNECTED} or {@link BluetoothProfile#STATE_CONNECTED}
     */
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        boolean isConnected = (newState == BluetoothAdapter.STATE_CONNECTED);

        boolean isSuccess = (status == BluetoothGatt.GATT_SUCCESS);
        if (isConnected && isSuccess) {
            // Discover services, and return connection state = connected
            // after services discovered
            isOk = gatt.discoverServices();
            if (isOk) {
                Log.d(TAG, "discoverServices ok");
                return;
            }
        }
    }
}

  到这一步,我们已经成功和蓝牙设备建立了可通信的连接,接下来就可以执行相应的蓝牙通信操作了,例如写入数据,读取蓝牙设备的数据等等。

读取数据
step1.获取服务:通过BluetoothGatt#getService 获取 BluetoothGattServic
step2.获取特征值:通过BluetoothGattService#getCharactristic 获取 BluetoothGattCharactristic。
step3.读取特征值:通过 BluetoothGattCharactristic#readCharacteristic 方法通知系统去读取特定的数据。
  如果系统读取到了蓝牙设备发送过来的数据就会调用 BluetoothGattCallback#onCharacteristicRead 方法。通过 BluetoothGattCharacteristic#getValue 可以读取到蓝牙设备的数据。以下是代码示例:

// 读取数据
BluetoothGattService service = gatt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
gatt.readCharacteristic();

//Tips:该回调方法是BluetoothGattCallback的回调方法
@Override
public void onCharacteristicRead(final BluetoothGatt gatt,
                                 final BluetoothGattCharacteristic characteristic,
                                 final int status) {
    Log.d(TAG, "callback characteristic read status " + status
            + " in thread " + Thread.currentThread());
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "read value: " + characteristic.getValue());
    }
}

写入数据
  和读取数据一样,在执行写入数据前需要获取到 BluetoothGattCharactristic。接着执行一下步骤:

  1. 调用 BluetoothGattCharactristic#setValue 传入需要写入的数据(蓝牙最多单次1支持 20 个字节数据的传输,如果需要传输的 数据大于这一个字节则需要分包传输)。
  2. 调用 BluetoothGattCharactristic#writeCharacteristic 方法通知系统异步往设备写入数据。
  3. 系统回调 BluetoothGattCallback#onCharacteristicWrite 方法通知数据已经完成写入。此时,我们需要执行 BluetoothGattCharactristic#getValue 方法检查一下写入的数据是否我们需要发送的数据,如果不是按照项目的需要判断是否需要重发。以下是示例代码:
//往蓝牙数据通道的写入数据
BluetoothGattService service = gatt.getService(SERVICE_UUID);
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
characteristic.setValue(sendValue);
gatt.writeCharacteristic(characteristic);

//回调
@Override
public void onCharacteristicWrite(final BluetoothGatt gatt,
                                    final BluetoothGattCharacteristic characteristic,
                                    final int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d(TAG, "write value: " + characteristic.getValue());
    }
    if(!characteristic.getValue().equal(sendValue)) {
        // 执行重发策略
        gatt.writeCharacteristic(characteristic);
    }
}

  向蓝牙设备注册监听实现实时读取蓝牙设备的数据,客户端需要监听获取设备中characteristic 变化的通知。下面的代码演示了怎么为一个Characteristic 设置一个监听。

//Step1:将需要监听的特征值设置通知状态为true
BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
gatt.setCharacteristicNotification(characteristic, true);

//Step2:取得当前特征值下的descriptor,并设置ENABLE_NOTIFICATION_VALUE值
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
//Step3:向gatt中写入Descriptor
mBluetoothGatt.writeDescriptor(descriptor);

  通过以上的流程即可实现客户端对远端设备针对当前监听的特征值的数据进行实时监听。当远端设备发送给客户端数据后,客户端就会通过BluetoothGattCallback#onCharacteristicChanged方法监听到数据

   /**
     * 收到特征值变更(通知)
     */
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic characteristic) {
        Log.i(TAG, "onCharacteristicChanged: address==" + gatt.getDevice().getAddress());
        //该数据为远端设备发给客户端的数据
		byte[] receiveData = characteristic.getValue();
		
    }

最后一步:断开连接

  当我们连接蓝牙设备完成一系列的蓝牙操作之后就可以断开蓝牙设备的连接了。
通过 BluetoothGatt#disconnect 可以断开正在连接的蓝牙设备。当这一个方法被调用之后,系统会异步回调 BluetoothGattCallback#onConnectionStateChange 方法。通过这个方法的 newState 参数可以判断是连接成功还是断开成功的回调。
  由于 Android 蓝牙连接设备的资源有限,当我们执行断开蓝牙操作之后必须执行 BluetoothGatt#close 方法释放资源。需要注意的是通过 BluetoothGatt#close 方法也可以执行断开蓝牙的操作,不过 BluetoothGattCallback#onConnectionStateChange 将不会收到任何回调。此时如果执行 BluetoothGatt#connect 方法会得到一个蓝牙 API 的空指针异常。
  推荐的写法是当蓝牙成功连接之后,通过 BluetoothGatt#disconnect 断开蓝牙的连接,紧接着在 BluetoothGattCallback#onConnectionStateChange 执行 BluetoothGatt#close 方法释放资源。
以下是代码示例:

@Override
    public void onConnectionStateChange(final BluetoothGatt gatt, final int status,
                                    final int newState) {
        Log.d(TAG, "onConnectionStateChange: thread "
                + Thread.currentThread() + " status " + newState);

        if (status != BluetoothGatt.GATT_SUCCESS) {
            String err = "Cannot connect device with error status: " + status;
      // 当尝试连接失败的时候调用 disconnect 方法是不会引起这个方法回调的,所以这里
            //   直接回调就可以了。
            gatt.close();
            Log.e(TAG, err);
            return;
        }


        if (newState == BluetoothProfile.STATE_CONNECTED) {
            gatt.discoverService();
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            gatt.close();
        }
    }

2.蓝牙操作的注意事项

    1.蓝牙的写入操作( 包括 Descriptor 的写入操作), 读取操作必须序列化进行. 写入数据和读取数据是不能同时进行的, 如果调用了写入数据的方法, 马上调用又调用写入数据或者读取数据的方法,第二次调用的方法会立即返回 false, 代表当前无法进行操作。原因:蓝牙读写操作返回 false,为什么多次读写只有一次回调?

    2.Android 连接外围设备的数量有限(7个),当不需要连接蓝牙设备的时候,必须调用 BluetoothGatt#close 方法释放资源。

    3.扫描不到设备—>先确定下列几项是否满足: 1、蓝牙是否打开 2、蓝牙相关权限是否授权(6.0以上位置权限) 3、7.0以上手机是否打开GPS

    4.有时候刚开始扫描还正常,过段时间扫描不到设备?
    原因: 出现这个问题的很多是Android7.0以上手机,为什么呢? 因为Google为了防止Android7中的BLE扫描滥用,从而做了一些    限制,即不要在30s内对蓝牙扫描 重复开启-关闭超过5次。 建议: 设置扫描周期>6s, 用户点击扫描后不要重复进行扫描,可以做一个是否正在扫描的标志位,如果 正在扫描就不做重复扫描动作了。

    5.Android Ble默认单次最大传输20个byte,所以数据量大时需要设计分包发送机制。Android5.0及其以后系统版本可以通过BluetoothGatt#requestMtu与设备端协商最大传输字节(系统限制最高512),超过会自动返回当前协商成功的mtu长度。

    6.蓝牙设备的BLE Mac地址和经典蓝牙Mac地址相同的情况下,在Android某些型号(低版本)手机会造成BLE无法连接的问题,解决方案就是需要将设备端的BLE的经典蓝牙地址和BLE蓝牙地址设为不同地址。

总结

  本篇文章是对Android BLE的入门知识点及相关的API的调用方式的解析,也许初学者刚入门的时候很可能会对BLE的各种API的调用感到困惑陷入细节中难以自拔,但实际上我们只要好好的理解了Service(服务)、Characteristic(特征)、Descriptor(描述值)这三者之间的关系之后,再去看各种API的调用就会豁然开朗了。只要我们掌握了本质,再去看各种招式就会特别简单啦。其实学习BLE的过程中我也走了不少弯路,同时感谢互联网丰富的资料以及相关网友的解答,让我虽然曲曲折折但终能到达目的地!以下是我参考的相关资料,分享给大家一起学习

参考资料

1.Android官方文档:蓝牙低功耗概览
2.Android BLE 蓝牙开发入门
3.Android BLE蓝牙踩坑总结
4.蓝牙【GATT】协议介绍
5.开源项目:Android-BLE

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值