Linux 蓝牙自动扫描:bluetoothctl 排错

Linux 蓝牙自动扫描的正式实现需要通过系统总线调用 org.bluez.Adapter1.StartDiscovery,并在调用前订阅 BlueZ 的对象和属性变化,不能只等待一个“设备发现”回调。现代 Linux 的蓝牙控制面由 BlueZ 通过 D-Bus 暴露,适配器、方法和属性分别对应对象路径、接口调用与信号监听,直接拼接底层 HCI 命令不适合作为应用接口。

最短工作顺序是:枚举 Object Manager 管理的适配器 → 订阅 InterfacesAdded、InterfacesRemoved 和 PropertiesChanged → 合并 GetManagedObjects 快照 → 按需设置过滤器 → 调用 StartDiscovery → 超时或退出时调用 StopDiscovery。如果调用返回成功却收不到设备,先用同一用户、同一适配器顺序执行 bluetoothctl scan on 做对照:CLI 能扫到而程序扫不到,优先查事件订阅、过滤条件和去重;CLI 也扫不到,再查 BlueZ 服务、控制器、协议类型和远端设备状态。

路径 适合验证什么 局限 建议定位
BlueZ D-Bus 应用真实调用链、事件订阅、过滤器和设备属性 调试信息较抽象,需要处理异步事件 生产实现的首选
bluetoothctl 同一主机上的 BlueZ、适配器和基本发现链路 不能复现程序自己的订阅、解析和去重逻辑 作为同机对照
hciconfig、hcitool 等旧式 HCI 工具 控制器是否响应、传统扫描能否启动 输出难解析,工具可能未安装,不适合作为稳定应用接口 只作辅助检查

扫描链路与 D-Bus 对象管理机制

先把服务、总线和适配器对象确认清楚,再讨论设备发现。以下命令没有预设输出,具体状态以本机实际结果为准:

systemctl status bluetooth
uname -r
bluetoothctl --version
journalctl -u bluetooth -b --no-pager
busctl --system list org.bluez
busctl --system tree org.bluez

如果 org.bluez 尚未出现,或者服务存在但对象树里没有 hci0、hci1 等适配器路径,通常说明 BlueZ 还没有完成适配器注册。此时应继续检查当前启动周期的 BlueZ 日志,而不是直接在程序里假定对象一定是 /org/bluez/hci0。

/org/bluez/hci0 只是常见示例。换机器、多 USB 适配器或驱动注册顺序变化后,编号可能不同。手工检查时可以枚举 Object Manager:

busctl --system call org.bluez / org.freedesktop.DBus.ObjectManager GetManagedObjects

正式程序应通过 GetManagedObjects 找到包含 org.bluez.Adapter1 接口的对象,而不是硬编码适配器路径。busctl 的输出适合人工检查,不建议作为生产程序的解析接口。

确认路径后,可以检查接口,并在另一个终端监听信号:

busctl --system introspect org.bluez /org/bluez/hci0
busctl --system monitor org.bluez

busctl monitor 阻塞运行时,在另一个终端调用:

busctl --system call org.bluez /org/bluez/hci0 org.bluez.Adapter1 StartDiscovery
busctl --system get-property org.bluez /org/bluez/hci0 org.bluez.Adapter1 Discovering
busctl --system call org.bluez /org/bluez/hci0 org.bluez.Adapter1 StopDiscovery

自动扫描需要依次处理以下阶段,单次调用 StartDiscovery 不足以覆盖整个过程。

阶段 BlueZ/D-Bus 操作 用途
查找适配器 获取 Object Manager 管理的对象 找到实际适配器及其接口
监听变化 订阅对象新增、移除和属性变化 接收发现设备及 RSSI 等属性更新
建立基线 获取当前托管对象快照 合并扫描前已存在的设备对象
配置扫描 按需设置发现过滤器 控制 BR/EDR、BLE 和重复数据等条件
发起扫描 调用 StartDiscovery 启动发现过程
结束扫描 调用 StopDiscovery 在超时、退出或重启前释放扫描状态

org.bluez.Adapter1 提供扫描控制接口,发现后的设备信息则通过 org.bluez.Device1 属性获取。下面的接口级伪代码只表达实现思路,不是适用于所有发行版和客户端库的安装脚本:

# 接口级伪代码,不依赖具体 D-Bus 客户端库

