Android BLE开发

2,702 阅读11分钟

有很多帖子都在说Android蓝牙开发的方法,但是对于其中的概念以及作用时间一直没有不是很清楚,下边整理一下相关概念性的东西,记录一下。

基础概念

蓝牙连接传输数据的过程中,会用到以下几个概念:服务,特性,描述。一个蓝牙设备会有多个服务,每一个服务都是一类操作;在这类操作下会存在几个不同的值需要读写或者通知,每一个值对应唯一一个标记,该标记即是特征值(特性characteristic),我的理解是键值(key);每一个特征值又有多个不同的属性

  1. 描述(descriptor): 描述是某个特征值的一个属性。每个特征值有其指定的属性,例如长度(size);权限(permission):属性访问权限,一般有Read、Write、Notifications、Indications;值(value):属性的值,一般是我们向设备写入的数据或者设备通知出来的数据;描述(descriptor);

  2. 特性(characteristic): 开发过程中定义的一种参数,可以对其进行读、写、通知操作,对应的就是我们需要的数据;

  3. 服务(service): 在存在多个特性值的情况下,一般会对其进行分类,每一个分类即是一个服务(service)

以上三个概念每一个都使用唯一的UUID指定。

作用时间

属性定义如下这般:

Handler:是属性表的一个句柄(索引),在程序里只是一个数组,这个值我们不需要专门去处理;

Type:属性的类型,即UUID,蓝牙标准组织对UUID进行了分类,如下:

0x1800 - 0x26FF 用作服务类通用唯一识别码
0x2700 - 0x27FF 用于标识计量单位
0x2800 - 0x28FF 用于区分属性类型
0x2900 - 0x29FF 用作特性描述
0x2A00 - 0x7FFF 用于区分特性类型

做蓝牙BLE开发过程中有以下类需要使用

BluetoothGatt: 通用属性协议。定义BLE通讯的基本规则,对通讯过程最上层的封装,例如重新连接蓝牙设备,发现蓝牙设备的Service等;

BluetoothGattService: 服务。通过BluetoothGatt实例调用getService(UUID)获取,即前面说的对特性分组的服务;用于获取和管理其包含的特性;

BluetoothGattCharacteristic: 特性。通过BluetoothGattService实例调用getCharacteristic(UUID)获取,是GATT通讯中的最小数据单元;我们想蓝牙设备发送数据、接收蓝牙设备的通知都需要用到,是进行通讯的最小的操作对象;

BluetoothGattDescriptor: 特性描述符。对特性的额外描述,包括但不仅限于特征的单位、属性等。

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

BluetoothAdapter: Android中的蓝牙适配器。用于操作蓝牙硬件,例如开启蓝牙扫描,根据已知MAC地址实例化一个BluetoothDevice用于连接蓝牙设备的操作等。

以上就是在蓝牙BLE通信过程中需要用到的一些类和概念。

开发步骤

具体看JBD的Android BLE 蓝牙开发入门

第一步 声明所需要的权限

<uses-permission android:name="android.permission.BLUETOOTH"/> 使用蓝牙所需要的权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用扫描和设置蓝牙的权限(申明这一个权限必须申明上面一个权限)

以上BLUETOOTH和BLUETOOTH_ADMIN两个权限是蓝牙开发必须的,同时不同的SDK版本还会要求其他的权限。在Android 5.0 之后,需要在manifest中声明GPS硬件模块功能的使用。

<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />

在Android 6.0及以上,还需要打开位置权限。如果应用没有位置权限,蓝牙扫描功能不能使用(其它蓝牙操作例如连接蓝牙设备和写入数据不受影响)。同时位置权限是敏感权限,需要进行权限动态申请及判断。

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

第二步 蓝牙连接前初始化

  1. 调用getSystemService(Context.BLUETOOTH_SERVICE)获取BluetoothManager实例,该实例为全局单例;然后通过BluetoothManager实例调用getAdapter()获取系统蓝牙适配器BluetoothAdapter实例;
  2. 检测蓝牙是否开启,并尝试开启蓝牙,同时也必须校验BlutoothAdapter实例是否为空。

第三步 扫描蓝牙设备

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

//扫描含有特定UUID Service的蓝牙设备
boolean startLeScan(UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback)
//扫描全部蓝牙设备
boolean startLeScan(BluetoothAdapter.LeScanCallback callback)

在SDK>=21时这两个方法已经被废弃,官方建议使用BluetoothLeScanner中的以下方法。

//该方法是在API版本26开始添加的,可以在主程序不再运行的时候再后台进行扫描操作,当发现符合过滤条件的设备时会向PendingIntent指定的操作发送通知,进行后续操作。
int startScan (List<ScanFilter> filters,ScanSettings settings,PendingIntent callbackIntent)
//从API版本21开始添加
//搜索全部蓝牙设备,使用默认扫描设置,并且没有扫描过滤器
void startScan (ScanCallback callback)
//从API版本21开始添加
//搜索符合过滤条件的设备
void startScan (List<ScanFilter> filters,ScanSettings settings,ScanCallback callback)

ScanFilter: 是一个蓝牙扫描过滤器,可以指定扫描条件,该参数可以为空,具体支持如下,后边两个应该不常使用,翻译可能不太准确:

  • 过滤包含指定Service UUID的设备(Service UUIDs which identify the bluetooth gatt services running on the device.)
  • 过滤指定名称的设备(Name of remote Bluetooth LE device.)
  • 过滤指定MAC地址的设备(Mac address of the remote device.)
  • 过滤包含与服务相关的服务数据的设备(Service data which is the data associated with a service.)
  • 过滤包含特定制造商特定数据的设备(Manufacturer specific data which is the data associated with a particular manufacturer. )

