Bootstrap

Android BLE 的扫描配对、连接流程梳理

目录

一、前提条件

二、扫描BLE设备

2.1.BLE扫描的模式

2.1.1. 主动扫描(Active Scanning)

2.1.2. 被动扫描(Passive Scanning)

2.1.3. 注意事项

2.2. 扫描流程的主要步骤

2.2.1. 获取蓝牙适配器

2.2.2. 开启蓝牙

2.2.3. 处理用户响应并开始扫描

2.2.4. 处理扫描结果

2.2.5. 停止扫描

三、配对BLE设备(可选)

3.1. 配对过程

3.1.1. 发起配对请求

3.1.2. 监听配对状态

3.1.3. 处理配对结果

3.1.4. 更新UI或执行后续操作

3.1.5. 注销BroadcastReceiver

3.2. 代码示例

3.3. 注意事项 

四、连接BLE设备

4.1. 连接到GATT Server

4.2. 处理连接回调

4.3. 发现服务

4.4. 读写数据

五、断开连接

5.1. 调用disconnect()方法

5.2. 设置标志以跟踪连接状态

5.3. 释放BluetoothGatt对象

5.4. 清理其他资源

5.5. 注意事项


Android BLE(Bluetooth Low Energy,低功耗蓝牙)连接流程涉及多个步骤,主要包括扫描、配对、连接三个阶段。以下是详细的流程梳理:

一、前提条件

1.1. 设备支持:确保Android设备支持BLE功能。从Android 4.3(API 级别 18)开始,Android系统内置了对BLE的支持。

1.2. 权限申请:在AndroidManifest.xml中申请必要的权限,包括BLUETOOTH、BLUETOOTH_ADMIN、BLUETOOTH_SCAN和BLUETOOTH_CONNECT等。

1.3. 动态权限请求:对于Android 6.0(API 级别 23)及以上版本,还需要在运行时请求位置权限(ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION),因为从Android 6.0开始,蓝牙扫描需要位置权限。

二、扫描BLE设备

在BLE建立连接之前,需要进行设备之间的扫描。BLE扫描主要分为两种模式:主动扫描(Active Scanning)和被动扫描(Passive Scanning)。这两种扫描模式在功耗、扫描范围和响应方式上存在显著差异。

2.1.BLE扫描的模式

2.1.1. 主动扫描(Active Scanning)

特点

  • 主动扫描模式下,扫描设备会主动发送扫描请求(Scan Request)给周围的BLE广播设备。
  • 收到扫描请求的BLE设备会响应一个扫描响应(Scan Response),其中可能包含设备的名称、服务UUID等额外信息。
  • 由于需要发送和接收数据,主动扫描的功耗相对较高,但能够获取更详细的设备信息。

应用场景

  • 适用于需要快速发现并连接BLE设备的场景,如蓝牙音箱、蓝牙耳机等外设的配对连接。
  • 在需要获取设备额外信息的情况下,主动扫描是更合适的选择。

2.1.2. 被动扫描(Passive Scanning)

特点

  • 被动扫描模式下,扫描设备仅监听周围的BLE广播信号,不发送任何扫描请求。
  • 因此,它只能接收到BLE设备广播的广告包(Advertising Packet),这些数据包中通常包含设备的MAC地址(但注意在BLE 5.0及以后版本中,出于隐私考虑,设备地址可能被随机化)、设备名称的一部分等信息。
  • 被动扫描的功耗较低,因为不需要发送数据,但可能无法获取到设备的全部信息。

应用场景

  • 适用于低功耗要求的场景,如物联网设备(IoT)的监控和发现。
  • 当仅需要快速检测周围BLE设备的存在,而不需要详细信息时,被动扫描是更优的选择。

2.1.3. 注意事项

  • 在Android系统中,特别是在Android 6.0及以上版本,由于系统对后台应用进行了更严格的限制,BLE扫描在后台运行时可能会受到更多限制。
  • 在熄屏状态下,Android系统可能会将BLE扫描限制为低功耗模式,即只能进行被动扫描。
  • 实际开发中需要根据应用的实际需求和用户场景,选择合适的扫描模式。

2.2. 扫描流程的主要步骤

2.2.1. 获取蓝牙适配器

通过BluetoothManager获取BluetoothAdapter对象。使用BluetoothAdapterisEnabled()方法来检查蓝牙是否已经开启。

BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();  
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {  
    // 蓝牙未开启  
}

2.2.2. 开启蓝牙

