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

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

List 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 results) {  
        super.onBatchScanResults(results);  
        // 处理批量扫描结果(如果启用了批量扫描)  
    }  

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

bluetoothLeScanner.startScan(filters, settings, scanCallback);
  • 带有PendingIntent的startScan(List 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 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 uuids = new ArrayList();
            for (UUID uuid : serviceUuids) {
                uuids.add(new ParcelUuid(uuid));
            }
            List 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蓝牙的扫描、配对与连接流程的大致梳理。需要注意的是,不同设备和不同应用场景下,具体的实现细节可能会有所不同。

你可能感兴趣的:(嵌入式智慧开发探索,#,T2:蓝牙技术探索与应用,android)