BLE是蓝牙4.0标准的一部分,旨在解决传统蓝牙连接慢、能耗大的问题,Google在Android 4.3(API 18)中引入了对BLE的支持。BLE连接使用GAP(Generic Access Profile)协议,通信使用GATT(Generic Attribute Profile)协议。GATT又以ATT为基础,所有的LE服务都以ATT作为应用层协议。以下深入地介绍这两个协议。
属性(attribute)是ATT的基础,一个attribute的组成部分有三:
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的用法。
GATT是所有高层LE协议的基础,它将ATT进一步封装,定义了连接LE设备使用的分层数据结构。
GATT Profile描述了基于GATT功能的用例、角色和通用行为。服务(Service)是特征(Characteristic)的集合,多个相关联的服务表现出了设备的行为。1
层次结构的顶级是一个配置文件(Profile),由一个或多个服务(Service)构成。服务由特征(Characteristic)或对其他服务的引用组成。特征包括一种类型(用UUID表示),一个值,一组指示特征支持操作的属性和一组与安全性有关的权限。特征还可以包括一个或多个描述符(Descriptor)——与所拥有的特征相关的元数据或配置标识。
GATT将这些服务分组以封装设备的行为,并根据GATT功能描述用例,角色和一般行为。该框架定义了服务的过程,格式及其特征,包括发现、读取、写入、通知和指示特征,以及配置特征的广播。
举个例子,一个Profile中有一个温度计服务(Service),这个服务包含一个只读的温度特征(Characteristic)和一个可读写的时间特征(Characteristic)。温度特征又包含了一个温度值(value),和单位描述(Descriptor);时间特征包含了一个时间值(value)和时区描述(Descriptor)。
在GATT中,Service,Characteristic 和 Descriptor 都使用UUID作为唯一标识。那么什么是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保留供将来使用。
另外,在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。
涉及到蓝牙相关开发需要在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)及以上的设备中使用该权限需要动态申请。
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: 权限被拒绝的操作
}
}
}各类之间的关系如下图所示(略去了每个方法的参数)。
BluetoothAdapter.getDefaultAdapter();
获取蓝牙适配器实例。BluetoothAdapter
对象的enable()
方法打开蓝牙。
- 也可构建intent,请求用户打开蓝牙。Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, QWALLET_REQUEST_OPEN_BLUETOOTH);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的列表,首先分析广播包的数据格式。
广播包有两种:
此处讨论的包格式只讨论包中的数据段(即onLeScan()回调方法的参数byte[] scanRecord),不包括完整报文的其他部分,如前导、接入地址等。下图所示为包中数据段的格式。
数据包括了有效部分和无效部分。有效部分由若干个广播数据段(AD Structure)序列构成,每个广播数据段的组成为:
无效部分预留了数据包的扩展能力,无效部分全为0.
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,具体分析如下:
由以上分析可以看出,只需处理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,需先转换。
使用上一步获取到的BluetoothDevice
对象,或根据MAC地址,使用BluetoothAdapter
对象的getRemoteDevice(String address)
方法重构一个BluetoothDevice
对象。
使用方法connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)
连接设备,得到BluetoothGatt
对象。BluetoothGattCallback
用于BluetoothGatt
对象所有耗时操作的回调。connectGatt方法获取到BluetoothGatt对象之后,设备将处于正在连接状态(可能会连接失败),当设备处于已连接状态时,才可进行后续操作。可用BluetoothGattCallback
中的onConnectionStateChange
方法监听连接状态的变化。
GATT 连接需要特别注意的是:GATT 连接是独占的。也就是一个 BLE 外设同时只能被一个中心设备连接。一旦外设被连接,它就会马上停止广播,这样它就对其他设备不可见了。当设备断开,它又开始广播。
使用BluetoothGatt
对象的discoverServices()
方法发现服务,在回调方法onServicesDiscovered()
中进行发现服务后的操作。
使用BluetoothGatt
对象的getServices()
获取服务BluetoothGattService
列表。若getServices()
先于discoverServices()
执行,则获取不到服务。
使用BluetoothGattService
的getCharacteristics()
方法获取该服务的所有特征值。可使用特征对象的一系列get方法获取特征的信息。
这里需要注意的是getProperties()
方法,该方法得到的是一个int值,换为二进制,每一位表示了特征对象的一个属性值,执行总属性值与对应属性位的与操作可得到该位的属性值。举个例子,如果要获取该特征的写属性,可执行getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE
,若得到的值大于0,则说明该特征支持写属性。
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);
}
在iOS应用商店可以搜到应用LightBlue
,该应用可模拟BLE设备,可添加服务、特征等。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。