一文带你吃透Android BLE蓝牙开发全流程

一文带你吃透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 的步骤如下:

  1. 访问Android Studio 官网,下载适用于你操作系统的安装包。

  2. 运行安装包,按照安装向导的提示完成安装。安装过程中,你可以选择安装路径、是否创建桌面快捷方式等选项。

  3. 首次启动 Android Studio 时,它会自动检测并安装必要的 SDK 组件。你也可以在后续通过 SDK Manager 手动管理和更新 SDK。

  4. 在安装过程中,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 文件中添加蓝牙相关权限声明。

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 设备时,系统会将蓝牙扫描视为一种位置信息获取行为,即使只是用于发现附近的蓝牙设备,也需要获取定位权限,相关权限声明如下:

xml 复制代码
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

在代码中动态请求权限的示例如下:

java 复制代码
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 之前,我们需要进行兼容性检查,示例代码如下:

java 复制代码
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 蓝牙开发流程详解

(一)打开蓝牙

在搜索设备之前,需要确保手机的蓝牙已经打开。我们可以通过以下代码获取系统蓝牙适配器管理类,并询问打开手机蓝牙:

java 复制代码
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方法中处理申请打开蓝牙请求的回调:

java 复制代码
@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,以确定是打开蓝牙的请求回调。如果resultCodeRESULT_OK,表示用户同意打开蓝牙,弹出提示 "蓝牙已经开启";如果resultCodeRESULT_CANCELED,表示用户拒绝打开蓝牙,弹出提示 "没有蓝牙权限",并调用finish()方法结束当前活动。

(二)搜索设备

获取到蓝牙适配器后,就可以开始搜索 BLE 设备了。Android 提供了startLeScan方法来扫描 BLE 蓝牙设备,示例代码如下:

java 复制代码
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 的延时,保证系统能够完全停止搜索蓝牙设备,示例代码如下:

java 复制代码
mBluetoothAdapter.stopLeScan(callback);
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}

停止搜索之后,启动连接过程。BLE 蓝牙的连接方法相对简单,只需调用connectGatt方法:

java 复制代码
public BluetoothGatt connectGatt (Context context, boolean autoConnect, BluetoothGattCallback callback);

参数说明如下:

  • 返回值BluetoothGatt:BLE 蓝牙连接管理类,主要负责与设备进行通信。

  • boolean autoConnect:建议置为false,能够提升连接速度。如果设置为true,系统会尝试自动连接设备,可能会因为设备状态等原因导致连接过程耗时较长;设置为false时,应用程序可以更精确地控制连接时机和过程,有助于提升连接的成功率和速度。

  • BluetoothGattCallback callback:连接回调,这是一个重要参数,BLE 通信的核心部分,用于处理连接状态变化、服务发现、数据读写等事件的回调。

调用connectGatt方法连接目标设备的示例代码如下:

java 复制代码
BluetoothGatt bluetoothGatt = targetDevice.connectGatt(this, false, gattCallback);

(四)设备通信

与设备建立连接之后,就可以进行设备通信了。整个通信过程都是在BluetoothGattCallback的异步回调函数中完成的。BluetoothGattCallback中主要回调函数如下:

java 复制代码
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 手机需要扫描并连接到手环设备,发现其提供的服务和特征,然后通过读取特征值来获取心率、步数等数据。

(二)代码实现

  1. 界面布局 :在res/layout目录下的activity_main.xml文件中定义界面布局,包含连接按钮、显示心率和步数的文本框等。
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>
  1. 蓝牙操作逻辑 :在MainActivity.java中实现蓝牙操作逻辑,包括打开蓝牙、扫描设备、连接设备、发现服务和特征以及读取数据等功能。
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();
        }
    }
}
  1. 数据解析 :在onCharacteristicRead回调方法中,根据不同的特征 UUID 解析数据。例如,心率数据通常以FORMAT_UINT8格式存储,步数数据可能以FORMAT_UINT32格式存储,按照相应的格式解析出数据并显示在界面上。

