在蓝牙低功耗(BLE)开发中,主动扫描(Active Scanning)是一个常见且重要的机制。当Android设备作为扫描端时,其蓝牙芯片默认会在扫描到广播包后向周围所有传感器发送SCAN_REQ(扫描请求),以获取更完整的扫描响应数据。但近期有开发者提出一个刁钻却现实的需求:能否将SCAN_REQ仅发送给某一个特定的BLE传感器? 换言之,如何实现“定向扫描请求”。

这个问题听起来简单,但深入底层就会发现,Android的公开API并未提供这类控制,甚至直接违反了BLE协议栈的默认行为。本文将从技术原理、API限制与可能的替代方案三方面进行拆解。

一、SCAN_REQ的工作原理

在BLE协议中,扫描分为被动扫描和主动扫描。被动扫描仅监听广播包,不主动响应;主动扫描则在收到广播包后发送SCAN_REQ,请求传感器返回更详尽的扫描响应数据。默认情况下,Android的BluetoothLeScanner.startScan()方法在主动扫描模式下,会向所有可被发现的传感器广播SCAN_REQ。这是因为BCP(蓝牙核心规范)要求扫描器以广播方式发送SCAN_REQ,而非单播——这意味着传感器接收到SCAN_REQ后,会响应自己的完整数据,而不管它是否是目标设备。

这个设计是为了简化协议,避免复杂的一对一握手。但若开发者只希望从特定传感器获取扩展数据,而忽略其他设备,就会陷入困境:即便你设置了ScanFilter,它也只能过滤结果回调,无法阻止蓝牙芯片向非目标设备发送SCAN_REQ

二、Android API的现实壁垒

在Android BLE框架中,所有底层扫描控制都封闭在BluetoothLeScanner类中。开发者可设置的参数仅包括扫描模式、报告延迟以及ScanFilter列表。但ScanFilter本质上是一个后处理过滤器:蓝牙芯片仍会捕捉所有广播包并发送SCAN_REQ,只是系统在上层过滤掉不需要的结果。这导致两个后果:

  1. 能耗浪费:向非目标设备发送SCAN_REQ会触发不必要的扫描响应,增加功耗。
  2. 隐私/干扰隐患:在某些场景(如精准定位或考场防作弊)下,广播SCAN_REQ可能会干扰其他传感器或被恶意利用。

更关键的是,Android并未开放任何方法让开发者指定“只向某个特定MAC地址的传感器发送SCAN_REQ”。原因很底层:蓝牙芯片的固件通常将SCAN_REQ与扫描窗口强绑定,无法单独控制其目标地址。即便尝试通过BluetoothGatt建立连接后协商,但连接过程本身也需要首先通过扫描发现设备,形成死循环。

三、底层可行性探讨:理论可行但代价巨大

从蓝牙核心规范层面看,SCAN_REQ本身是一个数据包,其头部包含发起者的地址和有效载荷。理论上,如果能直接操作HCI(Host Controller Interface)命令,就有可能构造一个定向SCAN_REQ,将目标地址设为特定传感器的MAC。但HCI命令在Android系统中属于系统级权限,普通应用无法触碰。即便通过Root或越狱获取权限,也需要自行实现整个蓝牙协议栈的修改,并且适配不同芯片厂商的固件,工程难度极高。

目前已知的变通思路包括:

  • 利用连接后的数据请求:先通过被动扫描发现设备,然后直接发起连接(不需要SCAN_REQ),通过BluetoothGattreadRemoteRssi()requestConnectionPriority()获取部分信息。但这种方式无法获取扫描响应数据,且连接本身成本更高。
  • 使用Android 10+的扫描报告回调:低版本系统中,ScanCallback可返回ScanResult.isConnectable()等标志,但依旧无法干预SCAN_REQ的发送。
  • 定制固件:在某些特定硬件(如BlueNRG、nRF52)上,可重写蓝牙栈的扫描层,但这就脱离了Android通用平台范畴。

四、结论:目前无解,但可曲线救国

综合来看,在未Root的Android设备上,通过公开API完全不可能实现定向SCAN_REQ发送。这个限制植根于BLE协议的设计初衷和Android对蓝牙控制权限的分层隔离。如果确实需要减少扫描干扰或降低功耗,更务实的方案是:

  1. 采用被动扫描仅获取广播包,放弃扫描响应数据。
  2. 使用连接后的单点通信替代扫描请求。
  3. 在系统层或硬件层定制,但仅适用于原型机或嵌入式场景。

对于大部分开发者而言,承认这个“不可能”或许比寻找黑科技更高效。不过,随着Android BLE框架的持续演进(如未来可能引入定向扫描特性),这个问题或许有朝一日能够解决。在此之前,认清技术边界、优化扫描策略才是正道。