async def scan_once(duration_s, discovery_filter=None):
    manager = get_proxy("org.bluez", "/", "org.freedesktop.DBus.ObjectManager")

    # 必须先订阅,再取快照并启动扫描。
    subscribe_bus(
        sender="org.bluez",
        interface="org.freedesktop.DBus.ObjectManager",
        member="InterfacesAdded",
        callback=on_object_added,
    )
    subscribe_bus(
        sender="org.bluez",
        interface="org.freedesktop.DBus.ObjectManager",
        member="InterfacesRemoved",
        callback=on_object_removed,
    )
    subscribe_bus(
        sender="org.bluez",
        interface="org.freedesktop.DBus.Properties",
        member="PropertiesChanged",
        callback=on_property_changed,
    )

    snapshot = call(manager, "GetManagedObjects")
    adapter = select_adapter_with_interface(snapshot, "org.bluez.Adapter1")
    if adapter is None:
        raise RuntimeError("No BlueZ adapter object")

    started_by_us = False

    try:
        # 同一适配器的检查和启动应放在进程锁内,
        # 避免多个守护进程同时通过检查后重复启动。
        if get_property(adapter, "Discovering"):
            return

        if discovery_filter is not None:
            call(adapter, "SetDiscoveryFilter", discovery_filter)

        call(adapter, "StartDiscovery")
        started_by_us = True

        await_sleep(duration_s)
    finally:
        # 只停止本进程启动的扫描,不接管其他程序已经开启的会话。
        if started_by_us:
            call(adapter, "StopDiscovery")

快照并不等于“当前射频范围内可见的设备”,它可能包含 BlueZ 已经知道的配对设备或缓存对象。真正的扫描结果应由实时信号与快照合并得到,去重优先使用设备对象路径,也可以使用 Address 与 AddressType 组合。名称可能晚于地址出现,也可能为空,因此不适合作为主键;RSSI 更新通常属于属性变化,也不应被当作一台新设备。

过滤器要在启动本轮扫描前设置,并在本机接口支持时优先查看该版本暴露的选项:

busctl --system call org.bluez /org/bluez/hci0 org.bluez.Adapter1 GetDiscoveryFilters

例如,当前版本支持 Transport 且没有正在运行的扫描时,可以显式请求自动选择协议:

busctl --system call org.bluez /org/bluez/hci0 org.bluez.Adapter1 SetDiscoveryFilter 'a{sv}' 1 Transport s auto

auto、bredr、le 的行为应结合所安装 BlueZ 版本的接口文档核对。如果程序固定使用 le,经典蓝牙设备不会进入结果;固定使用 bredr,BLE 广播设备同样不会出现。过滤器在运行中能否修改、修改何时生效,也取决于具体版本。

另一个常见错误是看到日志里的 StartDiscovery 不断重试,就继续增加调用频率。正确做法是先读取 Discovering,再使用适配器级锁、退避和重试上限。BlueZ 尚未就绪时,StartDiscovery 可能失败;扫描已经在运行时再次调用,也可能得到“已有发现过程”之类的错误。不要捕获异常后立即无间隔重试,更不要在退出清理时停止另一个进程拥有的扫描。

资料 中出现了 DeviceFound 这个名称,但不能直接把它当作所有 BlueZ 版本共有的 D-Bus 事件名。跨版本实现应以本机 introspection 和官方接口为准,重点监听 Object Manager 的对象新增、移除以及 PropertiesChanged。

阅读 BlueZ 源码时,可以沿四条路径定位:适配器方法分发、org.bluez.Adapter1 的发现控制、发现后设备对象的创建与属性更新、对象注册和信号通知。《实战 2:基于 BlueZ 源码,实现蓝牙设备的自动扫描功能》可以作为源码导读入口,但具体函数名、分支和行号都应按项目采用的 BlueZ 版本核对。

设备发现异常的同机对照排查

bluetoothctl 的价值在于验证同一台机器上的 BlueZ 服务、适配器和基本发现链路。为了保证对照的有效性,验证流程必须让 CLI 和程序使用同一用户、同一适配器,并顺序运行。

bluetoothctl
> list
> show
> power on
> scan on
> devices
> info <实际设备对象路径>
> scan off

list 用来确认 BlueZ 是否识别到控制器,show 用来检查适配器状态,scan on 才是启动发现过程。扫描是异步的,命令执行后不会立刻返回全部设备;应留出观察时间,再执行 scan off。其中 devices 主要用于查看 BlueZ 已知的设备对象,不能单独证明设备此刻就在射频范围内。

不要让 bluetoothctl 和自定义程序同时控制同一个适配器。需要对照时,应先停止程序中的扫描,再运行 CLI;反过来也一样。

对照结果 优先怀疑 下一步
bluetoothctl 和 D-Bus 都调用失败 BlueZ 服务、权限、适配器未就绪 查服务、总线、调用用户和对象路径
bluetoothctl 成功,程序收不到设备 事件订阅、过滤条件、代码解析或去重 对照过滤器,检查订阅与快照逻辑
两者都调用成功但没有设备 控制器、协议类型、远端设备状态 换已知可发现设备,分别检查 BLE 与 BR/EDR
只有 root 能成功 用户身份或 D-Bus 策略 用相同命令分别测试普通用户与 root
程序反复触发扫描 Discovering 状态和重试逻辑 增加状态检查、进程锁、退避及停止逻辑

