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 事件;剩余问题仍要靠同机对照和上述系统信息继续缩小范围。












暂无评论内容