如果蓝牙未开启,可以通过启动一个Intent来请求用户启用蓝牙。这通常会导致系统显示一个对话框,询问用户是否允许启用蓝牙。

if (!bluetoothAdapter.isEnabled()) {  
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);  
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);  
}

其中,REQUEST_ENABLE_BT是一个自定义的请求码,用于在onActivityResult()方法中识别这个特定的请求。这样,当用户响应蓝牙启用请求后,应用就可以知道用户是否允许了蓝牙的启用。

2.2.3. 处理用户响应并开始扫描

重写onActivityResult()方法来处理用户对蓝牙启用请求的响应。

@Override  
protected void onActivityResult(int requestCode, int resultCode, Intent data) {  
    if (requestCode == REQUEST_ENABLE_BT) {  
        if (resultCode == Activity.RESULT_OK) {  
            // 蓝牙已启用,继续执行BLE操作  
        } else {  
            // 用户拒绝了蓝牙的启用,处理这种情况  
            Toast.makeText(this, "蓝牙未开启,无法继续操作", Toast.LENGTH_SHORT).show();  
        }  
    }  
    super.onActivityResult(requestCode, resultCode, data);  
}

如果用户允许了蓝牙的启用,可以继续执行BLE扫描和连接操作。使用BluetoothAdapter的getBluetoothLeScanner()方法获取BluetoothLeScanner对象,并调用其startScan()方法开始扫描BLE设备。可以设置扫描参数,如扫描模式(SCAN_MODE_LOW_LATENCY、SCAN_MODE_BALANCED、SCAN_MODE_LOW_POWER)和报告延迟等。

BluetoothLeScanner 类是用于执行BLE设备扫描的核心类,该类中的startScan方法允许以更灵活和强大的方式开始BLE设备的扫描。可以指定扫描过滤器(ScanFilter)、扫描设置(ScanSettings)以及扫描回调(ScanCallback),以便在发现设备时接收通知。

  以下是startScan方法的一些重载版本及其简要说明:

  • 带有扫描回调的startScan(List<ScanFilter> filters, ScanSettings settings, ScanCallback callback):这是最常用的版本,它允许指定一个或多个扫描过滤器(用于匹配特定的设备广告数据),扫描设置(如扫描模式、报告延迟等),以及一个扫描回调(用于接收扫描结果)。 示例用法:

List<ScanFilter> filters = new ArrayList<>();  
// 可以根据需要添加ScanFilter到列表中  

ScanSettings settings = new ScanSettings.Builder()  
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 设置扫描模式  
    .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 设置回调类型  
    .build();  

ScanCallback scanCallback = new ScanCallback() {  
    @Override  
    public void onScanResult(int callbackType, ScanResult result) {  
        super.onScanResult(callbackType, result);  
        // 处理扫描结果  
    }  

    @Override  
    public void onBatchScanResults(List<ScanResult> results) {  
        super.onBatchScanResults(results);  
        // 处理批量扫描结果(如果启用了批量扫描)  
    }  

    @Override  
    public void onScanFailed(int errorCode) {  
        super.onScanFailed(errorCode);  
        // 处理扫描失败的情况  
    }  
};  

bluetoothLeScanner.startScan(filters, settings, scanCallback);
  • 带有PendingIntent的startScan(List<ScanFilter> filters, ScanSettings settings, PendingIntent callbackIntent)(不常用):

这个版本的startScan方法通过PendingIntent来接收扫描结果,这种方法允许应用在不直接运行的情况下(例如,当应用处于后台或被系统挂起时),通过广播接收器(Broadcast Receiver)或其他方式接收扫描到的BLE设备信息。然而,在大多数情况下,直接使用ScanCallback会更简单、更直接。

下面是一个基本的示例:首先,需要创建一个 PendingIntent,它指向一个能够接收扫描结果的广播接收器(Broadcast Receiver):

Intent intent = new Intent(context, YourBroadcastReceiver.class);  
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

然后,使用 startScan 方法,并传入扫描过滤器、扫描设置和上面创建的 PendingIntent

List<ScanFilter> filters = new ArrayList<>();  
// 如果需要,可以向filters列表中添加ScanFilter  
  
ScanSettings settings = new ScanSettings.Builder()  
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)  
    .build();  
  
bluetoothLeScanner.startScan(filters, settings, pendingIntent);

 在广播接收器中,需要处理接收到的扫描结果。但是,需要注意的是,BLE扫描的 PendingIntent 并不直接传递扫描结果作为Intent的额外数据(extras)。相反,当扫描到匹配的设备时,系统会发送一个带有特定动作(action)的广播,而广播接收器需要监听这个action,并可能需要通过其他方式(如查询最新的扫描结果)来获取具体的设备信息。

