2. 扫描并发现蓝牙设备

任务说明

本案例实现一个基于 BlueZ 的 BLE 扫描器,通过 D-Bus 接口调用 BlueZ 的扫描功能,发现周围的 BLE 广播设备,并输出每个设备的 MAC 地址与 RSSI 值。代码使用 Python 3 与 dbus-next 库,不依赖额外蓝牙硬件抽象,直接使用系统 BlueZ 协议栈。

实现方案

扫描器通过系统 D-Bus 与 BlueZ 通信。核心流程为:获取 BlueZ 的适配器对象(hci0),注册一个 D-Bus 对象接收设备发现信号,然后启动扫描。当收到设备信号时,从信号参数中提取设备路径,再通过该路径的属性接口读取 Address 和 RSSI 属性。扫描持续 10 秒后自动停止,并打印发现的设备列表。

Python 扫描器 (dbus-next 客户端) BlueZ 守护进程 (org.bluez) BLE 广播设备 (周边环境) D-Bus 调用 射频广播 InterfacesAdded 信号 (设备路径) 属性读取 Address / RSSI 发现事件 AA:BB:CC:DD:EE:FF RSSI: -45 dBm 11:22:33:44:55:66 RSSI: -62 dBm

完整代码

#!/usr/bin/env python3
"""BLE 扫描器:发现周边设备并打印 MAC 地址与 RSSI。"""
import asyncio
from dbus_next.aio import MessageBus
from dbus_next import BusType, Variant

BLUEZ_SERVICE = 'org.bluez'
ADAPTER_PATH = '/org/bluez/hci0'
DEVICE_IFACE = 'org.bluez.Device1'
ADAPTER_IFACE = 'org.bluez.Adapter1'
PROPS_IFACE = 'org.freedesktop.DBus.Properties'
OM_IFACE = 'org.freedesktop.DBus.ObjectManager'

class BLEScanner:
    def __init__(self):
        self.bus = None
        self.devices = {}   # path -> (mac, rssi)

    async def start(self):
        self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
        # 监听新设备出现的信号
        self.bus.add_message_handler(self._on_signal)
        # 启动扫描
        adapter = self.bus.get_proxy_object(BLUEZ_SERVICE, ADAPTER_PATH)
        props = adapter.get_interface(PROPS_IFACE)
        await props.call_set(ADAPTER_IFACE, 'DiscoveryFilter', Variant('s', 'LE'))
        adapter_iface = adapter.get_interface(ADAPTER_IFACE)
        await adapter_iface.call_start_discovery()
        print('[*] 扫描中,持续 10 秒...')
        await asyncio.sleep(10)
        await adapter_iface.call_stop_discovery()
        self._print_results()

    def _on_signal(self, msg):
        if msg.interface == OM_IFACE and msg.member == 'InterfacesAdded':
            path = msg.body[0]
            ifaces = msg.body[1]
            if DEVICE_IFACE in ifaces:
                self._read_device(path)

    def _read_device(self, path):
        try:
            dev = self.bus.get_proxy_object(BLUEZ_SERVICE, path)
            props = dev.get_interface(PROPS_IFACE)
            mac = props.get(DEVICE_IFACE, 'Address').value
            rssi_v = props.get(DEVICE_IFACE, 'RSSI')
            rssi = rssi_v.value if rssi_v else None
            self.devices[path] = (mac, rssi)
        except Exception:
            pass    # 设备可能已消失,忽略

    def _print_results(self):
        print(f'\n[+] 共发现 {len(self.devices)} 个 BLE 设备:')
        for mac, rssi in self.devices.values():
            rssi_str = f'{rssi} dBm' if rssi is not None else 'N/A'
            print(f'    MAC: {mac}  RSSI: {rssi_str}')

async def main():
    scanner = BLEScanner()
    await scanner.start()

if __name__ == '__main__':
    asyncio.run(main())

依赖与运行

需要 Linux 系统且 BlueZ 版本不低于 5.43,并确保蓝牙适配器已启用(bluetoothctl power on)。安装 Python 依赖:

pip install dbus-next

运行扫描器:

sudo python3 ble_scanner.py

验证方法

程序启动后输出“扫描中,持续 10 秒...”,等待 10 秒后自动停止并打印设备列表。判定标准:

  • 若周围存在开启广播的 BLE 设备(如手机、蓝牙耳机、信标),输出中应包含其 MAC 地址,RSSI 为负值(如 -40 到 -90 之间),数值越大表示信号越强。
  • 可用手机安装 nRF Connect 应用并开启广播,对比扫描器输出的 MAC 是否与手机显示的 MAC 一致。
  • 若输出为空,请确认适配器已开启且周围确实有 BLE 广播设备。

预期输出示例(实际值取决于环境):

[*] 扫描中,持续 10 秒...

[+] 共发现 2 个 BLE 设备:
    MAC: C4:63:1B:5A:2F:11  RSSI: -58 dBm
    MAC: F0:08:D1:7E:3C:9A  RSSI: -71 dBm