一文带你吃透Android BLE蓝牙开发全流程
一、BLE 蓝牙基础概念
蓝牙(Bluetooth)技术是一种无线数据和语音通信开放的全球规范,基于低成本的近距离无线连接,为固定设备和移动设备建立短距离的无线通信环境。该技术最初由爱立信公司开发,它的出现解决了传统电缆连接的不便和局限性,使得便携移动设备和计算机设备能够无需电缆就能连接到互联网 。从 1994 年问世以来,蓝牙技术不断更新迭代,从最初的 1.0 版本发展到如今的蓝牙 5.4 版本,功能越来越丰富和强大,广泛应用于智能网联汽车、语音传输、数据传输、智能家居、医疗保健等众多领域。
蓝牙技术主要分为两大类:经典蓝牙(Classical Bluetooth)和低功耗蓝牙(Bluetooth Low Energy,BLE)。经典蓝牙主要指蓝牙协议 4.0 之前的版本,它以较高的数据传输速率和支持多种类型的数据流而著称,适合需要持续连接和大量数据交换的应用场景,如蓝牙耳机、蓝牙鼠标、蓝牙键盘等数码产品的周边设备。而随着蓝牙 4.0 版本的发布,蓝牙技术引入了低功耗蓝牙这一重要分支,它的设计初衷是为了满足那些对电力消耗极为敏感的设备需求,比如可穿戴设备、健康监测设备等。
BLE 蓝牙,即低功耗蓝牙,具有诸多显著特点。首先,它最大的优势就是低功耗,通过减少广播频段、缩短射频开启时间、引入深度睡眠模式等策略,BLE 设备在传输数据时能耗极低,使得小型电池供电设备能够运行数月甚至数年 。其次,BLE 设备可以快速连接与断开,支持快速建立连接和断开连接的机制,适用于即时通信场景 。再者,其成本较低,BLE 芯片成本较低,适合大规模部署。另外,BLE 适用于小数据包传输,适用于少量数据的频繁传输。
基于这些特点,BLE 蓝牙在众多领域得到了广泛应用。在物联网(IoT)领域,它用于连接各种智能设备,如传感器、智能标签、信标等,实现数据采集、环境监测、资产跟踪等功能;在健康监测方面,应用于智能穿戴设备(如智能手环、智能手表)中,实现心率监测、步数统计、睡眠监测等功能;智能家居领域,BLE 蓝牙用于控制家中的灯光、窗帘、空调等电器设备,提高家居生活的舒适性和便捷性;运动跟踪场景下,它可以记录用户的运动数据(如步数、距离、速度、心率等),并传输到智能手机或其他终端设备上进行分析;在位置服务方面,通过 BLE 信标设备实现室内定位和导航功能,适用于大型商场、博物馆、机场等公共场所。
二、开发前的准备工作
(一)开发环境搭建
开发 Android 应用,首先需要搭建开发环境,主要包括安装 Android Studio 和配置开发环境。Android Studio 是官方推荐的集成开发环境(IDE) ,提供了丰富的工具和功能,方便我们进行代码编写、调试、构建等操作。安装 Android Studio 的步骤如下:
-
访问Android Studio 官网,下载适用于你操作系统的安装包。
-
运行安装包,按照安装向导的提示完成安装。安装过程中,你可以选择安装路径、是否创建桌面快捷方式等选项。
-
首次启动 Android Studio 时,它会自动检测并安装必要的 SDK 组件。你也可以在后续通过 SDK Manager 手动管理和更新 SDK。
-
在安装过程中,Android Studio 会自动安装 Java Development Kit(JDK)。若你需要自定义 JDK 或使用旧版本,可以前往Eclipse Adoptium下载并安装 JDK 17(推荐版本,兼容最新 Android Studio)。安装完成后,需记下 JDK 安装路径,并在系统环境变量中添加
JAVA_HOME,值为 JDK 安装路径,将%JAVA_HOME%\in加入Path变量中,验证 JDK 是否安装成功。
(二)权限申请
在使用 BLE 蓝牙功能之前,我们需要在 AndroidManifest.xml 文件中添加蓝牙相关权限声明。
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
其中,android.permission.BLUETOOTH权限用于请求连接、接受连接和传输数据,android.permission.BLUETOOTH_ADMIN权限则允许程序管理蓝牙设备,如打开、关闭蓝牙,扫描蓝牙设备等。
从安卓 6.0(API 23)开始,系统引入了运行时权限机制。对于危险权限,除了在 AndroidManifest.xml 中声明,还需要在运行时动态请求用户授权。对于 BLE 蓝牙开发,还需要申请模糊定位权限,因为在扫描 BLE 设备时,系统会将蓝牙扫描视为一种位置信息获取行为,即使只是用于发现附近的蓝牙设备,也需要获取定位权限,相关权限声明如下:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
在代码中动态请求权限的示例如下:
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_BLUETOOTH_PERMISSIONS = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 请求蓝牙和定位权限
requestBluetoothPermissions();
}
private void requestBluetoothPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED
|| ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// 如果未授予权限,请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_COARSE_LOCATION},
REQUEST_BLUETOOTH_PERMISSIONS);
} else {
// 如果权限已授予,继续蓝牙操作
enableBluetooth();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_BLUETOOTH_PERMISSIONS) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED
&& grantResults[2] == PackageManager.PERMISSION_GRANTED) {
// 权限被授予
enableBluetooth();
} else {
// 权限被拒绝,给出提示
// 这里可以添加相应的处理代码,比如提示用户权限被拒绝
}
}
}
private void enableBluetooth() {
// 启用蓝牙相关功能
}
}
(三)设备兼容性检查
在进行 BLE 蓝牙开发之前,还需要检查设备是否支持 BLE 蓝牙。并非所有的 Android 设备都支持 BLE 功能,因此在使用 BLE 相关 API 之前,我们需要进行兼容性检查,示例代码如下:
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
public class BluetoothUtils {
public static boolean isBLESupported(Context context) {
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
// 检查是否支持BLE
return bluetoothAdapter != null && bluetoothAdapter.isMultipleAdvertisementSupported();
}
}
上述代码通过BluetoothManager获取BluetoothAdapter,然后检查BluetoothAdapter是否为空以及设备是否支持多广告功能(这是判断设备是否支持 BLE 的一种方式)。如果bluetoothAdapter为空或者不支持多广告功能,则说明设备不支持 BLE 蓝牙。
此外,还需要确保设备的 Android 系统版本符合要求。BLE 蓝牙从 Android 4.3(API 18)开始支持,因此在开发时,需要将minSdkVersion设置为 18 或更高版本,以确保应用能够在支持 BLE 的设备上运行。同时,为了保证应用在不同版本的 Android 系统上都能正常工作,还需要进行充分的兼容性测试,可以使用 Android Studio 的模拟器或真实设备进行测试,确保应用在不同 Android 版本和设备上的表现符合预期。
三、Android BLE 蓝牙开发流程详解
(一)打开蓝牙
在搜索设备之前,需要确保手机的蓝牙已经打开。我们可以通过以下代码获取系统蓝牙适配器管理类,并询问打开手机蓝牙:
private BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// 询问打开蓝牙
if (mBluetoothAdapter != null &&!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, 1);
}
上述代码首先通过BluetoothAdapter.getDefaultAdapter()获取系统蓝牙适配器管理类mBluetoothAdapter。如果mBluetoothAdapter不为空且蓝牙未启用,则创建一个Intent,使用BluetoothAdapter.ACTION_REQUEST_ENABLE动作来请求用户打开蓝牙。通过startActivityForResult方法启动这个Intent,并传入请求码1,以便在回调中处理用户的操作结果。
接下来,我们需要在onActivityResult方法中处理申请打开蓝牙请求的回调:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
if (resultCode == RESULT_OK) {
Toast.makeText(this, "蓝牙已经开启", Toast.LENGTH_SHORT).show();
} else if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, "没有蓝牙权限", Toast.LENGTH_SHORT).show();
finish();
}
}
}
在onActivityResult方法中,首先判断requestCode是否为我们之前传入的1,以确定是打开蓝牙的请求回调。如果resultCode为RESULT_OK,表示用户同意打开蓝牙,弹出提示 “蓝牙已经开启”;如果resultCode为RESULT_CANCELED,表示用户拒绝打开蓝牙,弹出提示 “没有蓝牙权限”,并调用finish()方法结束当前活动。
(二)搜索设备
获取到蓝牙适配器后,就可以开始搜索 BLE 设备了。Android 提供了startLeScan方法来扫描 BLE 蓝牙设备,示例代码如下:
mBluetoothAdapter.startLeScan(callback);
private LeScanCallback callback = new LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int arg1, byte[] arg2) {
// device为扫描到的BLE设备
if (device.getName()!= null && device.getName().equals("目标设备名称")) {
// 获取目标设备
targetDevice = device;
}
}
};
上述代码中,mBluetoothAdapter.startLeScan(callback)方法开始扫描 BLE 设备,当扫描到设备时,会回调LeScanCallback接口的onLeScan方法。在onLeScan方法中,参数device即为扫描到的 BLE 设备,我们可以通过device.getName()获取设备名称,并与目标设备名称进行比较,如果相等,则找到了目标设备,将其赋值给targetDevice。
(三)连接设备
通过扫描 BLE 设备,根据设备名称区分出目标设备targetDevice后,下一步实现与目标设备的连接。在连接设备之前,要停止搜索蓝牙,停止搜索一般需要一定的时间来完成,最好调用停止搜索函数之后加以 100ms 的延时,保证系统能够完全停止搜索蓝牙设备,示例代码如下:
mBluetoothAdapter.stopLeScan(callback);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
停止搜索之后,启动连接过程。BLE 蓝牙的连接方法相对简单,只需调用connectGatt方法:
public BluetoothGatt connectGatt (Context context, boolean autoConnect, BluetoothGattCallback callback);
参数说明如下:
-
返回值
BluetoothGatt:BLE 蓝牙连接管理类,主要负责与设备进行通信。 -
boolean autoConnect:建议置为false,能够提升连接速度。如果设置为true,系统会尝试自动连接设备,可能会因为设备状态等原因导致连接过程耗时较长;设置为false时,应用程序可以更精确地控制连接时机和过程,有助于提升连接的成功率和速度。 -
BluetoothGattCallback callback:连接回调,这是一个重要参数,BLE 通信的核心部分,用于处理连接状态变化、服务发现、数据读写等事件的回调。
调用connectGatt方法连接目标设备的示例代码如下:
BluetoothGatt bluetoothGatt = targetDevice.connectGatt(this, false, gattCallback);
(四)设备通信
与设备建立连接之后,就可以进行设备通信了。整个通信过程都是在BluetoothGattCallback的异步回调函数中完成的。BluetoothGattCallback中主要回调函数如下:
private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}
};
这些回调函数的作用如下:
-
onConnectionStateChange:当连接状态发生变化时调用,例如连接成功、连接断开等。在这个回调函数中,我们可以根据newState的值判断当前的连接状态。当newState == BluetoothGatt.STATE_CONNECTED时,表示设备已成功连接,此时可以开始扫描服务,如mBluetoothGatt.discoverServices();;当newState == BluetoothGatt.STATE_DISCONNECTED时,表示连接断开,可以在这个回调中进行相应的处理,如重新连接等。 -
onCharacteristicWrite:当向设备的特征(BluetoothGattCharacteristic)写入数据成功时调用。在这个回调中,我们可以根据status判断写入操作是否成功,如果status == BluetoothGatt.GATT_SUCCESS,表示写入成功,可以进行下一步操作;如果写入失败,可以根据status的值判断失败原因,并进行相应的错误处理。 -
onDescriptorWrite:当向设备的描述符(BluetoothGattDescriptor)写入数据成功时调用,与onCharacteristicWrite类似,也是用于处理写入操作的结果。 -
onServicesDiscovered:当发现设备的服务(BluetoothGattService)时调用。在这个回调中,我们可以获取设备的服务列表,如List<BluetoothGattService> servicesList = mBluetoothGatt.getServices();,然后进一步获取服务中的特征,以便进行数据读写操作。 -
onCharacteristicChanged:当设备的特征值发生变化时调用。当设备主动向手机发送数据时,会触发这个回调函数,我们可以在这个回调中获取设备发送的数据,如BluetoothGattCharacteristic characteristic中包含了设备发送的数据,通过characteristic.getValue()方法可以获取具体的数据内容 。
四、实战案例:Android 与智能手环的 BLE 通信
(一)需求分析
本案例旨在实现 Android 手机与智能手环通过 BLE 蓝牙进行数据交互,具体需求包括读取智能手环的心率、步数等数据。智能手环作为 BLE 设备,会将心率、步数等数据通过特定的服务和特征值进行广播。Android 手机需要扫描并连接到手环设备,发现其提供的服务和特征,然后通过读取特征值来获取心率、步数等数据。
(二)代码实现
- 界面布局:在
res/layout目录下的activity_main.xml文件中定义界面布局,包含连接按钮、显示心率和步数的文本框等。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/connect_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="连接手环" />
<TextView
android:id="@+id/heart_rate_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="心率:"
android:textSize="20sp"
android:layout_marginTop="16dp"/>
<TextView
android:id="@+id/step_count_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="步数:"
android:textSize="20sp"
android:layout_marginTop="16dp"/>
</LinearLayout>
- 蓝牙操作逻辑:在
MainActivity.java中实现蓝牙操作逻辑,包括打开蓝牙、扫描设备、连接设备、发现服务和特征以及读取数据等功能。
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.List;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_ENABLE_BT = 1;
private static final int REQUEST_LOCATION_PERMISSION = 2;
private static final long SCAN_PERIOD = 10000; // 扫描时间10秒
private BluetoothAdapter mBluetoothAdapter;
private BluetoothLeScanner mBluetoothLeScanner;
private BluetoothGatt mBluetoothGatt;
private TextView mHeartRateText;
private TextView mStepCountText;
private Handler mHandler;
private boolean mScanning;
// 假设心率服务和特征UUID
private static final UUID HEART_RATE_SERVICE_UUID = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb");
private static final UUID HEART_RATE_CHARACTERISTIC_UUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb");
// 假设步数服务和特征UUID
private static final UUID STEP_COUNT_SERVICE_UUID = UUID.fromString("自定义步数服务UUID");
private static final UUID STEP_COUNT_CHARACTERISTIC_UUID = UUID.fromString("自定义步数特征UUID");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button connectButton = findViewById(R.id.connect_button);
mHeartRateText = findViewById(R.id.heart_rate_text);
mStepCountText = findViewById(R.id.step_count_text);
mHandler = new Handler();
// 获取蓝牙适配器
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
connectButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!mBluetoothAdapter.isEnabled()) {
// 打开蓝牙
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
} else {
// 检查定位权限
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
REQUEST_LOCATION_PERMISSION);
} else {
startScan();
}
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Bundle data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
Toast.makeText(this, "蓝牙已开启", Toast.LENGTH_SHORT).show();
// 检查定位权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
REQUEST_LOCATION_PERMISSION);
} else {
startScan();
}
} else {
Toast.makeText(this, "蓝牙未开启", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startScan();
} else {
Toast.makeText(this, "定位权限未授予,无法扫描设备", Toast.LENGTH_SHORT).show();
}
}
}
private void startScan() {
mScanning = true;
mBluetoothLeScanner.startScan(scanCallback);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothLeScanner.stopScan(scanCallback);
}
}, SCAN_PERIOD);
}
private ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
BluetoothDevice device = result.getDevice();
// 假设手环设备名称为"SmartBand"
if ("SmartBand".equals(device.getName())) {
mScanning = false;
mBluetoothLeScanner.stopScan(scanCallback);
connectDevice(device);
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Toast.makeText(MainActivity.this, "扫描失败:" + errorCode, Toast.LENGTH_SHORT).show();
}
};
private void connectDevice(BluetoothDevice device) {
mBluetoothGatt = device.connectGatt(this, false, gattCallback);
}
private BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Toast.makeText(MainActivity.this, "已连接到手环", Toast.LENGTH_SHORT).show();
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Toast.makeText(MainActivity.this, "已断开与手环的连接", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
List<BluetoothGattService> services = gatt.getServices();
for (BluetoothGattService service : services) {
UUID serviceUuid = service.getUuid();
if (HEART_RATE_SERVICE_UUID.equals(serviceUuid)) {
BluetoothGattCharacteristic heartRateCharacteristic = service.getCharacteristic(HEART_RATE_CHARACTERISTIC_UUID);
if (heartRateCharacteristic!= null) {
gatt.readCharacteristic(heartRateCharacteristic);
}
} else if (STEP_COUNT_SERVICE_UUID.equals(serviceUuid)) {
BluetoothGattCharacteristic stepCountCharacteristic = service.getCharacteristic(STEP_COUNT_CHARACTERISTIC_UUID);
if (stepCountCharacteristic!= null) {
gatt.readCharacteristic(stepCountCharacteristic);
}
}
}
} else {
Toast.makeText(MainActivity.this, "发现服务失败:" + status, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
UUID characteristicUuid = characteristic.getUuid();
if (HEART_RATE_CHARACTERISTIC_UUID.equals(characteristicUuid)) {
int heartRate = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
mHeartRateText.setText("心率:" + heartRate);
} else if (STEP_COUNT_CHARACTERISTIC_UUID.equals(characteristicUuid)) {
int stepCount = characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0);
mStepCountText.setText("步数:" + stepCount);
}
}
}
};
@Override
protected void onDestroy() {
super.onDestroy();
if (mBluetoothGatt!= null) {
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
}
}
}
- 数据解析:在
onCharacteristicRead回调方法中,根据不同的特征 UUID 解析数据。例如,心率数据通常以FORMAT_UINT8格式存储,步数数据可能以FORMAT_UINT32格式存储,按照相应的格式解析出数据并显示在界面上。
(三)运行与调试
-
运行程序:将 Android 设备连接到电脑,确保设备已开启开发者选项和 USB 调试模式。在 Android Studio 中,点击运行按钮,选择连接的设备,即可将应用安装并运行在设备上。或者使用 Android 模拟器,在 Android Studio 中创建一个支持 BLE 的模拟器,然后运行应用。
-
调试过程:
-
权限问题:如果在运行时遇到权限相关的问题,如无法扫描设备,可能是蓝牙权限或定位权限未正确授予。确保在 AndroidManifest.xml 中声明了必要的权限,并且在运行时动态请求了定位权限。
-
设备连接问题:如果无法连接到手环设备,首先检查手环是否处于可连接状态,周围是否有其他干扰。其次,查看日志信息,在
gattCallback的onConnectionStateChange回调中打印status和newState,以确定连接失败的原因。可能的原因包括设备不支持 BLE、设备忙、连接超时等。 -
数据读取问题:如果无法正确读取心率或步数数据,检查特征 UUID 是否正确,数据解析格式是否与手环发送的数据格式一致。可以在
onCharacteristicRead回调中打印接收到的数据,以便分析问题。
-
五、常见问题及解决方法
在 Android BLE 蓝牙开发过程中,开发者可能会遇到各种问题,下面将列举一些常见问题及对应的解决方法。
(一)扫描不到设备
-
原因分析:
-
蓝牙未打开:用户可能未开启手机的蓝牙,导致无法进行扫描操作。
-
权限问题:在 Android 6.0 及以上版本,需要在运行时请求蓝牙和定位相关权限,若未正确授权,可能无法扫描到设备。此外,7.0 以上手机很多需要手动打开 GPS,因为扫描 BLE 设备时系统会将其视为位置信息获取行为 。
-
设备兼容性:并非所有 Android 设备都支持 BLE,需要检查设备是否支持 BLE 蓝牙,以及设备的蓝牙堆栈是否是最新的。
-
设备范围:BLE 设备的信号范围有限,若设备距离过远或信号被遮挡,可能无法被扫描到。
-
设备状态:BLE 设备可能未处于广播状态,只有处于广播状态的设备才能被扫描到。
-
扫描设置问题:扫描参数设置不当,如扫描模式、扫描周期等,可能影响扫描效果。例如,在 Android 7.0 以上,Google 为防止 BLE 扫描滥用做了限制,不要在 30s 内对蓝牙扫描重复开启 - 关闭超过 5 次;Android 8.0 以上退到后台息屏后,若不设置 ScanFilters,默认扫不到设备。
-
-
解决方法:
-
检查蓝牙状态:在扫描前,通过
BluetoothAdapter.isEnabled()方法检查蓝牙是否已打开,若未打开,提示用户打开蓝牙,如前文 “打开蓝牙” 部分代码所示。 -
请求权限:在 AndroidManifest.xml 中声明必要的权限,并在运行时动态请求权限,参考 “权限申请” 部分代码。对于 Android 7.0 以上手机,确保 GPS 已打开;对于 Android 8.0 以上手机退到后台扫描的情况,设置合适的
ScanSettings和ScanFilter,如设置扫描模式为低功耗,并添加服务 UUID 过滤。 -
设备兼容性检查:使用
BluetoothManager和BluetoothAdapter检查设备是否支持 BLE,如 “设备兼容性检查” 部分代码。 -
调整设备位置:确保 BLE 设备在有效信号范围内,且无遮挡。
-
确认设备广播状态:与硬件设备供应商沟通,确认设备是否处于广播状态。
-
优化扫描设置:合理设置扫描周期,避免在短时间内频繁开启和关闭扫描;对于 Android 8.0 以上退到后台扫描的情况,按照上述方法设置扫描参数。
-
(二)连接不稳定
-
原因分析:
-
BLE 协议特性:BLE 通常适用于短小数据的高效传输,传输大量数据时,其低带宽和高延迟特性可能导致连接断开 。
-
连接参数问题:连接参数(如 ConnectionInterval、SlaveLatency、SupervisionTimeout)设置不当,可能影响连接稳定性。这些参数一起决定了 BLE 的功耗,一般硬件设备会在 APP 连接成功时主动更新这些参数以保证不同手机的差异性得到一致,但 APP 端无法直接控制。
-
信号干扰:周围存在其他蓝牙设备、Wi-Fi 设备或其他无线信号源,可能对 BLE 连接产生干扰。
-
设备硬件问题:BLE 设备或手机的蓝牙硬件存在故障或兼容性问题,例如部分华为手机可能出现连接不稳定、连接慢且易断开的情况 。
-
软件问题:代码中在连接过程或数据传输过程中处理不当,如在连接成功后主线程中进行过多操作(尤其是频繁绘制操作),可能影响
BluetoothGatt.discoverServices()的执行,进而影响连接稳定性。
-
-
解决方法:
-
优化数据传输:根据 BLE 协议特性,尽量减少单次传输的数据量,将大数据拆分成多个小数据包进行传输;合理设置 MTU(最大传输单元),如
bluetoothGatt.requestMtu(256);,以优化数据流。 -
与硬件沟通优化:与硬件设备供应商沟通,调整设备的连接参数,确保参数设置适合当前应用场景。
-
减少信号干扰:尽量避免在信号干扰较强的环境中使用 BLE 设备;或者在代码中增加连接重试机制,当连接断开时自动尝试重新连接。
-
硬件兼容性测试:在不同品牌和型号的设备上进行兼容性测试,对于出现问题的设备,与硬件厂商协商解决,或者在应用中针对特定设备进行适配优化。
-
优化代码逻辑:在连接成功后的
BluetoothGatt.discoverServices()过程中,避免在主线程中进行过多操作,确保连接过程的可靠性。
-
(三)数据传输异常
-
原因分析:
-
数据格式不匹配:手机端和 BLE 设备端对数据的解析格式不一致,导致数据传输后无法正确解析。
-
特征读写失败:可能由于权限问题、设备未连接或连接不稳定等原因,导致对特征(
BluetoothGattCharacteristic)的读写操作失败。 -
数据分包问题:当传输的数据量较大时,需要进行分包处理,若分包和组包逻辑有误,可能导致数据丢失或错误。
-
缓冲区溢出:接收数据的缓冲区大小设置不合理,当接收到的数据量超过缓冲区大小时,可能导致数据丢失。
-
-
解决方法:
-
统一数据格式:与硬件设备供应商沟通,确定统一的数据格式,并在手机端和设备端按照相同的格式进行数据解析和封装。
-
检查读写权限和连接状态:在进行特征读写操作前,检查设备是否已连接,以及是否具有读写权限;在读写操作的回调函数中,根据
status判断操作是否成功,若失败,根据错误码进行相应处理。 -
完善分包组包逻辑:实现正确的分包和组包算法,确保数据在传输过程中的完整性。例如,可以在数据包中添加序号、校验和等信息,以便在接收端进行数据校验和重组。
-
合理设置缓冲区大小:根据实际传输的数据量,合理设置接收数据的缓冲区大小,避免缓冲区溢出。可以动态调整缓冲区大小,根据接收到的数据量实时扩展或收缩缓冲区 。
-