本教程详细阐述了在 Android BLE AdvertisingSet 中正确配置和发送扫描响应(Scan Response)数据的方法。核心在于确保 AdvertisingSetParameters 中设置 setScannable(true),以允许设备响应扫描请求并发送包含额外信息的扫描响应包。文章将通过代码示例和注意事项,指导开发者有效利用 AdvertisingSet 传输完整的 BLE 广告数据。
1. 理解 Android BLE 广告与扫描响应
低功耗蓝牙(ble)通信始于广告(advertising)过程。一个 ble 设备(advertiser)通过广播广告包(advertisement packet)来宣布其存在和提供的信息。这些广告包通常包含设备名称、服务 uuid 等基本信息。然而,广告包的大小是有限的(通常为 31 字节)。
为了传输更多数据,BLE 引入了扫描响应包(Scan Response Packet)的概念。当一个扫描器(Scanner)执行“主动扫描”(Active Scan)时,它会发送一个扫描请求(Scan Request)给正在广播的设备。如果该设备被配置为可响应扫描请求,它将发送一个扫描响应包,其中可以包含额外的设备信息,如制造商特定数据、更长的设备名称或服务数据等。
从 Android 8.0 (API 26) 开始,Google 引入了 AdvertisingSet API,提供了更灵活和强大的 BLE 广告能力,允许开发者同时管理多个广告集,并支持 LE Coded PHY 和 LE 2M PHY 等新特性。
2. 核心问题:扫描响应数据未发送的原因
在 AdvertisingSet 中,即使开发者通过 startAdvertisingSet 方法明确提供了 AdvertiseData 作为扫描响应包,扫描器端仍可能无法接收到这些数据。常见的问题表现为,扫描器接收到的 ScanResult 中,ScanRecord 对象的 mServiceData 或其他本应包含在扫描响应中的字段为空。
导致这一问题的根本原因在于,AdvertisingSetParameters 中缺少一个关键的配置:setScannable(true)。默认情况下,如果未明确设置,设备可能不会响应扫描请求,从而导致扫描响应数据无法发送。setScannable(true) 明确告诉 BLE 协议栈,该广告集是可被扫描的,并且应该在收到扫描请求时发送扫描响应。
3. 正确配置 AdvertisingSet 发送扫描响应
要确保 AdvertisingSet 能够成功发送扫描响应数据,需要正确配置 AdvertisingSetParameters、主广告数据和扫描响应数据,并启动广告集。
3.1 配置 AdvertisingSetParameters
AdvertisingSetParameters 定义了广告集的行为,包括是否可连接、是否使用传统模式、广告间隔和发射功率等。要发送扫描响应,最关键的一步是调用 setScannable(true)。
import android.bluetooth.le.AdvertisingSetParameters; // ... (假设 serviceId 已经定义为 ParcelUuid) AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder() .setConnectable(false) // 根据需求设置是否可连接。此处设置为不可连接。 .setLegacyMode(true) // 是否使用传统广告模式(ADV_IND, ADV_SCAN_IND等)。 // 即使在传统模式下,setScannable(true) 也是发送扫描响应的关键。 .setScannable(true) // 关键:允许设备响应扫描请求并发送扫描响应。 .setInterval(AdvertisingSetParameters.INTERVAL_HIGH) // 广告间隔,例如高频间隔。 .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM) // 发射功率。 .build();
3.2 构建主广告数据 AdvertiseData
主广告数据是设备广播的基本信息,扫描器即使不发送扫描请求也能接收到。
import android.bluetooth.le.AdvertiseData; import android.os.ParcelUuid; // ... (假设 serviceId 已经定义为 ParcelUuid) AdvertiseData advertiseData = new AdvertiseData.Builder() .setIncludeDeviceName(true) // 是否在主广告包中包含设备名称。 .setIncludeTxPowerLevel(false) // 是否在主广告包中包含发射功率。 .addServiceUuid(serviceId) // 添加服务 UUID。 .build();
3.3 构建扫描响应数据 AdvertiseData
扫描响应数据包含在主广告包中无法容纳的额外信息。这些数据只会在扫描器发送扫描请求后才被发送。
import java.nio.charset.StandardCharsets; // ... (假设 serviceId 已经定义为 ParcelUuid) byte[] packetContent = "hello".getBytes(StandardCharsets.UTF_8); // 示例:要发送的服务数据内容。 AdvertiseData scanResponseData = new AdvertiseData.Builder() .setIncludeDeviceName(false) // 扫描响应中通常不需要再次包含设备名称,除非有特定需求。 .setIncludeTxPowerLevel(false) // 扫描响应中通常不需要再次包含发射功率。 .addServiceUuid(serviceId) // 再次添加服务 UUID,或添加其他服务 UUID。 .addServiceData(serviceId, packetContent) // 添加服务数据,这是本例中期望传输的关键信息。 .build();
3.4 启动广告集 startAdvertisingSet
最后,使用 BluetoothLeAdvertiser 实例启动广告集,传入之前构建的参数和数据。
import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeAdvertiser; import android.bluetooth.le.AdvertisingSetCallback; import android.bluetooth.le.AdvertisingSet; import android.util.Log; // ... (假设 parameters, advertiseData, scanResponseData 已经定义) // ... (假设 serviceId 已经定义为 ParcelUuid) BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser(); if (advertiser == null) { Log.e("BLEAdvertiser", "BluetoothLeAdvertiser not available."); return; } AdvertisingSetCallback advertisingSetCallback = new AdvertisingSetCallback() { @Override public void onAdvertisingSetStarted(AdvertisingSet advertisingSet, int txPower, int status) { Log.d("BLEAdvertiser", "AdvertisingSet started, status: " + status); // 可以在此处保存 advertisingSet 实例以便后续停止广告 } @Override public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) { Log.d("BLEAdvertiser", "Advertising data set, status: " + status); } @Override public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) { Log.d("BLEAdvertiser", "Scan response data set, status: " + status); } @Override public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) { Log.d("BLEAdvertiser", "AdvertisingSet stopped."); } @Override public void onAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enable, int status) { Log.d("BLEAdvertiser", "Advertising enabled: " + enable + ", status: " + status); } // ... 其他回调方法,如 onAdvertisingSetStopped, onAdvertisingEnabled 等 }; // 启动广告集 advertiser.startAdvertisingSet(parameters, advertiseData, scanResponseData, null, null, advertisingSetCallback);
4. 验证:通过 BLE 扫描器接收扫描响应
为了验证扫描响应是否成功发送,需要配置一个 BLE 扫描器,并确保其执行的是主动扫描。
4.1 配置 ScanFilter 和 ScanSettings
扫描器需要设置 ScanSettings 以指定扫描模式。SCAN_MODE_LOW_LATENCY 是一个主动扫描模式,它会发送扫描请求并等待扫描响应。
import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.bluetooth.le.ScanRecord; import android.os.ParcelUuid; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Map; // ... (假设 serviceId 已经定义为 ParcelUuid) List<ScanFilter> filters = new ArrayList<>(); ScanFilter filter = new ScanFilter.Builder() .setServiceUuid(serviceId) // 根据服务 UUID 过滤设备。 .build(); filters.add(filter); // 设置扫描模式为低延迟,这通常意味着主动扫描。 ScanSettings scanSettings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 确保是主动扫描,以便接收扫描响应。 .build();
4.2 处理 ScanResult
在 ScanCallback 的 onScanResult 方法中,可以通过 ScanResult.getScanRecord() 获取 ScanRecord 对象,然后从中提取 ServiceData 来验证扫描响应是否成功接收。
// ... (假设 filters, scanSettings 已经定义) // ... (假设 serviceId 已经定义为 ParcelUuid) BluetoothLeScanner bluetoothLeScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); if (bluetoothLeScanner == null) { Log.e("BLEScanner", "BluetoothLeScanner not available."); return; } ScanCallback scanCallback = new ScanCallback() { private static final String TAG = "BLEScanner"; @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); ScanRecord scanRecord = result.getScanRecord(); if (scanRecord != null) { // 打印完整的 ScanRecord 信息以便调试 Log.d(TAG, "ScanResult: " + result.toString()); // 检查 ServiceData 是否包含期望的内容 Map<ParcelUuid, byte[]> serviceData = scanRecord.getServiceData(); if (serviceData != null && serviceData.containsKey(serviceId)) { byte[] receivedData = serviceData.get(serviceId); String dataString = new String(receivedData, StandardCharsets.UTF_8); Log.d(TAG, "Received ServiceData for UUID " + serviceId.toString() + ": " + dataString); // 验证收到的数据是否为 "hello" if ("hello".equals(dataString)) { Log.d(TAG, "Scan response data received successfully!"); } } else { Log.d(TAG, "ServiceData for UUID " + serviceId.toString() + " not found in scan response."); } } } @Override public void onBatchScanResults(List<ScanResult> results) { super.onBatchScanResults(results); // 处理批量扫描结果 } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); Log.e(TAG, "Scan failed with error code: " + errorCode); } }; // 启动扫描 bluetoothLeScanner.startScan(filters, scanSettings, scanCallback); // 在适当的时候停止扫描 // bluetoothLeScanner.stopScan(scanCallback);
5. 注意事项与最佳实践
- 主动扫描与被动扫描:再次强调,扫描响应只会在扫描器执行主动扫描时发送。被动扫描(如 SCAN_MODE_LOW_POWER 或 SCAN_MODE_BALANCED 的某些配置)只会接收主广告包。
- legacyMode 的影响:即使在 AdvertisingSetParameters 中设置 setLegacyMode(true),表示使用传统广告格式,setScannable(true) 仍然是确保扫描响应发送的关键。AdvertisingSet API 提供了对传统广告模式的封装和更精细的控制。
- 数据大小限制:无论是主广告包还是扫描响应包,其有效载荷大小都受到 BLE 规范的限制,通常为 31 字节。如果需要传输更多数据,可能需要考虑 GATT 服务或分包传输。
- 权限:确保 AndroidManifest.xml 中声明了必要的蓝牙权限:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 对于扫描,尤其是在Android 6.0+ -->
对于 Android 12 (API 31) 及更高版本,需要 BLUETOOTH_ADVERTISE, BLUETOOTH_SCAN, BLUETOOTH_CONNECT。对于 Android 11 (API 30) 及更低版本,通常需要 BLUETOOTH, BLUETOOTH_ADMIN 和 ACCESS_FINE_LOCATION。运行时权限请求也是必不可少的。
- 错误处理与回调:在 AdvertisingSetCallback 中,务必实现 onAdvertisingSetStarted、onAdvertisingSetStopped 等回调方法,以便监控广告集的状态和处理可能发生的错误(如 status 参数非 ADVERTISE_SUCCESS)。
总结
通过本教程,我们深入探讨了 Android BLE AdvertisingSet 中扫描响应数据未发送的问题,并提供了详细的解决方案。核心要点在于,当使用 AdvertisingSet 并期望发送扫描响应数据时,必须在 AdvertisingSetParameters.Builder 中明确调用 setScannable(true)。同时,扫描器端也需要执行主动扫描才能请求并接收这些响应数据。遵循这些指导原则,开发者可以有效利用 AdvertisingSet 的强大功能,实现更丰富和灵活的 BLE 广告通信。
评论(已关闭)
评论已关闭