2.2.4. 处理扫描结果

通过BluetoothLeScanner.ScanCallback的onScanResult(int callbackType, ScanResult result)回调中,可以处理扫描到的BLE设备信息。这个回调方法会在每次扫描到新的BLE设备或已扫描到的设备更新了其广播数据时被调用。以下是如何在onScanResult回调中处理BLE设备信息的基本步骤:

ScanCallback scanCallback =
new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        if (callbackType != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
            // Should not happen.
            Log.e(TAG, "LE Scan has already started");
            return;
        }
        ScanRecord scanRecord = result.getScanRecord();
        if (scanRecord == null) {
            return;
        }
        if (serviceUuids != null) {
            List<ParcelUuid> uuids = new ArrayList<ParcelUuid>();
            for (UUID uuid : serviceUuids) {
                uuids.add(new ParcelUuid(uuid));
            }
            List<ParcelUuid> scanServiceUuids = scanRecord.getServiceUuids();
            if (scanServiceUuids == null
                    || !scanServiceUuids.containsAll(uuids)) {
                if (DBG) {
                    Log.d(TAG, "uuids does not match");
                }
                return;
            }
        }
        callback.onLeScan(
                result.getDevice(), result.getRssi(), scanRecord.getBytes());
    }
};

2.2.5. 停止扫描

在适当的时候(比如找到了目标设备或者用户请求停止扫描时),调用BluetoothLeScannerstopScan()方法来停止扫描。这有助于减少不必要的功耗,并避免在不再需要时继续接收扫描结果。

// 停止扫描  
scanner.stopScan(scanCallback);

注意:在上面的stopScan()调用中,scanCallback是传递给startScan()方法的同一个ScanCallback实例。然而,从Android API 21(Lollipop)开始,stopScan()方法实际上并不要传递任何参数。如果没有引用到特定的ScanCallback实例,或者只是想停止所有扫描(无论是由哪个ScanCallback触发的),可以简单地调用无参数的stopScan()方法。

// 更简单的停止扫描方式(不需要传递ScanCallback)  
scanner.stopScan();

实际开发中结合具体需求和Android API版本选择适当的方法。

三、配对BLE设备(可选)

BLE设备通常不需要像经典蓝牙那样进行配对,因为它们通过密钥交换来确保通信安全。但是,某些BLE设备可能要求配对或绑定操作,可以通过调用BluetoothDevice对象的createBond()方法来发起配对请求。这个过程对于需要更高安全性的通信场景尤为重要,因为配对过程中会生成并存储密钥,这些密钥在未来的连接中用于身份验证和加密通信,从而保护数据的机密性和完整性。

3.1. 配对过程

3.1.1. 发起配对请求

通过调用BluetoothDevice的createBond()方法,可以向远程蓝牙设备发起配对请求。这个方法会返回一个布尔值,但在Android中,由于配对是异步进行的,这个返回值通常不会被用来判断配对是否成功。相反,为了确定配对是否成功,需要注册一个BroadcastReceiver来监听BluetoothDevice.ACTION_BOND_STATE_CHANGED这一系统广播。当蓝牙设备的配对状态发生变化时(如从未配对变为正在配对,或从正在配对变为已配对或配对失败),系统会发送这个广播。

BroadcastReceiveronReceive()方法中,通过检查Intent中的BluetoothDevice.EXTRA_DEVICEBluetoothDevice.EXTRA_BOND_STATE来获取发生状态变化的蓝牙设备和其新的配对状态。配对状态可以是以下几种之一:

  • BluetoothDevice.BOND_NONE:表示设备未配对。
  • BluetoothDevice.BOND_BONDING:表示设备正在配对过程中。
  • BluetoothDevice.BOND_BONDED:表示设备已经成功配对。

通过检查这些状态,你可以在UI中相应地更新状态,或者在配对成功后执行其他操作(如建立BLE连接)。

此外,值得注意的是,在某些情况下(如用户取消配对请求或设备不支持配对),配对可能会失败。在这些情况下,你也应该在UI中向用户显示适当的错误消息。

最后,别忘了在不再需要监听配对状态变化时注销BroadcastReceiver,以避免内存泄漏或其他潜在问题。

3.1.2. 监听配对状态

为了获知配对的状态,需要注册一个BroadcastReceiver来监听BluetoothDevice.EXTRA_BOND_STATEBluetoothDevice.EXTRA_DEVICE这两个Intent extra来获取配对状态和相关的BluetoothDevice对象。

