第三课:BlueZ核心库入门——DBus通信机制、核心接口与hci/mgmt命令基础

好,咱们进入第三课。说实话,这一课是蓝牙开发的分水岭。你搞懂了DBus和BlueZ的接口模型,后面写代码就是水到渠成的事。如果这关没过,后面看代码就跟看天书一样。

我个人习惯,学任何东西先搞清楚它“怎么说话”的。BlueZ在Linux上跟应用层通信,靠的就是DBus。你想想看,一个蓝牙适配器要同时处理电话、音乐、文件传输,如果没有一套清晰的通信机制,那不乱套了?

一、DBus通信机制——BlueZ的“神经系统”

DBus说白了就是一个进程间通信的“总线”。BlueZ作为服务端,把蓝牙功能暴露成一个个DBus对象。你的应用程序作为客户端,通过DBus调用这些对象的方法、获取属性、监听信号。

我在项目中遇到过一个问题:同时连接两部手机,结果蓝牙音乐和电话通道互相干扰。后来发现,就是因为没有正确监听DBus信号,导致状态同步出了问题。

核心概念:

  • 总线(Bus):系统总线(system bus)用于系统服务,会话总线(session bus)用于桌面应用。BlueZ跑在系统总线上。
  • 对象路径(Object Path):每个蓝牙设备、适配器都有一个唯一路径,比如 /org/bluez/hci0 代表第一个蓝牙适配器。
  • 接口(Interface):定义了一组方法和信号。比如 org.bluez.Adapter1 接口有 StartDiscovery() 方法。
  • 方法(Method):你调用的函数,比如扫描、连接。
  • 信号(Signal):事件通知,比如设备发现、连接断开。

嗯,这里要注意:DBus的调用是异步的。你调用一个方法,它不会立刻返回结果,而是通过回调告诉你。刚开始写代码的人容易在这里栽跟头——调了方法就以为执行完了,结果数据还没回来。

二、BlueZ提供的核心接口

BlueZ暴露的DBus接口,我整理成了一张表。这张表你最好存下来,开发时经常要翻。

接口名称 主要用途 常用方法/属性
org.bluez.Adapter1 蓝牙适配器管理 StartDiscovery(), StopDiscovery(), Powered, Discovering
org.bluez.Device1 远程设备操作 Connect(), Disconnect(), Pair(), Name, Address
org.bluez.MediaTransport1 音频流传输控制 Acquire(), Release(), State, Codec
org.bluez.PhonebookAccess1 电话簿访问(PBAP) Select(), List(), Size
org.bluez.Call1 通话控制 Answer(), Hangup(), State, LineIdentification

举个例子,你要让蓝牙开始扫描,代码大概长这样:

# 使用dbus-send命令行工具(调试时很方便)
dbus-send --system --dest=org.bluez \
  /org/bluez/hci0 \
  org.bluez.Adapter1.StartDiscovery

# 用Python写的话(实际开发常用)
import dbus

bus = dbus.SystemBus()
adapter = bus.get_object('org.bluez', '/org/bluez/hci0')
adapter_iface = dbus.Interface(adapter, 'org.bluez.Adapter1')
adapter_iface.StartDiscovery()

我曾经踩过一个坑:调用 StartDiscovery() 之后,以为设备会立刻出现在列表里。实际上,你得监听 InterfacesAdded 信号,等BlueZ通知你“发现新设备了”。

我的小技巧:调试阶段先用 busctl monitor org.bluez 看DBus消息流。你能亲眼看到BlueZ发出了什么信号,参数是什么。这比猜代码快十倍。

三、hci与mgmt命令基础——底层控制的两把刀

DBus是高层接口,方便但不够灵活。有时候你需要直接操作蓝牙芯片,比如重置适配器、设置扫描参数、读取RSSI。这时候就要用 hcimgmt 命令。

说白了,DBus是“自动挡”,hci/mgmt是“手动挡”。做车载开发,你早晚得学会手动挡。

3.1 hci命令

hcitool 是经典的蓝牙调试工具。虽然现在BlueZ推荐用 bluetoothctl,但hcitool在底层调试时依然不可替代。

# 查看蓝牙适配器信息
hcitool dev

# 扫描周围设备(经典模式)
hcitool scan

# 查看连接状态
hcitool con

# 读取远程设备名称
hcitool name 11:22:33:44:55:66

我记得有一次,客户反馈蓝牙连不上。用 bluetoothctl 看一切正常,但就是连不上。最后用 hcitool cmd 发了几个底层HCI命令,才发现是芯片的固件版本太老,有个命令的返回值不对。

注意:hcitool在较新的BlueZ版本中已被标记为“废弃”,但底层HCI命令 hcitool cmd 依然可用。如果你用Ubuntu 22.04以上,可能需要安装 bluez-deprecated-tools 包。

3.2 mgmt命令

mgmt 是BlueZ引入的新一代底层控制接口。它通过 /dev/mgmt 字符设备与内核蓝牙子系统通信。相比hci,mgmt更安全、更结构化。

# 使用btmgmt工具(推荐)
btmgmt power off
btmgmt power on
btmgmt find  # 扫描设备
btmgmt info  # 查看适配器信息

# 设置扫描参数(比如只扫描经典蓝牙)
btmgmt --index 0 br  # 开启BR/EDR扫描
btmgmt --index 0 le  # 开启LE扫描

为什么要有两套底层命令?我个人的理解是:hci是“硬件接口”,直接跟蓝牙芯片对话;mgmt是“内核接口”,管理蓝牙子系统的状态。做车载开发,你大概率两个都要用。

四、实战:用DBus监听蓝牙电话状态

咱们来点实际的。假设你要在车载系统上监听电话状态——来电、接通、挂断。用DBus信号是最优雅的方式。

import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib

def on_properties_changed(interface, changed, invalidated):
    if interface == 'org.bluez.Call1':
        state = changed.get('State', '')
        number = changed.get('LineIdentification', '未知号码')
        if state == 'incoming':
            print(f'📞 来电:{number}')
        elif state == 'active':
            print(f'📞 通话中:{number}')
        elif state == 'terminated':
            print(f'📞 通话结束:{number}')

DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()

# 监听所有蓝牙对象的属性变化
bus.add_signal_receiver(
    on_properties_changed,
    signal_name='PropertiesChanged',
    bus_name='org.bluez',
    path='/org/bluez'
)

loop = GLib.MainLoop()
loop.run()

这段代码我实际用在项目里过。当时有个需求:来电时自动降低音乐音量。我就是通过监听 Call1.State 变成 incoming 时,调用音频服务把音量降下来。

避坑指南:我曾经以为 PropertiesChanged 信号只会在状态变化时触发。结果发现,BlueZ在初始化时也会发一次。所以代码里要加个判断,别一上来就以为有电话进来了。

五、总结一下

这一课的内容,说白了就是三件事:

  • DBus:BlueZ的通信语言,你通过它发命令、收通知。
  • 核心接口:Adapter1管适配器,Device1管设备,Call1管通话——各司其职。
  • hci/mgmt:底层调试的两把刀,遇到DBus搞不定的问题,就撸起袖子用它们。

下一课,咱们要真正动手了——用Python写一个蓝牙电话的Demo。到时候你会发现,今天学的DBus知识,全都会用上。

嗯,就这样。有问题咱们群里聊。