(三)运行与调试

  1. 运行程序:将 Android 设备连接到电脑,确保设备已开启开发者选项和 USB 调试模式。在 Android Studio 中,点击运行按钮,选择连接的设备,即可将应用安装并运行在设备上。或者使用 Android 模拟器,在 Android Studio 中创建一个支持 BLE 的模拟器,然后运行应用。

  2. 调试过程

    • 权限问题:如果在运行时遇到权限相关的问题,如无法扫描设备,可能是蓝牙权限或定位权限未正确授予。确保在 AndroidManifest.xml 中声明了必要的权限,并且在运行时动态请求了定位权限。

    • 设备连接问题 :如果无法连接到手环设备,首先检查手环是否处于可连接状态,周围是否有其他干扰。其次,查看日志信息,在gattCallbackonConnectionStateChange回调中打印statusnewState,以确定连接失败的原因。可能的原因包括设备不支持 BLE、设备忙、连接超时等。

    • 数据读取问题 :如果无法正确读取心率或步数数据,检查特征 UUID 是否正确,数据解析格式是否与手环发送的数据格式一致。可以在onCharacteristicRead回调中打印接收到的数据,以便分析问题。

五、常见问题及解决方法

在 Android BLE 蓝牙开发过程中,开发者可能会遇到各种问题,下面将列举一些常见问题及对应的解决方法。

(一)扫描不到设备

  1. 原因分析

    • 蓝牙未打开:用户可能未开启手机的蓝牙,导致无法进行扫描操作。

    • 权限问题:在 Android 6.0 及以上版本,需要在运行时请求蓝牙和定位相关权限,若未正确授权,可能无法扫描到设备。此外,7.0 以上手机很多需要手动打开 GPS,因为扫描 BLE 设备时系统会将其视为位置信息获取行为 。

    • 设备兼容性:并非所有 Android 设备都支持 BLE,需要检查设备是否支持 BLE 蓝牙,以及设备的蓝牙堆栈是否是最新的。

    • 设备范围:BLE 设备的信号范围有限,若设备距离过远或信号被遮挡,可能无法被扫描到。

    • 设备状态:BLE 设备可能未处于广播状态,只有处于广播状态的设备才能被扫描到。

    • 扫描设置问题:扫描参数设置不当,如扫描模式、扫描周期等,可能影响扫描效果。例如,在 Android 7.0 以上,Google 为防止 BLE 扫描滥用做了限制,不要在 30s 内对蓝牙扫描重复开启 - 关闭超过 5 次;Android 8.0 以上退到后台息屏后,若不设置 ScanFilters,默认扫不到设备。

  2. 解决方法

    • 检查蓝牙状态:在扫描前,通过BluetoothAdapter.isEnabled()方法检查蓝牙是否已打开,若未打开,提示用户打开蓝牙,如前文 "打开蓝牙" 部分代码所示。

    • 请求权限:在 AndroidManifest.xml 中声明必要的权限,并在运行时动态请求权限,参考 "权限申请" 部分代码。对于 Android 7.0 以上手机,确保 GPS 已打开;对于 Android 8.0 以上手机退到后台扫描的情况,设置合适的ScanSettingsScanFilter,如设置扫描模式为低功耗,并添加服务 UUID 过滤。

    • 设备兼容性检查:使用BluetoothManagerBluetoothAdapter检查设备是否支持 BLE,如 "设备兼容性检查" 部分代码。

    • 调整设备位置:确保 BLE 设备在有效信号范围内,且无遮挡。

    • 确认设备广播状态:与硬件设备供应商沟通,确认设备是否处于广播状态。

    • 优化扫描设置:合理设置扫描周期,避免在短时间内频繁开启和关闭扫描;对于 Android 8.0 以上退到后台扫描的情况,按照上述方法设置扫描参数。

