前言
因笔者工作需要,开发一款蓝牙秤的数据读取软件。有些心得,便在此记录。俗话说:好记性不如烂笔头。笔者也是为了备忘。写下本篇博客。帮助有需要的同学~
本篇文章来源于开发者指导,开发者指导示例代码。以及自己的一些心得。帮助不会蓝牙低功耗的同学快速入门。
本篇博客有大多数都是官网直译,小部分为自己心得。如有翻译出错地方,及文章错误。望众大神在评论区指出。笔者定勤加修改
低功耗蓝牙概述
Android 4.3(API级别18)引入了内置平台支持蓝牙低功耗(BLE)的核心角色,并提供应用程序可用于发现设备,查询服务和传输信息的API。
常见用例包括以下内容:
- 在附近设备之间传输少量数据。
- 与Google Beacons等接近传感器进行交互,根据用户的当前位置为用户提供定制体验。
与传统蓝牙相比,蓝牙低功耗(BLE)旨在提供显着降低的功耗。 这允许Android应用程序与具有更严格电源要求的BLE设备通信,例如接近传感器,心率监视器和健身设备。
注:Android从4.3(API Level 18) 开始支持低功耗蓝牙,但是刚开始只支持作为中央设备(central)模式,从 Android 5.0(API Level 21) 开始才支持作为外围设备(peripheral)的模式,因此我们最好使用Android 5.0以上版本的手机进行下面的操作。
关键术语和概念
以下是关键BLE术语和概念的摘要:
-
Generic Attribute Profile (GATT) ——GATT配置文件是用于在BLE链路上发送和接收称为“属性”的短数据的通用规范。 目前所有低能耗应用配置文件均基于GATT。
-
Bluetooth SIG为低能耗设备定义了许多配置文件。 配置文件是设备在特定应用程序中的工作方式的规范。 请注意,设备可以实现多个配置文件。 例如,设备可以包含心率监测器和电池水平检测器。
-
Attribute Protocol (ATT) ——GATT建立在属性协议(ATT)之上。 这也称为GATT / ATT。 ATT经过优化,可在BLE设备上运行。 为此,它使用尽可能少的字节。 每个属性由通用唯一标识符(UUID)唯一标识,UUID是用于唯一标识信息的字符串ID的标准化128位格式。 ATT传输的属性被格式化为特征和服务。
-
Characteristic——特征包含单个值和描述特征值的0-n描述符。 特征可以被认为是类型,类似于类。
-
Descriptor——描述符是定义描述特征值的属性。 例如,描述符可以指定人类可读的描述,特征值的可接受范围,或特征值特定的度量单位。
-
Service——服务是一系列特征。 例如,您可以使用名为“心率监测器”的服务,其中包括“心率测量”等特征。 您可以在bluetooth.org上找到基于GATT的现有配置文件和服务的列表。
角色和责任
以下是Android设备与BLE设备交互时应用的角色和职责:
-
中央设备(central)(简单说:GATT服务器)
作用:收到外围设备发出的广播信号后能主动发起连接的主设备,例如我们给摩拜单车开锁时我们的手机就是作为中央设备连接单车并进行开锁等一系列操作的,通常情况下同一时间一台中央设备只能与最多7台外围设备建立连接。 -
外围设备(peripheral)(简单说:GATT客户端)
作用:能被中央设备连接的从设备,同一时间外围设备只能被一个中央设备连接。 -
GATT服务器与GATT客户端。 这确定了两个设备建立连接后如何相互通信。
低功耗蓝牙权限
要在您的应用程序中使用蓝牙功能,您必须声明蓝牙权限BLUETOOTH。 您需要此权限才能执行任何蓝牙通信,例如请求连接,接受连接和传输数据。
如果应用程序需要操作蓝牙(如打开,关闭蓝牙等),则还须声明BLUETOOTH_ADMIN权限。
注:如果使用BLUETOOTH_ADMIN权限,则必须具有BLUETOOTH权限。
在应用程序清单文件中声明蓝牙权限。 例如:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!--如果需要搜索蓝牙设备,需要加上定位权限-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
如果您要声明您的应用仅适用于支持BLE的设备,请在应用清单中包含以下内容:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
如果你希望应用装在没有低功耗蓝牙的设备上,你需要将required设置为false, 然后在运行时通过PackageManager.hasSystemFeature进行动态判断,是否加载蓝牙特性:
// 使用此检查确定设备是否支持BLE。 然后,您可以有选择地禁用与BLE相关的功能。
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
finish();
}
注意:LE Beacons通常与位置相关联。 要使用BluetoothLeScanner,您必须通过在应用程序的清单文件中声明ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION权限来请求用户的权限。 没有这些权限,扫描将不会返回任何结果。
开始
接下来我们就准备开始实际操作了,首先我们准备2台手机,手机A作为中央设备(客户端),手机B作为外围设备(服务器),在打开B手机的ble广播后,我们使用A手机进行打开蓝牙–>扫描–>连接–>获取服务,特征–>打开通知–>写特征–>读特征–>断开连接,通过这些步骤我们就能学会Android Ble 的基本方法的使用。
设置低功耗蓝牙
在您的应用程序可以通过BLE进行通信之前,您需要验证设备是否支持BLE,如果是,请确保它已启用。 请注意,仅当<uses-feature … />设置为false时才需要进行此检查。
如果不支持BLE,则应优雅地禁用任何BLE功能。 如果BLE受支持但已禁用,则您可以请求用户启用蓝牙而无需离开您的应用程序。 使用BluetoothAdapter,可以分两步完成此设置。
- 获取BluetoothAdapter
任何和所有蓝牙活动都需要BluetoothAdapter。 BluetoothAdapter代表设备自己的蓝牙适配器(蓝牙无线电)。 整个系统都有一个蓝牙适配器,您的应用程序可以使用此对象与其进行交互。 下面的代码段显示了如何获取适配器。 请注意,此方法使用getSystemService()返回BluetoothManager的实例,然后使用该实例获取适配器。 Android 4.3(API Level 18)介绍了BluetoothManager:
private BluetoothAdapter bluetoothAdapter;
final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
- 启用蓝牙(两种方式)
接下来,您需要确保启用蓝牙。 调用isEnabled()检查当前是否启用了蓝牙。 如果此方法返回false,则蓝牙是关闭状态。 以下代码段会检查是否已启用蓝牙。 如果不是,则会提示用户打开蓝牙(方法一)或 自行打开(方法二):
//方式一: 确保设备上可以使用蓝牙并启用蓝牙。 如果没有,则显示一个对话框,请求用户启用蓝牙权限。
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
//方法二
if (mBluetoothAdapter != null){
mBluetoothAdapter.enable();
}
使用方法二将会直接打开蓝牙,使用方法一会跳转到系统Activity由用户手动打开蓝牙
注意:传递给startActivityForResult(android.content.Intent,int)的REQUEST_ENABLE_BT常量是一个本地定义的整数(必须大于0),系统会在你的onActivityResult(int,int,android.content)中传回给你。 Intent)实现为requestCode参数。
扫描低功耗设备
查找BLE设备,请使用BluetoothLeScanner.startScan(ScanCallback)方法。 此方法将ScanCallback作为参数。 您必须实现此回调,因为这是返回扫描结果的方式。 由于扫描是非常浪费资源的,因此您应遵守以下准则:
- 找到所需设备后,请立即停止扫描。
- 切勿循环扫描,并设置扫描时间限制。 之前可用的设备可能已超出范围,继续扫描会消耗资源。
以下代码段显示了如何启动和停止扫描:
/**
* 停止蓝牙扫描
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public void stopScanLeDevice(ScanCallback scanCallback) {
if (isEnable && mBluetoothAdapter.isEnabled()) {
BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.stopScan(scanCallback);
}
}
/**
* 开始蓝牙扫描
*/
@RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
public void scanLeDevice(ScanCallback scanCallback) {
if (isEnable && mBluetoothAdapter.isEnabled()) {
/*
旧api:Android 5.0被标记过时
//如果想要指定搜索设备,可以使用下面这个构造方法,传入外围设备广播出的服务的UUID数组
UUID[] uuids=new UUID[]{UUID_ADV_SERVER};
mBluetoothAdapter.startLeScan(uuids,mLeScanCallback);
mBluetoothAdapter.startLeScan(mLeScanCallback);
这是个在Android 5.0时被标注deprecated的API,该方法目前仍能使用。
由于onLeScan中回调出的设备的广播数据需要自己手动解析,这是个比较麻烦的过程。
*/
//新api:基本扫描方式
BluetoothLeScanner bluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
bluetoothLeScanner.startScan(scanCallback);
/*
//设置一些扫描参数
ScanSettings settings = new ScanSettings.Builder()
//例如这里设置的低延迟模式,也就是更快的扫描到周围设备,相应耗电也更厉害
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
//你需要设置的过滤条件,不只可以像旧API中的按服务UUID过滤
//还可以按设备名称,MAC地址等条件过滤
List<ScanFilter> scanFilters = new ArrayList<>();
//如果你需要过滤扫描到的设备可以用下面的这种构造方法
bluetoothLeScanner.startScan(scanFilters, settings, mScanCallback);
*/
}
}
以下是ScanCallback的说明:
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
//callbackType:扫描模式
//result:扫描到的设备数据,包含蓝牙设备对象,解析完成的广播数据等
result.getDevice();//蓝牙设备
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
L.e("批次扫描结果-onBatchScanResults");
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
L.e("扫描失败:onScanFailed-errorCode:" + errorCode);
}
};
相比旧API,新API的功能更全面,但是需要Android 5.0以上才能使用,究竟需要使用哪种方法,大家可以根据自己的实际情况选择。
注意坑来了:
1.如果搜索不到设备,请检查对于Android 6.0及以上版本ACCESS_COARSE_LOCATION或者ACCESS_FINE_LOCATION权限是否已经动态授予,同时检查位置信息(也就是GPS)是否已经打开,一般来说搜不到设备就是这两个原因。
2.不管是新旧API的扫描结果回调都是不停的回调扫描到的设备,就算是相同的设备也会重复回调,直到你停止扫描,因此最好不要在回调方法中做过多的耗时操作,否则可能会出现问题,如果需要处理回调的数据可以把数据放到另外一个线程处理,让回调尽快返回。
连接到GATT 服务
同一时间我们只能对一个外围设备发起连接,如果需要对多个设备连接可以等上一个连接成功后再进行下一个连接,否则如果前面的某个连接操作失败了没有回调,后面的操作会被一直阻塞。
与BLE设备交互的第一步是连接到它 - 更具体地说,连接到设备上的GATT服务器。 要连接到BLE设备上的GATT服务器,请使用connectGatt()方法。 此方法有三个参数:一个Context对象,autoConnect(指示是否在可用时自动连接到BLE设备),以及对BluetoothGattCallback的引用:
mBluetoothGatt = device.connectGatt(mContext, true, mBluetoothGattCallback);
这将连接到BLE设备托管的GATT服务器,并返回一个BluetoothGatt实例,然后您可以使用该实例执行GATT客户端操作。BluetoothGattCallback用于向客户端提供结果,例如连接状态,以及任何进一步的GATT客户端操作。
Gatt服务器状态改变会调用监听器中的回调函数:
//发起连接
private void connect(BluetoothDevice device){
mBluetoothGatt = device.connectGatt(context, false, mBluetoothGattCallback);
}
//Gatt操作回调,此回调很重要,后面所有的操作结果都会在此方法中回调
BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
//当连接状态改变时触发此回调
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
//newState:当前连接处于的状态,例如连接成功,断开连接等
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//成功获取服务时触发此回调,“获取服务&特征”一节会介绍
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
}
@Override
public void onCharacteristicRead(
BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
//当对特征的读操作完成时触发此回调,“读特征”一节会介绍
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
//characteristic:被读的特征
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic, final int status) {
//当对特征的写操作完成时触发此回调,“写特征”一节会介绍
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
//characteristic:被写的特征
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic) {
//当特征值改变时触发此回调,“打开通知”一节会介绍
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
//characteristic:特征值改变的特征
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status) {
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
//descriptor:被读的descriptor
//当对descriptor的读操作完成时触发
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
int status) {
//gatt:GATT客户端
//status:此次操作的状态码,返回0时代表操作成功,返回其他值就是各种异常
//descriptor:被写的descriptor
//当对descriptor的写操作完成时触发,“打开通知”一节会介绍
}
};
当我们调用connectGatt方法后会触发onConnectionStateChange这个回调,回调中的status我们用来判断这次操作的成功与否,newState用来判断当前的连接状态。
注:
我们在调用连接和断开连接这两方法的时候最好放到主线程调用,否则可能会在一些手机上遇到奇怪的问题
获取服务,特征
当我们连接成功后,GATT客户端(手机A)可以通过发现方法检索GATT服务端(手机B)的服务和特征,以便后面操作使用。
//连接成功后掉用发现服务
gatt.discoverServices();
//当服务检索完成后会回调该方法,检索完成后我们就可以拿到需要的服务和特征
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//获取特定UUID的服务
BluetoothGattService service = gatt.getService(UUID_SERVER);
if (service != null) {
//获取该服务下特定UUID的特征
mCharacteristic = service.getCharacteristic(UUID_CHARWRITE);
//获取该服务下所有特征
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
}
//获取所有服务
List<BluetoothGattService> services = gatt.getServices();
}
/**
* 工具方法——遍历所有服务和特征
* @param gatt
*/
private void displayGattServices(BluetoothGatt gatt) {
List<BluetoothGattService> gattServices = gatt.getServices();
if (gattServices == null) return;
for (BluetoothGattService gattService : gattServices) {
for (BluetoothGattCharacteristic bluetoothGattCharacteristic : gattService.getCharacteristics()) {
}
}
}
数据变化通知
//官方文档做法
private BluetoothGatt mBluetoothGatt;
private BluetoothGattCharacteristic characteristic;
private boolean enabled;
...
//第一步,开启手机A(本地)对这个特征的通知
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
//第二步,通过对手机B(远程)中需要开启通知的那个特征的CCCD写入开启通知命令,来打开通知
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
由于Android7.0以前版本存在一个bug:对descriptor的写操作会复用父特征的写入类型,这个bug在7.0之后进行了修复,为了提高兼容性,我们可以对官方做法稍许修改
private BluetoothGatt mBluetoothGatt;
private BluetoothGattCharacteristic characteristic;
private boolean enabled;
...
//第一步,开启手机A(本地)对这个特征的通知
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
//第二步,通过对手机B(远程)中需要开启通知的那个特征的CCCD写入开启通知命令,来打开通知
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG));
//获取特征的写入类型,用于后面还原
int parentWriteType = characteristic.getWriteType();
//设置特征的写入类型为默认类型
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
//还原特征的写入类型
characteristic.setWriteType(parentWriteType);
- 对于有的设备可能我们只需要执行第一步就能收到通知,但是为了保险起见我们最好两步都做,以防出现通知开启无效的情况。
- 再次强调读、写、通知等这些GATT的操作都只能串行的使用,并且在执行下一个任务前必须保证上一个任务已经完成并且成功回调,否则可能出现后面的任务都阻塞无法进行的情况。
- 对于开启通知这个操作触发onDescriptorWrite时代表任务完成,可以进行下一个GATT操作。
写特征
//默认的写入类型,需要外围设备响应
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
//无需设备响应的写入类型
mCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
mCharacteristic.setValue(data);
mBluetoothGatt.writeCharacteristic(mCharacteristic);
//写入特征回调
@Override
public void onCharacteristicWrite(
BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
}
写特征的用法和前面打开通知中的写descriptor类似。
上面提到了2种写入类型,他们的区别是:
- WRITE_TYPE_DEFAULT:写入数据后需要外围设备给出响应才会回调onCharacteristicWrite
- WRITE_TYPE_NO_RESPONSE:写入数据后无需外围设备给出响应就会回调onCharacteristicWrite
- 一次写入最多能写入20字节的数据,如果需要写入更多的数据可以分包多次写入,或者如果设备支持更改MTU的话一次最多可以传输512字节。
- 如果使用WRITE_TYPE_DEFAULT这种类型写入,而外围设备没有回应,那后面的操作都会被阻塞。因此,使用哪种方式需要大家根据自己的外围设备决定,大家可以尝试把
//读特征
mBluetoothGatt.readCharacteristic(mCharacteristic);
//读特征的回调
@Override
public void onCharacteristicRead(
BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
}
使用该回调的小例子:
private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
L.e("===========\t连接状态改变\t===========");
if (newState == BluetoothProfile.STATE_CONNECTING) {
L.e("-----------\t连接中....\t-----------");
} else if (newState == BluetoothProfile.STATE_CONNECTED) {
L.e("连接到GATT服务器\n启动搜索GATT服务器存在的服务:" +
(mBluetoothGatt.discoverServices() ? "启动成功" : "启动失败"));
//调用discoverServices函数会触发onServicesDiscovered函数
//停止扫描
mBluetoothController.stopScanLeDevice(mScanCallback);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED || newState == BluetoothProfile.STATE_DISCONNECTING) {
L.e("与GATT服务器断开连接.");
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//发现了新服务
if (status == BluetoothGatt.GATT_SUCCESS) {
displayGattServices(gatt);
} else {
L.e("onServicesDiscovered received: " + status);
}
}
@Override
// 特征读取操作的结果
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
}
}
/**
* 特征值改变
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
byte[] data = characteristic.getValue();
}
};
收到GATT通知
BLE应用程序通常会要求在设备上的特定特征发生变化时收到通知。此代码段显示如何使用setCharacteristicNotification()方法设置特征的通知:
mBluetoothGatt.setCharacteristicNotification(mBluetoothGattCharacteristic, true);
BluetoothGattDescriptor descriptor = mBluetoothGattCharacteristic.getDescriptor(
mBluetoothGattCharacteristic.getUuid());
if (descriptor != null) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
为特性启用通知后,如果远程设备上的特性发生更改,则会触发onCharacteristicChanged()回调:
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
byte[] data = characteristic.getValue();
}
释放资源
当应用程序使用完蓝牙设备应该将其关闭,以节约资源、电量。
@Override
protected void onDestroy() {
if (mBluetoothGatt != null) {
mBluetoothGatt.close();
mBluetoothGatt = null;
}
super.onDestroy();
}
本篇博客的完整代码已经上传至GitHub:BlueToothLeClientDemo
断开连接和连接一样最好都在主线程执行
BluetoothGatt.disConnect()方法和BluetoothGatt.close()方法要成对配合使用,有一点需要注意:如果调用disConnect()方法后立即调用close()方法(就像上面注释掉的代码那样)蓝牙能正常断开,只是在onConnectionStateChange中我们就收不到newState为BluetoothProfile.STATE_DISCONNECTED的状态回调,因此,可以在收到断开连接的回调后在关闭GATT客户端。
如果断开连接后没调用close方法,在多次重复连接-断开之后可能你就再也连不上设备了。
开源库:https://github.com/NordicSemiconductor/Android-BLE-Library