3.1.3. 处理配对结果

  • 如果配对成功(即状态变为BOND_BONDED),就可以安全地与该设备进行通信了。Android系统会自动保存配对信息,并在后续的连接尝试中自动使用这些信息进行身份验证。
  • 如果配对失败(例如,用户拒绝了配对请求或设备不支持配对),应该在UI中向用户显示适当的错误消息,并可能提供重试配对的选项。

3.1.4. 更新UI或执行后续操作

根据配对状态,可以在UI中显示相应的消息,或者执行其他必要的操作(如尝试建立BLE连接)。

3.1.5. 注销BroadcastReceiver

当不再需要监听配对状态变化时(例如,当用户离开包含蓝牙功能的Activity时),应该注销BroadcastReceiver以避免内存泄漏。

3.2. 代码示例

下面是一个简化的代码示例,展示了如何调用createBond()方法并注册一个BroadcastReceiver来监听配对状态变化:

// 假设已经有了BluetoothDevice对象 bluetoothDevice  
  
// 调用createBond()方法(注意:这个方法不返回配对结果)  
boolean result = bluetoothDevice.createBond();  
// 注意:这里的result只是表示请求是否已经被系统接收,并不表示配对是否成功  
  
// 注册BroadcastReceiver来监听配对状态变化  
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);  
registerReceiver(mBondStateChangeReceiver, filter);  
  
// BroadcastReceiver的实现  
private final BroadcastReceiver mBondStateChangeReceiver = new BroadcastReceiver() {  
    @Override  
    public void onReceive(Context context, Intent intent) {  
        final String action = intent.getAction();  
        if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {  
            final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);  
            if (device == bluetoothDevice) {  
                final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);  
                // 处理配对状态变化...  
                switch (state) {  
                    case BluetoothDevice.BOND_NONE:  
                        // 未配对  
                        break;  
                    case BluetoothDevice.BOND_BONDING:  
                        // 正在配对  
                        break;  
                    case BluetoothDevice.BOND_BONDED:  
                        // 已配对  
                        break;  
                }  
            }  
        }  
    }  
};  
  
// 不要忘记在适当的时候注销BroadcastReceiver  
unregisterReceiver(mBondStateChangeReceiver);

3.3. 注意事项 

  • 并非所有BLE设备都支持配对。一些设备可能只使用基于密钥的加密(如AES加密)来保护通信,而不需要传统的配对过程。
  • 配对过程可能需要用户交互,特别是在Android 6.0(API级别23)及更高版本上,由于隐私和安全性的考虑,系统可能会限制对蓝牙扫描和连接行为的访问。
  • 配对信息(包括密钥)是敏感信息,应该妥善保管,并在不再需要时及时删除。然而,在Android中,一旦配对成功,通常不需要手动删除配对信息,因为系统会自动管理这些信息。但是,用户可以在系统设置中手动取消配对。
  • 在实际进行BLE开发时,查阅最新的Android文档和蓝牙规范,以了解最新的安全特性和最佳实践。

四、连接BLE设备

连接到BLE设备的GATT Server并与之交互的过程,通常包括连接设备、发现服务、读写数据等步骤。以下是一个简化的流程说明,以及如何在Android应用中使用这些步骤的示例代码。

4.1. 连接到GATT Server

首先,需要有一个BluetoothDevice对象,通常是通过扫描BLE设备并选择一个来获得的。然后,可以使用connectGatt()方法来尝试连接到该设备的GATT Server。

BluetoothGatt bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback);

onnectGatt方法参数和返回值的说明:

  • Context context:是一个上下文对象,用于应用级别的操作,如启动服务、发送广播或加载资源。在大多数情况下,可以使用当前的Activity或Application的上下文。

  • boolean autoConnect:这个参数指定是否应该自动连接到BLE设备。如果设置为true,则当BLE设备变为可连接状态时,系统会自动尝试连接。如果设置为false,则系统将立即尝试连接,如果BLE设备当前不可连接,则连接将失败。

  • BluetoothGattCallback callback:是一个回调接口,用于接收来自BLE设备的异步事件。这个接口包含多个回调方法,如onConnectionStateChange()(当连接状态发生变化时调用)、onServicesDiscovered()(当发现BLE设备的服务时调用)、onCharacteristicRead()(当读取特性值时调用)等。需要在实现这个接口的类中定义这些方法,以处理BLE通信中的不同事件。

  • 请注意,BLE通信是异步的,因此不能在调用connectGatt()之后立即执行特征读写等操作,而应该在onConnectionStateChange()回调中等待连接成功后再执行这些操作。同样地,在onServicesDiscovered()回调中,应该遍历发现的服务和特征,以便找到想要交互的特定特征。 

