前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android低功耗蓝牙BLE开发小结

Android低功耗蓝牙BLE开发小结

原创
作者头像
fdroid
发布2018-07-17 13:10:50
5.7K0
发布2018-07-17 13:10:50
举报
文章被收录于专栏:Android填坑指南

1. BLE及相关协议

BLE是蓝牙4.0标准的一部分,旨在解决传统蓝牙连接慢、能耗大的问题,Google在Android 4.3(API 18)中引入了对BLE的支持。BLE连接使用GAP(Generic Access Profile)协议,通信使用GATT(Generic Attribute Profile)协议。GATT又以ATT为基础,所有的LE服务都以ATT作为应用层协议。以下深入地介绍这两个协议。

(1) ATT协议

属性(attribute)是ATT的基础,一个attribute的组成部分有三:

  • handle: 16位的句柄,具有唯一性,用于区分不同的attribute
  • UUID: 定义attribute的意义,由高层协议决定
  • value: 定长的字节数组,意义由UUID决定

ATT Server负责存储attribute,Client不存储attribute,仅通过ATT线路协议读写Server中attribute的value。大多数ATT协议都基于简单的Client-Server模式(Client请求 -> Server应答),但ATT还有两个特性:notificationindication,Server可以主动发起请求,通知Client某个attribute发生了变化,Client不再需要轮询attribute。

ATT非常通用,给高层协议留下了很大的发挥空间,也将ATT的一些问题抛给了高层协议。这时,GATT协议出现了,它规范和扩展了attribute的用法。

(2) GATT协议

GATT是所有高层LE协议的基础,它将ATT进一步封装,定义了连接LE设备使用的分层数据结构。

GATT Profile描述了基于GATT功能的用例、角色和通用行为。服务(Service)是特征(Characteristic)的集合,多个相关联的服务表现出了设备的行为。1

GATT Profile Hierarchy
GATT Profile Hierarchy

层次结构的顶级是一个配置文件(Profile),由一个或多个服务(Service)构成。服务由特征(Characteristic)或对其他服务的引用组成。特征包括一种类型(用UUID表示),一个值,一组指示特征支持操作的属性和一组与安全性有关的权限。特征还可以包括一个或多个描述符(Descriptor)——与所拥有的特征相关的元数据或配置标识。

GATT将这些服务分组以封装设备的行为,并根据GATT功能描述用例,角色和一般行为。该框架定义了服务的过程,格式及其特征,包括发现、读取、写入、通知和指示特征,以及配置特征的广播。

举个例子,一个Profile中有一个温度计服务(Service),这个服务包含一个只读的温度特征(Characteristic)和一个可读写的时间特征(Characteristic)。温度特征又包含了一个温度值(value),和单位描述(Descriptor);时间特征包含了一个时间值(value)和时区描述(Descriptor)。

在GATT中,Service,Characteristic 和 Descriptor 都使用UUID作为唯一标识。那么什么是UUID呢?

(3) UUID

UUID(Universally Unique Identifier),通用唯一识别码,是一种软件构建的标准,其目的是使分布式系统中的所有元素都有唯一的辨识信息。UUID长度为128bit,标准形式以16进制数字表示,构成8-4-4-4-12的格式,例:00002901-0000-V000-N000-008059B34FB,其中V位置的数字表示版本号,目前为1~5,N位置的数字用于确认规范,目前只可能为8,9,A,B,另有0-7保留用于向后兼容,C、D保留给Microsoft,E、F保留供将来使用。

UUID版本
  • V1:基于时间戳的MAC地址 使用MAC地址保证UUID的全球唯一性,但暴露了MAC地址和UUID的生成时间。
  • V2:DCE安全(无实现) 使用V1方法生成UUID后,将时间戳的前四位换为POSIX的UID,由于规范未明确指定,该版本未被实现。
  • V3:基于命名空间(MD5) 由用户指定1个namespace和1个具体的字符串,通过MD5散列,来生成1个UUID。此版本用于向后兼容。
  • V4:基于随机数(最常用) 根据随机数,或者伪随机数生成UUID。该版本目前使用最多。
  • V5:基于名字空间(SHA-1) 与V3相同,不过把MD5换成了SHA-1.