CLI 成功而程序失败时,优先检查程序是否真的连接了系统总线。自定义 D-Bus 客户端如果误连用户总线,即使主机上 BlueZ 正常,也看不到 org.bluez。其次检查订阅是否发生在 StartDiscovery 之前,以及是否把 PropertiesChanged 中的 RSSI 更新误判成新设备。最后再检查过滤器和去重逻辑,尤其是程序设置了 Transport=le、按设备名称去重,或者只记录首次发现却没有合并快照的情况。

两者都成功却发现不了设备时,调用链已经基本排除,故障排查的重点应转向射频和远端状态。经典蓝牙设备通常需要进入可发现状态,BLE 设备依赖广播,程序设置的 Transport 过滤器也可能与 CLI 当时的过滤状态不同。BlueZ 同时覆盖 BR/EDR 和 BLE,但最终能否发现仍受控制器、固件、远端广播内容和过滤条件影响。BLE 还可能使用临时或可解析地址,因此不能依赖固定 MAC 地址判断是否为同一物理设备。

适配器的 Discoverable 属性表示“本机是否允许被其他设备发现”,它与“本机能否发现远端设备”不是一回事。启动扫描需要关注 Powered 和 Discovering,不需要为了扫描远端设备而把本机设置为 Discoverable。

权限、适配器与版本差异排查

兼容性排查应按“服务与总线、适配器与控制器、扫描语义、版本与源码”逐层缩小范围,不能用一次重启覆盖所有问题。

先确认程序连接的是主机系统总线,并且 org.bluez 已经注册:

systemctl status bluetooth
busctl --system list org.bluez
journalctl -u bluetooth -b --no-pager

普通用户失败而 root 成功时,应记录 D-Bus 返回的完整错误名和错误文本。发行版的 D-Bus 或 Polkit 策略、用户会话以及程序自身权限都可能造成差异,不能仅凭“root 能跑”就直接放开宽泛的总线权限。容器环境还要特别注意:容器内可能连接了自己的私有 D-Bus,而不是宿主机系统总线;即使切换成容器内 root,也不等于获得了宿主机 BlueZ 的访问权。

适配器是否存在,则通过控制器和驱动层继续确认:

bluetoothctl list
bluetoothctl show
rfkill list bluetooth
lsusb
journalctl -k -b --no-pager | grep -iE 'bluetooth|btusb|firmware'

bluetoothctl list 没有控制器时,问题通常还在 BlueZ 对象注册之前。USB 适配器可以结合 USB 枚举和内核日志查看驱动与固件加载情况;串口或 UART 适配器不一定出现在 lsusb 中,需要结合对应平台设备和驱动日志判断。无线开关被 rfkill 阻止、控制器固件加载失败或驱动反复重置,都可能表现为 D-Bus 服务存在、但适配器始终无法进入可用状态。

扫描语义层重点检查四项:适配器是否 Powered,是否已经处于 Discovering,过滤器是否过窄,以及远端设备是否真的可发现。StartDiscovery 返回成功只表示 BlueZ 接受了启动请求,不代表控制器已经收到任何远端响应。

版本差异必须分开记录。内核版本、BlueZ 用户态包、守护进程、客户端库和控制器固件不是同一层:

cat /etc/os-release
uname -r
bluetoothd --version
bluetoothctl --version
busctl --version

bluetoothctl --version 主要反映客户端版本,实际 D-Bus 接口由运行中的 bluetoothd 提供;Python、Go 或其他 D-Bus 客户端库还应记录各自的版本。不同发行版可能提供不同过滤器选项、工具和系统总线策略,因此不要先写死版本号再猜行为。先用本机 introspect 确认 StartDiscovery、StopDiscovery、Discovering 和过滤器接口是否存在,再决定兼容分支。

部分旧环境仍提供 hciconfig,可以把它作为控制器侧的辅助对照:

sudo hciconfig hci0 up
sudo hciconfig hci0 iscan

这类命令在旧式树莓派资料中确实用于开启适配器和传统扫描,但资料也指出其输出以地址为主、周围设备多时难以区分。工具可能没有安装,也可能因当前驱动、权限或 BlueZ 管理状态而失败,因此生产自动化仍应优先使用 D-Bus,而不是把旧式 HCI 命令当作稳定接口。

报障时建议直接附上下面这组信息,而不是只贴一句“搜不到设备”:

发行版及版本:
内核版本:
BlueZ 包/守护进程版本:
bluetoothctl 版本:
D-Bus 客户端库及版本:
适配器型号和连接方式:
驱动及固件状态:
调用用户:
适配器对象路径:
Powered / Discovering 状态:
过滤器配置:
busctl monitor 是否出现 InterfacesAdded:
bluetoothctl scan on 是否能发现:
失败发生在调用、订阅、解析还是去重阶段:
完整的 D-Bus 错误名和错误文本:

自动扫描程序无法绕过控制器硬件、控制器固件和远端设备可发现性,也无法消除 BlueZ、内核及客户端库之间的版本差异。代码只能保证在正确的系统总线、适配器和过滤条件下可靠接收 BlueZ 事件;剩余问题仍要靠同机对照和上述系统信息继续缩小范围。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容