安卓 BLE 开发详解
相关概念
-
BR
Basic Rate,早期的传统蓝牙技术 V1.1, V1.2 版本,传输速率为748~810kb/s。 -
EDR
Enhanced Data Rate,传统蓝牙技术 V2.0, V2.1 版本,优化传输速率,减少耗电,速率为1.8M/s~2.1M/s。 -
AMP
GenericAlternate MAC/PHY,高速蓝牙技术,V3.0版本。
采用交替射频技术,蓝牙模块仅创建设备间的配对,数据传输通过WIFI射频来完成以达到高速率。
假如设备某一方没有内建WIFI模块,速率将降至 EDR 速率。 -
BLE
Bluetooth Low Energy,低耗蓝牙技术,V4.0版本的新规范,通过三个方式实现超低功耗:
1.大幅度削减扫描信道
2.极短的链路连接时间
3.采用长度很短的数据包
低耗蓝牙的芯片有单模和双模,前者只支持LE技术,后者兼容BR/EDR技术。
1:GATT 协议
-
GATT概述
GATT(Generic Attributes,通用属性协议),定义了一种面向 BLE设备 的分层数据结构。
GATT建立在ATT( Attribute Protocol,通用访问协议)之上,ATT使用GATT数据定义两个BLE设备间收发标准消息的方式。
由于 GATT 是面向 LE 技术的协议,所以在只支持 BR/EDR 技术的设备上无法使用。 -
GATT分层数据结构的层次
GATT定义了用于BLE设备传输数据的标准数据结构,结构主要包括了如上图所示的:
1.服务(Service)
2.特征(Characteristic)
3.描述符(Descriptor)。 -
配置文件(Profile):
配置文件,GATT顶层,该由满足 配置实例 需要的一个或多个服务组成。 -
服务(Service):
服务 由 特征 和 其他服务的引用 组成,拥有固定的 UUID 作为标记值。
设备的功能主要体现在服务上,每种服务都对应着某一种功能。可以到官网上查看服务列表 GATT Services。
通过服务列表中的 Assigned Numbers 可以获取服务的UUID。Assigned Numbers转换成可用的服务UUID 的方法于文档 Service Discovery。
简单来说,就是:"服务的Assigned Numbers"-0000-1000-8000-00805F9B34FB
-
特征(Characteristic):
特征是BLE通信的主体,是一个服务端和客户端共享的读写空间。
主机在从机上获取所需的信息,实际就是通过获取对应的特征的内容进行的。特征由属性值和描述符组成:
- 属性值
属性值包括声明(Declaration),值(Value),一个属性值最少包括一个声明和一个值,即是属性值是特征必选的条目。 - 描述符
特征可以包括零到若干个描述符,可选条目。
特征信息列表可以查看官方文档 GATT Characteristics。
- 属性值
-
描述符(Descriptors)
用于表达 特征 的其他附加信息,如特征值的有效范围,可读性描述等信息。其中包含了特殊的 CCCD(Client Characteristic Configuration Descriptor, Assigned Number : 0x2902):
CCCD 可以设置 服务端 在对应特征值发生变化时,是否对 客户端 进行信息 推送(直接发送信息) 或 提示(发送一个提示并等待回复)。
当特征包含通知能力时,CCCD为必选项。描述符列表可以查看官方文档 GATT Descriptors。
2:Android BLE 相关 API
-
BluetoothAdapter
蓝牙适配器:本地设备蓝牙适配器,提供基本蓝牙功能的工具,例如开启蓝牙发现,查询配对设备,实例化蓝牙设备链接,监听连接请求,扫描设备等。
基本上说,蓝牙适配器是进行蓝牙操作的起点。获取BluetoothAdapter实例,在 API 18 及以上的设备,使用:
BluetoothManager.getAdapter
在API18以下设备使用以下API获取:
BluetoothAdapter.getDefaultAdapte
本类线程安全。涉及到的权限为:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-
BluetoothDevice
远程蓝牙设备:提供了远程蓝牙设备的基本信息,如名称,地址,类别,绑定状态等。
本质上只是对蓝牙硬件地址的简单包装。该类的实例不可修改。一般来说,通过扫描设备的扫描结果回调中获取。
也可以直接通过以下方式获取:/* 使用已知的物理地址作为参数进行连接 */ BluetoothAdapter.getRemoteDevice(address); /* 获取已适配的蓝牙记录列表 */ BluetoothAdapter.getBondedDevices();
-
BluetoothGatt
GATT客户端,GATT协议的公共API,提供了GATT的基本功能,如实现蓝牙设备的通信。
通过扫描支持LE技术的蓝牙设备,获取到 BluetoothDevice,然后通过:/* GATT连接操作的回调 */ BluetoothGattCallback mCallback; BluetoothDevice.connectGatt(content, autoConnect, mCallback);
通过设置 BluetoothGattCallback 回调,可以从回调中得到 BluetoothGatt 实例。
-
BluetoothGattCallback
GATT状态回调,大部分GATT操作的结果都会通过该类实例回调,包括:/* 连接状态回调,包括连接到服务器 / 从服务器断开连接 */ onConnectionStateChange(); /* 远程设备发现新服务 */ onServicesDiscovered(); /* 特征相关操作的回调 */ onCharacteristicRead(); onCharacteristicWrite(); onCharacteristicChanged();
同时,扫描设备 和 停止扫描 的操作,都需要用到该类的实例。
-
BluetoothGattService
GATT服务,根据服务的 UUID,尝试获取服务实例。/* 如果对应的设备支持该服务,则返回一个服务的实例,否则返回空 */ BluetoothGatt.getService(uuid);
-
BluetoothGattCharacteristic
GATT特征,实际通信中的数据信息主体。通过以下方法获取:/* 获取对应UUID的特征 */ BluetoothGattService.getCharacteristic(uuid); /* 获取服务的特征列表 */ BluetoothGattService.getCharacteristics();
3:Android BLE 开发示例
-
声明权限
一个声明和两个基本权限:
<uses-feature android:name"android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
执行搜索BLE设备的时候,需要使用定位权限。
而在5.0及以上的版本,需要手动声明GPS硬件模块功能的权限:<uses-feature android:name="android.hardware.location.gps"/>
而在6.0及以上版本,扫描设备还需要 动态申请 以下权限:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-
检查设备支持性
如果设备不支持BLE,可以跳过BLE相关操作了。boolean checkSupport() { return getPackageManager() .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); }
-
初始化BluetoothAdapter
private BluetoothAdapter mAdapter; BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); mAdapter = bluetoothManager.getAdapter();
然后检查蓝牙的支持性,及是否已打开蓝牙。
if (mAdapter == null) { return; } ... private final static int REQUEST_ENABLE_BT = 1; if (!mAdapter.isEnabled()) { Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(intent, REQUEST_ENABLE_BT); }
-
启动设备扫描
创建LeScanCallback实例:
首先需要实现一个 LeScanCallback 实例,扫描结果会通过实例的 onLeScan 方法返回:LeScanCallback mCallBack = new LeScanCallback (){ @Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {} }
启动扫描与停止扫描:
··· static int SCAN_TIME = 5_000; ··· Handler mHandler = new Handler(); /* 开始扫描: 由于扫描消耗电量,所以不能一直处于扫描状态, 设置扫描一段时间后关闭扫描 */ mAdapter.startLeScan(mCallBack); mHandler.postDelay(()->{ /* 关闭扫描: * 注意需要传入启动扫描时的 callback对象,否则无效 */ mAdapter.stopLeScan(mCallBack); }, SCAN_TIME);
在 API 21 及以上时,扫描操作应使用 BluetoothLeScanner:
final ScanCallback callback = new ScanCallback() {}; final BluetoothLeScanner scanner = mAdapter.getBluetoothLeScanner(); scanner.startScan(new ScanCallback(){}); mHandler.postDelay(()->{ scanner.stopScan(scanCallback); }, SCAN_TIME);
-
获取扫描结果
以 LeScanCallback 的回调方法 onLeScan 分析:/** * @param device: 识别到的远程设备 * * @param rssi: 信号强度指示,计数为dB。可以通过: d = 10^((abs(RSSI) - A) / (10 * n)) 计算出距离。A和n根据环境改变,需经实验测出, 给出两个网上的经验值: <1> A: 50 n: 2.5 <2> A: 59 n: 2.0 * * @param scanRecord:广播数据和扫描应答数据数据 BLE设备在对外广播中,广播中会携带一些有用的信息。 其中包含了 广播数据 和 扫描应答数据, 两者有效荷载最大都为 31字节(蓝牙4), 以十六进制格式存储,可通过 bytesToHex 转换成可用的字符串。 */ void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {}
注意相同的 BluetoothDevice 会重复出现在回调中,所以如果要记录蓝牙列表,需要自行 过滤 重复出现的设备,或更新对应重复出现的设备的信息。
bytesToHex 参考 -
连接外围设备
通过 BluetoothDevice 的 connectGatt 方法获取一个 BluetoothGatt 实例。
connectGatt 有多个重载方法,这里介绍其中最复杂的重载方法:/** * 以客户端的身份连接到该设备托管的GATT服务器 * * @param autoConnect:自动连接,设备不可用时会不断尝试重连。 * * @param callback: BluetoothGattCallback实例,用于接收异步回调 * * @param transport: GATT连接到双模设备的首选传输模式: * 1:TRANSPORT_AUTO 自动选择 (默认值) * 2:TRANSPORT_BREDR BR/EDR 传统蓝牙 * 3:TRANSPORT_LE LE 低耗蓝牙 * * @param phy: PHY物理层的模式选择: * 1:PHY_LE_1M_MASK: * 默认值,LE设备强制要求支持的模式, * 符号速率为1M/s,未编码。 * 2:PHY_LE_2M_MASK: * 符号速率为2M/s,未编码, * 用于 蓝牙5 的 "2x speed" 2倍速率。 * 3:PHY_LE_CODED_MASK: * 在数据包中增加纠错编码以实现更远的传输范围, * 以实现 蓝牙5 的 "4x range" 4倍范围。 * 使用FEC编码,根据方案又分为: * LE Coded S=2:2个编码位代替原来一个数据位, * 速率降为 500K/s,传输范围增大2倍; * LE Coded S=8:8个编码位代替原来一个数据位, * 速率降为 125K/s,传输范围增大4倍; * 设置 autoConnect 自动连接时,该项无效 * * @param handler: 传入一个Handler,以指定回调发生的线程, * 传入null时,回调将会在一个未指定的后台线程上进行。 */ BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback, int transport, int phy, Handler handler) { ··· }
一般情况下使用默认值既可,
注意必须传入非空的callback,否则会抛出 IllegalArgumentException:BluetoothDevice.connectGatt(content, autoConnect, callback);
当连接成功时,会回调 callback 的 onConnectionStateChange 方法
/** * GATT客户端的连接状态回调 * * @param gatt: GATT客户端。 * @param status: 连接或断开操作的执行结果, 成功返回 GATT_SUCCESS * @param newState:当前的连接状态:STATE_CONNECTED / STATE_DISCONNECTED */ void onConnectionStateChange(BluetoothGatt gatt, int status, int newState);
status 表示连接操作的结果,只有status为 GATT_SUCCESS 时,newState才是有效值。
注意一台安卓设备最多同时连接6个左右的蓝牙设备,超出时可能出现:
status == 133 连接错误,
所以需要注意调用 BluetoothGatt.close() 方法进行资源释放。
可参考:Android中BLE连接出现“BluetoothGatt status 133”的解决方法当 status == GATT_SUCCESS,且 newState == STATE_CONNECTED 时,表示已成功连接设备,可以进行下一步操作。
-
发现服务
在建立连接之后,就可以通过 BluetoothGatt实例 进行发现服务操作,查找设备支持的服务。/** * 异步操作,发现服务完成时,会回调onServicesDiscovered()方法。 * 假如发现服务已在启动状态中,则返回true */ boolean discoverService();
等待 BluetoothGattCallback 的 onServicesDiscovered() 被回调:
/** * @param gatt: 执行发现服务后的GATT客户端。 * @param status: 发现服务的执行结果, 成功返回 GATT_SUCCESS */ void onServicesDiscovered(BluetoothGatt gatt, int status) ;
当 status 返回GATT_SUCCESS,表示与外部设备成功建立 可通信连接,
意味着可以执行如:写入数据,读取蓝牙设备的数据等 蓝牙通信操作了。
先把获取到的 BluetoothGatt实例 记录为 mGatt:··· BluetoothGatt mGatt; void onServicesDiscovered(BluetoothGatt gatt, int status) { mGatt = gatt; }
-
获取服务
发现服务成功之后,可以通过以下的方法尝试获取 BluetoothGattService 实例:/* 获取远程设备提供的服务列表, * 如果未执行发现服务,会返回一个空列表 */ mGatt.getServices(); /* 通过服务的UUID,获取指定的服务, * 如果远程设备不支持给定UUID的服务,返回null, * 如果远程设备存在多个给定UUID的服务实例,则返回第一个实例 */ mGatt.getService(UUID);
获取到 BluetoothGattService 之后,就可以通过获取服务的特征进行读写。
-
特征的读写数据
前面介绍了,通信主体实际上是 特征,要进行读写操作,其实就是在操作特征里的属性词条,所以要先通过 服务 获取 特征:/* 假设 service 是从上一步获取到的一个 BluetoothGattService 实例*/ ··· BluetoothGattService service; /* 获取该服务的特征列表 */ service.getCharacteristics(); /* 通过特征的UUID,获取指定的特征, * 如果没有找到给定UUID的特征,返回null, * 如果服务中存在多个给定UUID的特征,则返回第一个实例 */ service.getCharacteristic(UUID);
获取到了特征之后,就可以通过上面获取到的 mGatt 读写信息:
/* 上一步获取的 BluetoothGattCharacteristic 实例 */ ··· BluetoothGattCharacteristic characteristic; /* 从关联的远程设备读取请求的特征, * 异步操作,请求发起成功则返回true,读取完成会回调: * BluetoothGattCallback.onCharacteristicRead() */ mGatt.readCharacteristic(characteristic); /* 将给定的特征及其值写入关联的远程设备, * 异步操作,请求发起成功则返回true,写入完成会回调: * BluetoothGattCallback.onCharacteristicWrite() */ mGatt.writeCharacteristic(characteristic);
读写操作都是异步操作,方法返回的是请求是否成功,请求结果都会回调 BluetoothGattCallback 的方法:
/** * 读操作的回调 * @param characteristic: 读取后的特征 * @param status: 读取结果,成功为 GATT_SUCCESS */ void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { ··· } /** * 写操作的回调 * @param characteristic: 写入后的特征 * 注意:这里返回的特征,为设备当前的特征, * 应该在该回调中,应对比该特征的内容是否符合期望值, * 如果与期望值不同,应该选择重发或终止写入。 * * @param status: 写入结果,成功为 GATT_SUCCESS */ void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { ··· }
写数据的时候要注意,需要对比返回的特征和写入的特征,判断是否写入成功或者产生了异常,选择继续写入或者重写,或者放弃操作。
-
描述符的读写数据
读写方式与 特征 的 读写方式基本一致,不再过多描述 :/* 获取描述符 */ ··· BluetoothGattCharacteristic characteristic; characteristic.getDescriptors(); characteristic.getDescriptor(UUID); /* 通过 mGatt 读写数据 * 同样,写操作需要做写入结果校验 */ ··· BluetoothGattDescriptor descriptor; mGatt.readDescriptor(descriptor); mGatt.writeDescriptor(descriptor); /* 结果回调 */ void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { ··· } void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { ··· }
-
读写数据需要注意的问题
写入数据量:
每次写操作的时候,无论是 特征 或者 描述符,一般来说最大只能设置 20个字节 的数据。
这是因为ATT协议中,最大传输单元MTU的默认大小为23字节,其中3字节用于ATT协议的控制数据,所以GATT可用的数据大小默认为剩余的20字节。ATT的MTU最大值为512,在API 21及以上的安卓平台,可以通过以下方法尝试改变MTU的大小:
··· int mMtu; /* 请求变更MTU的大小 */ BluetoothGatt.requestMtu(mMtu); /* 请求结果通过 BluetoothGattCallback 回调 * 当statue返回为 GATT_SUCCESS 时,表示变更成功 * 变更成功后,可以使用(mMtu - 3)的大小传输数据*/ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {}
无法改变的时候,超过20字节的数据,进行分包发送(BLE服务端需要支持)。
读写间隔:
读写操作都是队列操作,需要等待操作结果返回后,才能进行下次操作,若当次操作未完成,下次操作调用时,将直接返回操作启用失败。写入操作时,需等待服务器的确认信息,即写入回调,再进行下次写入操作。
当写入类型设置为 不需要接收服务器确认信息(PROPERTY_WRITE_NO_RESPONSE)以加快传输速度时,两次操作之间应保留 80ms ~ 100ms 或以上的延时。 -
数据变更通知
前面说到ATT支持通知,一些特征在值发生变化时,可以主动向申请了监听数据变化的客户端推送通知或指示(不带数据)。
开启特征的监听,需要进行两步操作:设置特征信息推送:
/** * 启用或禁用给定特征的通知或指示 * @param characteristic: 需要进行操作的特征 * @param enable : 开启或关闭 */ BluetoothGatt.setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enable);
写入CCCD:
虽然开启了特征的信息推送,但假如特征本身禁用了通知和指示,则不会有更新推送。
前面提到了一个特殊的标识符CCCD,用于控制特征的消息推送。需要对特征的CCCD描述符进行操作,将其值置为 1 / 2,才能开启对应的 通知 / 指示 功能。/* 设置特征信息推送 */ ··· BluetoothGattCharacteristic characteristic; mGatt.setCharacteristicNotification(characteristic,true); /* CCCD 的UUID */ private UUID ID_CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); /* 获取CCCD */ BluetoothGattDescriptor cccd = characteristic.getDescriptor(ID_CCCD); /* 设置推送通知,参考值为: * BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE: 通知 * BluetoothGattDescriptor.ENABLE_INDICATION_VALUE: 指示 * BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE: 关闭 */ cccd.setValue(参考值); /* 写入CCCD */ mGatt.writeDescriptor(descriptor);
以上操作完成后,即开启对应特征的更新推送了。
接收推送:
更新推送会回调BluetoothGattCallback的onCharacteristicChanged()方法:/** * 特征变更推送触发的回调 * @param gatt: 特征 关联的 BluetoothGatt 实例 * @param characteristic: 更新后的 特征 */ void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
-
关闭客户端
用完的东西总是要收拾好。断开连接:
/* 断开当前连接,如果正在连接中,则取消连接操作 */ BluetoothGatt.disconnect();
断开连接操作后,结果回调 onConnectionStateChange() 方法,应该通过回调返回的结果 status 和 newState 判断是否成功断开。
关闭Gatt客户端:
成功断开连接之后(甚至是断开失败),应该调用 BluetoothGatt 的close() 方法关闭客户端释放资源。
安卓同时连接远程设备的资源极其有限,在所以任何情况不再需要连接远程设备时,都要使用BluetoothGatt 的 close() 方法释放资源。
参考文章:
蓝牙技术基础知识学习
蓝牙核心技术概述
GATT协议及蓝牙核心系统结构
Android BLE的总结
Android BLE 蓝牙开发入门
更具体的蓝牙技术说明请查看官方网站
Bluetooth Technology Website
欢迎留言,欢迎关注,会持续更新 安卓开发 中遇到的问题和技术上的一些自我总结。
如有错误,欢迎指出。