4.2. 处理连接回调

在BluetoothGattCallback的onConnectionStateChange()方法中处理连接状态的改变。当连接状态变为BluetoothGatt.STATE_CONNECTED时,表示连接成功。

private final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {  
    @Override  
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {  
        if (newState == BluetoothProfile.STATE_CONNECTED) {  
            // 连接成功,开始发现服务  
            gatt.discoverServices();  
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {  
            // 连接丢失,处理断开连接的情况  
            // ...  
        }  
    }  
  
    // 其他回调方法,如onServicesDiscovered(), onCharacteristicRead()等  
};

4.3. 发现服务

连接成功后,调用BluetoothGatt的discoverServices()方法来发现设备提供的所有服务。服务发现的结果将在onServicesDiscovered()回调中返回。

@Override  
public void onServicesDiscovered(BluetoothGatt gatt, int status) {  
    if (status == BluetoothGatt.GATT_SUCCESS) {  
        // 服务发现成功,可以开始读取或写入数据  
        // 遍历gatt.getServices()获取服务列表  
        // ...  
    } else {  
        // 服务发现失败  
        // ...  
    }  
}

4.4. 读写数据

通过服务中的特性(Characteristic)来读写数据。可以使用BluetoothGatt的readCharacteristic()或writeCharacteristic()方法来进行数据的读写操作。

// 假设你已经找到了一个BluetoothGattCharacteristic对象名为characteristic  
  
// 读取数据  
bluetoothGatt.readCharacteristic(characteristic);  
  
// 写入数据  
characteristic.setValue(data); // data是一个byte[],包含你想要写入的数据  
bluetoothGatt.writeCharacteristic(characteristic);  
  
// 注意:写操作可能需要在`onCharacteristicWrite()`回调中确认是否成功

五、断开连接

5.1. 调用disconnect()方法

在不再需要BLE连接时,应调用BluetoothGatt的disconnect()方法来断开连接。

if (bluetoothGatt != null) {  
    bluetoothGatt.disconnect();  
}

5.2. 设置标志以跟踪连接状态

同样地,由于断开连接是异步的,可能需要设置一个标志来跟踪连接状态,并在onConnectionStateChange()回调中处理断开连接后的逻辑。

private boolean isConnected = false;  
  
// ...  
  
@Override  
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {  
    if (newState == BluetoothProfile.STATE_CONNECTED) {  
        isConnected = true;  
        // 连接成功,开始发现服务  
        gatt.discoverServices();  
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {  
        isConnected = false;  
        // 连接已断开,清理资源  
        if (gatt != null) {  
            gatt.close();  
            bluetoothGatt = null; // 避免内存泄漏  
        }  
        // 处理断开连接后的逻辑  
        // ...  
    }  
}

5.3. 释放BluetoothGatt对象

一旦连接状态变为STATE_DISCONNECTED,并且确定不再需要BluetoothGatt对象,就应该调用close()方法来释放它。这有助于避免资源泄漏,特别是如果应用可能会频繁地连接和断开BLE设备。

在上面的onConnectionStateChange()回调示例中,当连接状态变为STATE_DISCONNECTED时,我们调用了gatt.close()并将bluetoothGatt设置为null,以确保没有引用指向该对象,从而允许垃圾回收器回收它。

5.4. 清理其他资源

除了BluetoothGatt对象外,如果应用还使用了其他与BLE相关的资源(如线程、定时器、监听器等),也应该在断开连接后适当地清理它们。

5.5. 注意事项

  • 确保在调用disconnect()之前,BluetoothGatt对象不是null
  • 调用disconnect()后,不要立即调用close(),因为disconnect()是异步的,并且close()应该在连接状态确实变为STATE_DISCONNECTED后调用。
  • 如果应用可能会频繁地连接和断开BLE设备,请确保你的逻辑能够处理这种情况,以避免资源耗尽或性能问题。
  • 始终在UI线程之外执行BLE操作,以避免阻塞UI。如果你需要在UI中显示结果,请使用HandlerrunOnUiThread()等方法将结果传回UI线程。

以上即为Android BLE蓝牙的扫描、配对与连接流程的大致梳理。需要注意的是,不同设备和不同应用场景下,具体的实现细节可能会有所不同。

;