1. 介绍
本篇主要基于 Android 官方的低功耗蓝牙连接服务。
讲解如何通过 UUID 连接蓝牙设备。如果你针对 GATT 服务不太了解。那么这篇应该能够稍微帮助到你。
官方文档地址:https://developer.android.google.cn/guide/topics/connectivity/bluetooth-le?hl=zh_cn#connect
2. 概念
如果是老用户了,那么就应该知道曾经蓝牙设备是一个高耗电的部件。根本不可能长时间开启。而在蓝牙4.0版本之后,蓝牙的通讯,耗电,抗干扰都得到了显著提升。同时蓝牙成本也得到了降低。
然后才有了我们现在的各种穿戴设备例如手环,蓝牙耳机,蓝牙电子秤,蓝牙音箱等等的爆发。
同时,其他工业或者外置设备也都开始大量支持蓝牙通讯。因为能耗和成本降低了。
针对低功耗蓝牙通讯,Android 4.3(API 18)开始引入了 BLE 库。我们可以直接使用 Android SDK 中的蓝牙 BLE 库,而不用额外导入依赖库。
以前开发蓝牙通讯,还需要实现蓝牙配对。需要主动跳转到手机设置界面进行PIN码配对,然后配对通过之后才能进行蓝牙链接。
而使用BLE库,我们可以直接通过蓝牙设备的UUID进行连接(通过GATT服务),在当前应用内就能直接连接了。而不用通过系统设置。
市面上的各种手环的自动匹配链接,电子秤的自动连接等等都是通过GATT进行通讯和链接的。
2.1 术语
- GATT:全称为:Generic Attribute Profile,翻译为:通用属性配置文件。GATT 配置文件是一种通用规范,内容针对在 BLE 链路上发送和接收称为“属性ATT”的简短数据片段。目前所有低功耗应用配置文件均以 GATT 为基础。
- ATT:全称为:Attribute Protocol,翻译为:属性协议。它是 GATT 的构建基础,二者的关系也被称为 GATT/ATT。每个属性均由通用唯一标识符 (UUID) 进行唯一标识,后者是用于对信息进行唯一标识的字符串 ID 的 128 位标准化格式。由 ATT 传输的属性采用特征和服务格式。
- 特征 Characteristic: 特征包含一个值和 0 至多个描述特征值的描述符。您可将特征理解为类型,后者与类类似。
- 描述符:描述符是描述特征值的已定义属性。例如,描述符可指定人类可读的描述、特征值的可接受范围或特定于特征值的度量单位。
- Service — 服务是一系列特征。例如,您可能拥有名为“心率监测器”的服务,其中包括“心率测量”等特征。
以上术语的介绍来源于Android官网
2.2 通讯过程
假如我们有一个蓝牙外置设备(Device),然后有一个支持蓝牙的移动设备(Phone)。两者之间的通讯方式步骤是:
- Device 开启蓝牙。(通常这些设备都是开机之后,就默认开启蓝牙了)
- Phone 开启蓝牙。
- Phone 发现 Device。
- Phone 与 Device 创建蓝牙连接。
- Phone 创建 Gatt 客户端,与 Device Gatt 服务端连接。
- Phone 通过 Gatt 服务功能获取 Device 中的消息,并发送消息给 Device 设备。
整个过程就是这样的。下面我也将按照这个通讯过程进行介绍。
3.开发
基于我的使用情况,从无到有的介绍,完整的蓝牙开发配置过程。给大家一个参考
语言主要为 Java
3.1 权限
要在应用中使用蓝牙功能,必须声明 BLUETOOTH 蓝牙权限。需要此权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据等。
同时,还需要位置权限。因为蓝牙 LE 信标通常与位置相关联。如果不开启 ACCESS_FINE_LOCATION 权限。那么我们将会无法发现蓝牙设备。
也就是执行蓝牙扫描 API 无法得到任何结果(PS::Logcat 中的错误日志会告诉你,要开启位置权限,否则无法扫描发现蓝牙设备)。
<!-- 蓝牙搜索配对 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 操纵蓝牙的开启-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 如果应用必须安装在支持蓝牙的设备上,可以将下面的required的值设置为true。-->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="false" />
其中 android.permission.ACCESS_FINE_LOCATION 是高版本API 28 权限。如果要支持更低版本,就需要申请<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />。
如果要执行蓝牙扫描功能,我们需要申请:<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />权限
如果要执行蓝牙链接,开关蓝牙。需要申请:<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />权限
而上面两个权限呢,是在 API 31 上才有效。而低版本就是申请:
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />权限也就够了。
权限配置完毕之后,就是代码开发了。
不管是高版本,还是低版本。将权限都申请可以说最稳妥了。
3.2 检测设备是否支持蓝牙
通常情况下,手机是有蓝牙的。而我们如果在其他 Android 系统的设备中,例如TV,平板,一体机等等。是否有蓝牙还真不能完整保证。
如果不确定的情况下,那么可以通过以下代码检查 BLE 的可用性。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
//不支持蓝牙设备
finish();
} else {
//支持蓝牙设备
}
蓝牙是否开启都不影响检查结果。它检查的是设备是否有蓝牙功能,而不是蓝牙是否启动,下面会介绍如何判断蓝牙是否启动
3.3 开启蓝牙
当我们设备也支持蓝牙了,权限也配置了。下一步就是获取 BluetoothAdapter 对象了。
final BluetoothManager bluetoothManager =(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
我们后续控制蓝牙的状态,都是通过该方法实现的。
首先,检测蓝牙是否开启。可以通过isEnabled()方法进行检测:
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
//开启设备的蓝牙链接
bluetoothAdapter.enable();//开启蓝牙
//动态判断是否拥有位置权限ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION ,然后再执行蓝牙扫描
} else {
//动态判断是否拥有位置权限ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION,然后再执行蓝牙扫描
}
我们其实可以直接使用bluetoothAdapter.enable()开启蓝牙。当蓝牙没有开启时,我们可以直接开启蓝牙。
这个方法的结果,并不是实时返回的。我们如果要知道蓝牙是否开启,需要监听蓝牙状态的广播才行。下面会介绍广播监听。
PS:这个方法需要android.Manifest.permission.BLUETOOTH_CONNECT 权限才能使用。
官方是建议我们通过Intent让系统设置进行开启蓝牙的。
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
但是现在startActivityForResult方法已经过时。我们可以使用Launcher来调用:
ActivityResultLauncher<Intent> launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
//处理返回结果
}
});
上面的 launcher需要在Activity 的 onCreate 方法中初始化。然后在需要进行蓝牙设置界面启动的地方配置:
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); //创建一个蓝牙启动的意图
launcher.launch(enableBtIntent);//使用launcer启动这个意图就可以了。
我们如果使用bluetoothAdapter.enable();时Android Studio出现代码错误警告,可以在该代码使用的方法中添加:@SuppressLint("MissingPermission")注解。
3.4 广播监听
其实这个广播监听,是否需要。根据大家实际情况来定。不一定需要。
首先,创建一个动态广播对象:
public class BluetoothFoundReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
//监听蓝牙状态之后,发送消息
try {
if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
//开始扫描
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
//结束扫描
} else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
//发现设备,每扫码到一个设备,都会触发一次
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//我们可以得到蓝牙设备
} else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
//蓝牙开关状态
int statue = bluetoothAdapter.getState();
switch (statue) {
case BluetoothAdapter.STATE_OFF:
Log.e(TAG, "蓝牙状态:,蓝牙关闭");
break;
case BluetoothAdapter.STATE_ON:
Log.e(TAG, "蓝牙状态:,蓝牙打开");
break;
case BluetoothAdapter.STATE_TURNING_OFF:
Log.e(TAG, "蓝牙状态:,蓝牙正在关闭");
break;
case BluetoothAdapter.STATE_TURNING_ON:
Log.e(TAG, "蓝牙状态:,蓝牙正在打开");
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后进行广播注册:
bluetoothFoundReceiver = new BluetoothFoundReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//连接蓝牙,断开蓝牙
filter.addAction(BluetoothDevice.ACTION_FOUND);//找到设备的广播
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//搜索完成的广播
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);//状态改变 配对开始时,配对成功时
registerReceiver(bluetoothFoundReceiver, filter);
注册完毕后,在onDestroy方法中需要注销注册:
@Override
protected void onDestroy() {
if (bluetoothFoundReceiver != null)
unregisterReceiver(bluetoothFoundReceiver); //停止监听
super.onDestroy();
}
其实,我们只需要蓝牙状态的监听就可以了BluetoothAdapter.ACTION_STATE_CHANGED 其他的设备查找,配对。可以不用,因为触发到广播的设备查找效率太低,而且多次重复查找时,还会出现耗时变长。设备无法查找到的情况。
3.5 蓝牙设备查找
官方文档上推荐的查找方式是:
bluetoothAdapter.startLeScan(leScanCallback); //查找
bluetoothAdapter.stopLeScan(leScanCallback); //停止查找
可是现在这个方法也过时了。替换方法是:
BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
//不进行权限验证
ScanCallback callback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();//得到设备
// Log.e(TAG, "发现设备" + device.getName());
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.e(TAG, "搜索错误" + errorCode);
}
};
scanner.startScan(callback);
onScanResult方法是一个在子线程触发的回调,我们不能在该方法中直接操作UI对象。
其次,扫描到一个蓝牙设备就会触发一次消息回调。我们可以得到一个BluetoothDevice对象。 也就是说这个方法中会触发多次回调,
所以建议,在扫描到我们的蓝牙设备之后,主动调用scanner.stopScan(callback);停止扫描。
PS:这种查找方式,不会触发蓝牙的遍历广播。我们如果开启广播进行监听设备扫描情况。如果通过startScan方法,广播中不会有回调。
上面是一个通用搜索模式,我们还可以配置自己的过滤条件。例如:
ScanFilter sn =new ScanFilter.Builder().setDeviceName("蓝牙设备的名称").setServiceUuid(ParcelUuid.fromString("我们的设备的Service UUID")).build();
List<ScanFilter> scanFilters=new ArrayList<>();
scanFilters.add(sn);
scanner.startScan(scanFilters, new ScanSettings.Builder().build(),callback);
其中ScanFilter对象,我们可以配置我们想查找的蓝牙设备的信息。可以是setDeviceName,setServiceUuid,setDeviceAddress,setServiceSolicitationUuid等。
ScanSettings对象是可以定义我们的扫描模式,通过配置该项可以提高扫描效率。
默认情况下,执行的是:SCAN_MODE_LOW_POWER在低功耗模式下执行蓝牙LE扫描。 这是默认的扫描模式,因为它消耗最少的电量。
3.5.1 startDiscovery
如果上面的方法还不满足我们的情况,可以使用:
if (bluetoothAdapter.isDiscovering()) {//是否在扫描
bluetoothAdapter.cancelDiscovery(); //停止扫描
}
//查找蓝牙
bluetoothAdapter.startDiscovery();
我们可以直接使用bluetoothAdapter进行扫描。这个方法触发之后是由系统进行蓝牙扫描。就和我们在手机的设置界面中点击蓝牙扫描一样。
上面的这个方法没有回调,因为所有的蓝牙设备的发现都将通过广播事件进行传递。
需要通过我上面的广播监听介绍的内容。进行实时获取到扫描到的设备。
使用上面的方法有几个缺点:
1.效率慢,耗时很长。
2.重复扫描会失败。不能说是失败了,而是系统会将重复扫描的请求进行阻止,关键的问题在于这个阻止操作是手机厂商定制的。
PS:不管是BluetoothLeScanner 还是bluetoothAdapter.startDiscovery() 去查找蓝牙设备。都不建议一直重复扫描。否则会出现无法扫描到设备,没有任何扫描结果等等情况。因为扫描是一个耗时耗电的操作。
3.6 链接Gatt
当我们扫描到了蓝牙设备之后,就会获取到BluetoothDevice对象。然后我们通过BluetoothDevice对象创建GATT服务进行后续的蓝牙通讯。
BluetoothDevice device;// 当我们通过扫描得到device对象之后,进行Gatt服务创建
BluetoothGatt bluetoothGatt = device.connectGatt(this, false, gattCallback);
第一个传参context没有什么可以介绍的。
第二个传参autoConnect:是一个boolean值对象,false代表直接连接到蓝牙设备。true代表在蓝牙设备可用时自动连接。
第三个参数BluetoothGattCallback 是Gatt服务的各种回调了。
我们通过gattCallback回调的内容,来得到与蓝牙设备的链接状态,数据通信内容等。
下面来详细介绍下BluetoothGattCallback对象的几个方法。
String SERVICE_UUID="00000-000000-000000-000000";//这个是我要链接的蓝牙设备的ServiceUUID
BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
//GATT的链接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
Log.v(TAG, "连接成功");
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.e(TAG, "连接断开");
} else if (newState == BluetoothProfile.STATE_CONNECTING) {
//TODO 在实际过程中,该方法并没有调用
Log.e(TAG, "连接中....");
}
}
//获取GATT服务发现后的回调
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "GATT_SUCCESS"); //服务发现
for (BluetoothGattService bluetoothGattService : gatt.getServices()) {
Log.e(TAG, "Service_UUID" + bluetoothGattService.getUuid()); // 我们可以遍历到该蓝牙设备的全部Service对象。然后通过比较Service的UUID,我们可以区分该服务是属于什么业务的
if (SERVICE_UUID.equals(bluetoothGattService.getUuid().toString())) {
for (BluetoothGattCharacteristic characteristic : bluetoothGattService.getCharacteristics()) {
prepareBroadcastDataNotify(gatt, characteristic); //给满足条件的属性配置上消息通知
}
return;//结束循环操作
}
}
} else {
Log.e(TAG, "onServicesDiscovered received: " + status);
}
}
//蓝牙设备发送消息后的自动监听
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// readUUID 是我要链接的蓝牙设备的消息读UUID值,跟通知的特性的UUID比较。这样可以避免其他消息的污染。
if (READ_UUID.equals(characteristic.getUuid().toString())) {
try {
String chara = new String(characteristic.getValue(), "UTF-8");
Log.e(TAG, "消息内容:" + chara);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
};
我们可以通过链接成功和链接断开。来判断我们当前与蓝牙设备的通讯状态。
当我们比对Service的UUID成功之后, 我们就可以获取Service的Characteristic对象。该对象也就是特征。通过注册特征来实现消息的监听和发送业务。
3.7 注册消息监听-setCharacteristicNotification
@SuppressLint("MissingPermission")
private void prepareBroadcastDataNotify(BluetoothGatt mBluetoothGatt, BluetoothGattCharacteristic characteristic) {
Log.e(TAG, "CharacteristicUUID:" + characteristic.getUuid().toString());
int charaProp = characteristic.getProperties();
//判断属性是否支持消息通知
if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
BluetoothGattDescriptor descriptor =
characteristic.getDescriptor(UUID.fromString(UUIDManager.READ_DEDSCRIPTION_UUID));
if (descriptor != null) {
//注册消息通知
mBluetoothGatt.setCharacteristicNotification(characteristic, true);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
}
}
在上面的示例中:READ_DEDSCRIPTION_UUID = "00002902-0000-1000-8000-00805f9b34fb" 是固定的,不管你链接什么样的蓝牙设备。
在注册消息监听,都是使用UUID值是00002902-0000-1000-8000-00805f9b34fb进行的。这个是Android系统保留的。用于动态监听的。
你如果不想使用这个动态监听。就需要自己写线程主动去轮询获取到蓝牙设备发送过来的消息了。
到这里,我们其实就能够实现蓝牙设备的实时监听,并得到消息内容了。
3.8 写数据到蓝牙设备中
我们如果想将内容推送到蓝牙设备中,在发现服务的时候onServicesDiscovered 遍历特性中,确保是用于写消息的特性对象后。选择持有该特性,然后通过:
String data ="0x12";
BluetoothGattCharacteristic writeCharact = bluetoothGattService.
getCharacteristic(UUID.fromString(WRITE_UUID));
//查找UUID是写的特性,并检测是否拥有写权限
if (writeCharact == null || writeCharact.getProperties() != BluetoothGattCharacteristic.PROPERTY_WRITE) {
return ;//该特性没有写的权限。所以无法传入
}
// 当数据传递到蓝牙之后
// 会回调BluetoothGattCallback里面的write方法
writeCharact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
// 将需要传递的数据转为16进制数
writeCharact.setValue(data);
bluetoothGatt.writeCharacteristic(writeCharact);
3.9 关闭连接
当蓝牙通讯结束,或者界面关闭时。我们需要关闭GATT服务,减少资源占用。
if (bluetoothGatt != null) {
bluetoothGatt.close();
bluetoothGatt.disconnect();
bluetoothGatt = null;
}
也可以关闭BluetoothGattCallback 的回调监听:
gattCallback.disConnectBlue();//关闭GATT服务回调监听
4. 小结
到这里蓝牙的链接和读取就结束了。
我们通过bluetoothAdapter 查找到蓝牙设备之后,再通过GATT服务进行蓝牙设备与手机之间的配对。直接比对UUID,而不再需要PIN码进行配对了。
(PS:有些安全性要求比较高的设备,还是会需要主动进行PIN码配对。PIN配队就只能通过系统设备界面中的蓝牙功能项进行操作了。)
通过GATT服务连接成功后。就可以查询该Server下的各种特性了,不同的特性对应了一个功能。有发消息的特性,也有用于收消息的特性。
同时一个蓝牙设备对象,可能有多种服务功能。
如果不想自己写线程变量轮询设备发送过来的消息,就通过注册消息监听。让BLE框架帮我们进行轮询之后,再通知到我们。