ScanSettings: 可以配置扫描设置,采用构造器模式。可以设置

  • 扫描模式(低电量、均衡、最高占空比三种模式)
  • 扫描回调类型(第一次、全部、最后一次触发回调三种类型)
  • 扫描结果返回类型(全部和简洁两种模式)
  • 扫描结果数量(一个,少量,尽可能多三种配置)
  • 匹配模式(饥饿和粘滞两种模式)

等,具体可以查看源代码或者Google官方文档。

BluetoothAdapter.LeScanCallback: 是扫描结果回调,第一个参数是代表蓝牙设备的类;第二个参数是蓝牙的信号强弱指标,通过蓝牙的信号指标,我们可以大概计算出蓝牙设备离手机的距离。计算公式为:d = 10^((abs(RSSI) - A) / (10 * n));第三个参数是蓝牙广播出来的广播数据。

ScanCallback: 是startScan的扫描回调,本身是一个抽象类;有三个方法可选实现方法;

//扫描结束是返回所有匹配的设备列表
void onBatchScanResults(List<ScanResult> results)
//当发现设备广播时回调,第一个参数是回调类型即前边说的ScanSettings中设置的回调类型,第二个参数是扫描到的蓝牙设备信息
void onScanResult(int callbackType, ScanResult result) 
//当启动扫描失败时调用该方法
void onScanFailed(int errorCode)

然后就可以调用startLeScan或者startScan开始搜索设备了。若想停止扫描需要根据开始扫描方法确定调用stopLeScan或者stopScan;出现在回调中的设备会重复出现,所以需要手动过滤掉已经发现的设备。另外需要强调的是终止扫描时传入的回调必须是开始扫描时传入的回调,否则扫描动作不会停止。

第四步 连接蓝牙设备

连接蓝牙设备可以通过 BluetoothDevice#ConnectGatt 方法连接,也可以通过 BluetoothGatt#connect 方法进行重新连接。

//BluetoothDevice#connectGatt
//第二个参数代表是否需要自动连接,true-表示如果设备断开了,会不断的尝试自动连接,false-表示只进行一次连接尝试
BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback) 

当调用蓝牙的连接方法之后,蓝牙会异步执行蓝牙连接的操作,如果连接成功会回调 BluetoothGattCalbackl#onConnectionStateChange 方法。这个方法运行的线程是一个 Binder 线程,所以不建议直接在这个线程处理耗时的任务,因为这可能导致蓝牙相关的线程被阻塞。

void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)

这一个方法有三个参数,第一个就蓝牙设备的Gatt服务连接类。第二个参数代表是否成功执行了连接操作,如果为BluetoothGatt.GATT_SUCCESS表示成功执行连接操作,第三个参数才有效,否则说明这次连接尝试不成功。有时候,我们会遇到 status == 133 的情况,根据网上大部分人的说法,这是因为Android最多支持连接6到7个左右的蓝牙设备,如果超出了这个数量就无法再连接了。所以当我们断开蓝牙设备的连接时,还必须调用 BluetoothGatt#close方法释放连接资源。否则,在多次尝试连接蓝牙设备之后很快就会超出这一个限制,导致出现这一个错误再也无法连接蓝牙设备。 第三个参数代表当前设备的连接状态,如果newState==BluetoothProfile.STATE_CONNECTED说明设备已经连接,可以进行下一步的操作了(发现蓝牙服务,也就是Service)。当蓝牙设备断开连接时,这一个方法也会被回调,其中的newState==BluetoothProfile.STATE_DISCONNECTED。

第五步 发现服务

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

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

读取数据

当我们发现服务之后就可以通过BluetoothGatt#getService获取BluetoothGattService,接着通过 BluetoothGattService#getCharactristic获取BluetoothGattCharactristic。通过BluetoothGattCharactristic#readCharacteristic方法可以通知系统去读取特定的数据。如果系统读取到了蓝牙设备发送过来的数据就会调用BluetoothGattCallback#onCharacteristicRead方法。通过BluetoothGattCharacteristic#getValue可以读取到蓝牙设备的数据。

写入数据

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

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

向蓝牙设备注册监听实时读取蓝牙设备的数据

BLE app通常需要获取设备中characteristic 变化的通知。如下为一个Characteristic设置一个监听:

mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
//其中的参数为00002902-0000-1000-8000-00805f9b34fb
BluetoothGattDescriptor descriptor=characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

以上代码,除了通过 BluetoothGatt#setCharacteristicNotification开启Android端接收通知的开关,还需要往Characteristic的Descriptor属性写入开启通知的数据开关使得当硬件的数据改变时,主动往手机发送数据。

蓝牙接收数据的特性和蓝牙发送通知数据的特性不能相同,否则在高版本系统上无法接收到通知的数据

断开连接

当我们连接蓝牙设备完成一系列的蓝牙操作之后就可以断开蓝牙设备的连接了。通过 BluetoothGatt#disconnect可以断开正在连接的蓝牙设备。当这一个方法被调用之后,系统会异步回调 BluetoothGattCallback#onConnectionStateChange方法。通过这个方法的 newState 参数可以判断是连接成功还是断开成功的回调。

由于 Android 蓝牙连接设备的资源有限,当我们执行断开蓝牙操作之后必须执行 BluetoothGatt#close 方法释放资源。需要注意的是通过 BluetoothGatt#close 方法也可以执行断开蓝牙的操作,不过BluetoothGattCallback#onConnectionStateChange 将不会收到任何回调。此时如果执行 BluetoothGatt#connect方法会得到一个蓝牙 API 的空指针异常。所以,我们推荐的写法是当蓝牙成功连接之后,通过BluetoothGatt#disconnect 断开蓝牙的连接,紧接着在BluetoothGattCallback#onConnectionStateChange 执行 BluetoothGatt#close 方法释放资源。