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 还有两个特性: notification 和 indication,Server 可以主动发起请求, 通知 Client 某个 attribute 发生了变化, Client 不再需要轮询 attribute.
ATT 非常通用, 给高层协议留下了很大的发挥空间, 也将 ATT 的一些问题抛给了高层协议. 这时, GATT 协议出现了, 它规范和扩展了 attribute 的用法.
(2) GATT 协议
GATT 是所有高层 LE 协议的基础, 它将 ATT 进一步封装, 定义了连接 LE 设备使用的分层数据结构.
GATT Profile 描述了基于 GATT 功能的用例, 角色和通用行为. 服务 (Service) 是特征 (Characteristic) 的集合, 多个相关联的服务表现出了设备的行为. 1
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 官方 https://www.bluetooth.com/specifications/gatt 定义的一些标准服务, 就使用了 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 设备时需要使用.
- <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 时, 首先进行判断, 使用
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
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 方法, 在 LeScanCallback 的 onLeScan 方法中处理搜索到的设备.
停止扫描
使用方法
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
数据包括了有效部分和无效部分. 有效部分由若干个广播数据段 (AD Structure) 序列构成, 每个广播数据段的组成为:
长度 Len: 本段数据的长度(不包括 Len 占用的一个 byte)
AD 类型: 本段数据所表示的意义. 参考: Generic Access Profile https://www.bluetooth.com/specifications/assigned-numbers/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
由以上分析可以看出, 只需处理 AD Type 为 0x02, 0x03, 0x06, 0x07 的 AD 数据段即可获取服务 UUID 的列表, 以下是具体代码:
- /**
- * 从广播包中获取所有服务的 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()执行, 则获取不到服务.
使用
BluetoothGattService
的
getCharacteristics()
方法获取该服务的所有特征值. 可使用特征对象的一系列 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 位预留. 使用以下代码添加该属性值的通知属性.
- 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 设备, 可添加服务, 特征等.
来源: https://www.qcloud.com/developer/article/1158835