(二)连接不稳定

  1. 原因分析

    • BLE 协议特性:BLE 通常适用于短小数据的高效传输,传输大量数据时,其低带宽和高延迟特性可能导致连接断开 。

    • 连接参数问题:连接参数(如 ConnectionInterval、SlaveLatency、SupervisionTimeout)设置不当,可能影响连接稳定性。这些参数一起决定了 BLE 的功耗,一般硬件设备会在 APP 连接成功时主动更新这些参数以保证不同手机的差异性得到一致,但 APP 端无法直接控制。

    • 信号干扰:周围存在其他蓝牙设备、Wi-Fi 设备或其他无线信号源,可能对 BLE 连接产生干扰。

    • 设备硬件问题:BLE 设备或手机的蓝牙硬件存在故障或兼容性问题,例如部分华为手机可能出现连接不稳定、连接慢且易断开的情况 。

    • 软件问题:代码中在连接过程或数据传输过程中处理不当,如在连接成功后主线程中进行过多操作(尤其是频繁绘制操作),可能影响BluetoothGatt.discoverServices()的执行,进而影响连接稳定性。

  2. 解决方法

    • 优化数据传输:根据 BLE 协议特性,尽量减少单次传输的数据量,将大数据拆分成多个小数据包进行传输;合理设置 MTU(最大传输单元),如bluetoothGatt.requestMtu(256);,以优化数据流。

    • 与硬件沟通优化:与硬件设备供应商沟通,调整设备的连接参数,确保参数设置适合当前应用场景。

    • 减少信号干扰:尽量避免在信号干扰较强的环境中使用 BLE 设备;或者在代码中增加连接重试机制,当连接断开时自动尝试重新连接。

    • 硬件兼容性测试:在不同品牌和型号的设备上进行兼容性测试,对于出现问题的设备,与硬件厂商协商解决,或者在应用中针对特定设备进行适配优化。

    • 优化代码逻辑:在连接成功后的BluetoothGatt.discoverServices()过程中,避免在主线程中进行过多操作,确保连接过程的可靠性。

(三)数据传输异常

  1. 原因分析

    • 数据格式不匹配:手机端和 BLE 设备端对数据的解析格式不一致,导致数据传输后无法正确解析。

    • 特征读写失败:可能由于权限问题、设备未连接或连接不稳定等原因,导致对特征(BluetoothGattCharacteristic)的读写操作失败。

    • 数据分包问题:当传输的数据量较大时,需要进行分包处理,若分包和组包逻辑有误,可能导致数据丢失或错误。

    • 缓冲区溢出:接收数据的缓冲区大小设置不合理,当接收到的数据量超过缓冲区大小时,可能导致数据丢失。

  2. 解决方法

    • 统一数据格式:与硬件设备供应商沟通,确定统一的数据格式,并在手机端和设备端按照相同的格式进行数据解析和封装。

    • 检查读写权限和连接状态:在进行特征读写操作前,检查设备是否已连接,以及是否具有读写权限;在读写操作的回调函数中,根据status判断操作是否成功,若失败,根据错误码进行相应处理。

    • 完善分包组包逻辑:实现正确的分包和组包算法,确保数据在传输过程中的完整性。例如,可以在数据包中添加序号、校验和等信息,以便在接收端进行数据校验和重组。

    • 合理设置缓冲区大小:根据实际传输的数据量,合理设置接收数据的缓冲区大小,避免缓冲区溢出。可以动态调整缓冲区大小,根据接收到的数据量实时扩展或收缩缓冲区 。

相关推荐
小码哥_常1 小时前
从“新老交锋”看Retrofit与Ktor
前端
小J听不清2 小时前
CSS 外边距(margin)全解析:取值规则 + 实战用法
前端·javascript·css·html·css3
还是大剑师兰特2 小时前
Stats.js 插件详解及示例(完全攻略)
前端·大剑师·stats
前端小超超2 小时前
Vue计算属性computed:可写与只读的区别
前端·javascript·vue.js
IT_陈寒3 小时前
SpringBoot实战:3个隐藏技巧让你的应用性能飙升50%
前端·人工智能·后端
weixin199701080163 小时前
唯品会商品详情页前端性能优化实战
前端·性能优化
爱学习的程序媛3 小时前
【Web前端】Pinia状态管理详解
前端·vue.js·typescript
爱学习的程序媛3 小时前
“数字孪生”详解与前端技术栈
前端·人工智能·计算机视觉·智慧城市·信息与通信
海石3 小时前
微信小程序开发02:原始人也能看懂的着色器与视频处理
前端·微信小程序·视频编码