另外,在BLE中,还可能会遇到16bit的UUID,Bluetooth官方定义的一些标准服务,就使用了16bit的UUID,16bitUUID更短小,传输数据更小。另外,Bluetooth SIG成员也可申请分配16bit的UUID,鹅厂申请到了0xFEBA和0xFEE7这两个16bit的UUID。

注意:在Java中,16bit的UUID只是在传输过程中使用,在构建UUID对象时,还需转换为128bit的UUID。转换方式为:

128_bit_value = 16_bit_value * 2^96 + BLUETOOTH_BASE_UUID

BLUETOOTH_BASE_UUID= 00000000-0000-1000-8000-00805F9B34FB

实际上,就是将BASE_UUID第一段的末四位替换为16bitUUID。

2. BLE应用权限

涉及到蓝牙相关开发需要在AndroidManifest.xml中声明权限,其中位置权限在扫描LE设备时需要使用。

代码语言:txt
复制
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

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

另外,还需添加uses-feature,设置android.hardware.bluetooth_le的属性为false,否则在不支持BLE的设备上无法安装本应用。当代码中用到BLE时,首先进行判断,使用

代码语言:txt
复制
getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);

位置权限属于敏感权限,在Android 6.0(API 23)及以上的设备中使用该权限需要动态申请。

  • 首先检查权限,若没有权限则申请private void checkBluetoothPermission() { if (Build.VERSION.SDK_INT >= 23) { //校验是否已具有模糊定位权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // 无权限,请求申请 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, QWALLET_REQUEST_ACCESS_COARSE_LOCATION); } else { //具有权限 } } }
  • 重写Activity中的onRequestPermissionsResult方法,处理权限申请后的操作。 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == QWALLET_REQUEST_ACCESS_COARSE_LOCATION) { if (grantResults0 == PackageManager.PERMISSION_GRANTED) { // TODO: 获得权限的操作 } else { // TODO: 权限被拒绝的操作 } } }

3. Android BLE相关类

  • BluetoothAdapter:Android设备的蓝牙适配器,可执行基本的蓝牙任务,如启动、停止设备发现,查询已配对设备,获取蓝牙适配器状态,使用MAC地址实例化蓝牙设备类BluetoothDevice等。其中,设备发现是异步的,需实现BluetoothAdapter.LeScanCallback接口。
  • BluetoothDevice:作为GATT客户端调用connectGatt()方法连接到由该设备托管的GATT服务器。
  • BluetoothGatt:该类提供了蓝牙的GATT功能,以实现与BLE设备的通信。如连接、发现服务、读写特征、设置通知等。发现服务、读写特征等操作是异步的,若有自定义操作,需要继承BluetoothGattCallback类。
  • BluetoothGattService:蓝牙GATT服务,包含了BluetoothGattCharacteristic的集合。
  • BluetoothGattCharacteristic:蓝牙GATT特征,包含了一个值、附加信息及GATT descriptors。
  • BluetoothGattDescriptor:蓝牙GATT描述,用于描述特征的属性。

各类之间的关系如下图所示(略去了每个方法的参数)。

Class
Class

4.初始化适配器

  • 初始化适配器
    • 使用BluetoothAdapter.getDefaultAdapter();获取蓝牙适配器实例。
    • 在API 18后,也可使用BluetoothManager实例获取适配器实例。
    • 若获取到的值为null,则该设备不支持蓝牙。
  • 打开蓝牙 - 可直接使用BluetoothAdapter对象的enable()方法打开蓝牙。 - 也可构建intent,请求用户打开蓝牙。Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(intent, QWALLET_REQUEST_OPEN_BLUETOOTH);

5. 扫描BLE设备

  • 开始扫描 使用方法boolean startLeScan (BluetoothAdapter.LeScanCallback callback),但该方法在API 21中已过时,若应用的目标版本超过21,可使用startScan(List, ScanSettings, ScanCallback)代替。本次开发仍使用startLeScan方法,在LeScanCallbackonLeScan方法中处理搜索到的设备。
  • 停止扫描 使用方法void stopLeScan (BluetoothAdapter.LeScanCallback callback)停止扫描,需传入开启扫描时的callback对象。!!!搜索设备非常地消耗资源,当搜索到所需设备后,请立即停止扫描操作。扫描超时后也需停止扫描,可使用**Handler.postDelayed(Runnable, TIME_OUT_PERIOD)**方法执行。

注意:若需要只搜索包含指定服务的设备,不要使用API中提供的boolean startLeScan (UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback)方法,该方法在一些设备上会存在搜不到结果的情况。在小米5的测试结果为:仅匹配一个16bit的UUID时可得到设备,其他情况(a. 多个16bitUUID; b. 一个16bit UUID和一个128bit UUID; c. 一个128bit UUID)都提示设备不匹配,已过滤。

解决方法:在回调方法onLeScan中读取广播包,自行实现服务列表的读取及设备过滤。使用下面的方法获取到该设备的服务的UUID列表,根据该列表对设备进行过滤。2另外,在API 21之后,也引入了android.bluetooth.le包及ScanRecord等类,可以直接获取服务的UUID列表,更方便地处理扫描结果。

为了从广播包中读取服务UUID的列表,首先分析广播包的数据格式。

广播及扫描响应包格式8

广播包有两种:

  • Advertising Data:从机主动广播自己。
  • Scan Response:当主机主动扫描时,从机收到扫描请求,返回扫描响应数据给主机。

此处讨论的包格式只讨论包中的数据段(即onLeScan()回调方法的参数byte[] scanRecord),不包括完整报文的其他部分,如前导、接入地址等。下图所示为包中数据段的格式。

Advertising and scan response data format
Advertising and scan response data format

数据包括了有效部分和无效部分。有效部分由若干个广播数据段(AD Structure)序列构成,每个广播数据段的组成为:

  • 长度Len:本段数据的长度(不包括Len占用的一个byte)
  • AD类型:本段数据所表示的意义。参考:Generic Access Profile
  • 数据部分

无效部分预留了数据包的扩展能力,无效部分全为0.

常见的AD类型

Data Type

Description

0x01

设备标志

0x02

不完整的16bit服务UUID列表

0x03

完整的16bit服务UUID列表

0x06

不完整的128bit服务UUID列表

0x07

完整的128bit服务UUID列表

0x09

完整的设备名称

0xFF

厂商特定数据

这是一个小米手环的广播包数据的例子:0x02, 0x01, 0x06, 0x1B, 0xFF, 0x57, 0x01, 0x00, 0x4B, 0x30, 0x67, 0xD4, 0xED, 0xDB, 0xB6, 0x08, 0xDA, 0xF3, 0x85, 0x42, 0x64, 0x5D, 0x3A, 0xF5, 0x02, 0xCB, 0x66, 0xFC, 0x33, 0xC0, 0x0A, 0x0A, 0x09, 0x4D, 0x49, 0x20, 0x42, 0x61, 0x6E, 0x64, 0x20, 0x32, 0x03, 0x02, 0xE0, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,具体分析如下:

Mi Band Advertise Data
Mi Band Advertise Data

由以上分析可以看出,只需处理AD Type为0x02, 0x03, 0x06, 0x07的AD数据段即可获取服务UUID的列表,以下是具体代码:

代码语言:txt
复制
/**
 * 从广播包中获取所有服务的UUID列表
 * @param scanRecord
 * @return
 */
private List<UUID> getUuidsFromRecordData(byte[] scanRecord) {
        List<UUID> uuids = new ArrayList<>();
        ByteBuffer buffer = ByteBuffer.wrap(scanRecord).order(ByteOrder.LITTLE_ENDIAN);
        while (buffer.remaining() > 2) {
            byte length = buffer.get();
            if (length == 0) break;
            byte type = buffer.get();
            switch (type) {
                case 0x02: // Partial list of 16-bit UUIDs
                case 0x03: // Complete list of 16-bit UUIDs
                    while (length >= 2) {
                        uuids.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805F9B34FB", buffer.getShort())));
                        length -= 2;
                    }
                    break;
                case 0x06: // Partial list of 128-bit UUIDs
                case 0x07: // Complete list of 128-bit UUIDs
                    while (length >= 16) {
                        long lsb = buffer.getLong();
                        long msb = buffer.getLong();
                        uuids.add(new UUID(msb, lsb));
                        length -= 16;
                    }
                    break;
                default:
                    buffer.position(buffer.position() + length - 1);
                    break;
            }
        }
        return uuids;
    }

Tip:Java中构建UUID对象可使用UUID.fromString(String str)方法,但传入参数必须为8-4-4-4-12标准UUID格式,若使用16bit的UUID,需先转换。

6. 连接BLE设备

使用上一步获取到的BluetoothDevice对象,或根据MAC地址,使用BluetoothAdapter对象的getRemoteDevice(String address)方法重构一个BluetoothDevice对象。

使用方法connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)连接设备,得到BluetoothGatt对象。BluetoothGattCallback用于BluetoothGatt对象所有耗时操作的回调。connectGatt方法获取到BluetoothGatt对象之后,设备将处于正在连接状态(可能会连接失败),当设备处于已连接状态时,才可进行后续操作。可用BluetoothGattCallback中的onConnectionStateChange方法监听连接状态的变化。

GATT 连接需要特别注意的是:GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当设备断开,它又开始广播。

7. 获取服务与特征

使用BluetoothGatt对象的discoverServices()方法发现服务,在回调方法onServicesDiscovered()中进行发现服务后的操作。

使用BluetoothGatt对象的getServices()获取服务BluetoothGattService列表。若getServices()先于discoverServices()执行,则获取不到服务。

使用BluetoothGattServicegetCharacteristics()方法获取该服务的所有特征值。可使用特征对象的一系列get方法获取特征的信息。

这里需要注意的是getProperties()方法,该方法得到的是一个int值,换为二进制,每一位表示了特征对象的一个属性值,执行总属性值与对应属性位的与操作可得到该位的属性值。举个例子,如果要获取该特征的写属性,可执行getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE,若得到的值大于0,则说明该特征支持写属性。

8. 读写特征与设置通知

  • 读特征值:使用BluetoothGatt对象的readCharacteristic (BluetoothGattCharacteristic characteristic),该操作同样是异步的,在方法onCharacteristicRead中回调。
  • 写特征值与读类似。
  • 属性值变化通知:使用BluetoothGatt对象的boolean setCharacteristicNotification (BluetoothGattCharacteristic characteristic, boolean enable)设置某个特征是否通知,设置为true后,当属性值变化,可在回调方法onCharacteristicChanged中执行相关操作。
  • 读写、设置通知操作都需特征有对应的属性支持才能执行成功。

注意:如果开发中使用的是虚拟BLE设备,还需先设置虚拟设备中需要通知的特征的Descriptor为开启通知,后续才会收到通知事件。3

从蓝牙组织提供的文档可以看到,UUID = 0x2902的描述符为客户端特征配置,具体的,该描述符的值为16bit,其中第0位表示Notifications disabled/enabled,第1位表示Indications disabled/enabled,其余14位预留。使用以下代码添加该属性值的通知属性。

代码语言:txt
复制
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(parseUuidFromStr("2902"));
if (descriptor != null) {
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    connDevice.getGatt().writeDescriptor(descriptor);
}

9.Tip

BLE模拟应用

在iOS应用商店可以搜到应用LightBlue,该应用可模拟BLE设备,可添加服务、特征等。

参考

  1. https://www.bluetooth.com/specifications/gatt/generic-attributes-overview
  2. startLeScan with 128 bit UUIDs doesn't work on native Android BLE implementation
  3. Subscribe to a BLE Gatt notification Android
  4. https://www.bluetooth.com/specifications/gatt/descriptors
  5. http://legendmohe.net/2015/01/16/%E7%BF%BB%E8%AF%91att%E5%92%8Cgatt%E6%A6%82%E8%BF%B0/
  6. https://www.bluetooth.com/specifications/bluetooth-core-specification
  7. https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. BLE及相关协议
    • (1) ATT协议
      • (2) GATT协议
        • (3) UUID
          • UUID版本
      • 2. BLE应用权限
      • 3. Android BLE相关类
      • 4.初始化适配器
      • 5. 扫描BLE设备
        • 广播及扫描响应包格式8
          • 常见的AD类型
      • 6. 连接BLE设备
      • 7. 获取服务与特征
      • 8. 读写特征与设置通知
      • 9.Tip
        • BLE模拟应用
        • 参考
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档