bluetoothctl client tool
介绍
在使用某一家SoC的蓝牙库的时候,很多东西都无法修改。但是在测试的过程中,发现这家SoC的蓝牙库也是基于bluez的dbus实现的,那么我也可以封装一个蓝牙库。
首先,我手里有树莓派4B一台,该设备具备了蓝牙和Wi-Fi功能。那么只需要通过研究bluez项目中的bluetoothctl工具的源码,然后进行自定义修改,最后封装成自己的蓝牙库。
准备
硬件平台
树莓派环境介绍:
系统:Ubuntu 24.04 LTS
内核:6.8.0-1009-raspi
架构:aarch64
内存:8GB
阅读蓝牙官方文档,通过基于bluez的dbus接口编程需要相关依赖库:glib-2.0 、gio-2.0、 dbus-1,如果树莓派环境没有以上依赖库,则通过apt安装
1 | sudo apt update |
1 | # 通过pkg-config可以查找对应的头文件目录和库文件 |
涉及的头文件目录和库文件有如下:
-I/usr/include/glib-2.0
-I/usr/lib/aarch64-linux-gnu/glib-2.0/include
-pthread -I/usr/include/libmount
-I/usr/include/blkid
-lgio-2.0
-lgobject-2.0
-lglib-2.0
-ldbus-1
1 | # 通过dpkg查看相关依赖库是否已经安装 |
涉及的依赖包有如下:
libglib2.0-dev-bin
libglib2.0-dev-bin
libdbus-1-dev:arm64
协议文档
dbus数据模型
dbus使用一套类型于JSON类型的数据模型,但还是有所差别,dbus数据模型:
常规名字 | ASCII符号 | 数据定义 | 编码存储 |
---|---|---|---|
BYTE | y | guchar | unsigned 8-bit integer |
BOOlEAN | b | gboolean | boolean value: 0 is false, 1 is true |
INT16 | n | gint16 | signed 16-bit integer |
UINT16 | q | guint16 | unsigned 16-bit integer |
INT32 | i | gint32 | signed 32-bit integer |
UINT32 | u | guint32 | unsigned 32-bit integer |
INT64 | x | gint64 | signed 64-bit integer |
UINT64 | t | guint64 | unsigned 64-bit integer |
DOUBLE | d | gdouble | double-precision floating point |
UNIX_FD | h | unsigned 32-bit representing an index into an out-of-band array of file descriptors | |
STRING | s | string, 必须是有效的UTF-8字符串 | |
OBJECT_PATH | o | object_path,例如:/org/bluez/hci0/dev_50_64_2B_BF_46_36 | |
SIGNATURE | g | signature类型,即签名类型 | |
ARRAY | a | array, like [] | |
VARIANT | v | variant,变体类型 | |
STRUCT | r 、( 、 ) | 结构类型 | |
DICT_ENTRY | e 、{ 、 } | 字典或映射(键值对数组)的条目 |
关于结构类型,其组成是由其他类型组合而成。例如:(a{oa{sa{sv}}})
通过上述表格的数据类型来解析结构类型数据。
1、括号 ()
表示一个结构体
2、a{oa{sa{sv}}}
表示一个字典数组,其中的键为对象路径(即 o),值为另外一个字典数组(即 a{sa{sv}})
3、a{sa{sv}}
表示一个字典数组,其中的键为字符串(即 s),值为另外一个字典数组(即 a{sv})
4、a{sv}
表示一个字典数组,其中的键为字符串(即 s),值为变体(即v)
注意:字典数组要分成两个部分:字典 + 数组,类型是字典,但是字典里面多个字典元素
a{oa{sa{sv}}}
的具体案例如下:
1 | { |
dbus接口模型
服务名称(dbus name):
服务端程序在D-Bus上注册的服务名称(即 dbus name),在D-Bus上的注册的服务后会产生一个地址(例如:/org/bluez
)和唯一名称(例如:1.466
),地址和唯一名称都是随机生成的,客户端通过dbus name来知道服务名称。dbus name的格式,例如:org.bluez
,这是D-Bus的规定,没有为什么。对象(Object):
对象以路径的形式表示,对象路径代表一个对象实例。对象路径的前缀以dbus name为参考,接上其他的。例如,上述dbus name:org.bluez
,那么对象路径前缀是:/org/bluez
,接上其他的,形成完成的对象路径,例如:/org/bluez/hci0
接口(Interface):
接口,顾名思义。接口名称的格式规定与dbus name一样,例如:org.bluez.Adapter1
成员名称(Member name):
成员包括:方法(Method)和信号(Signal),在大多数方面,他们几乎是一样的,除了两点:1、Signal是在总线中进行广播的,而Method是指定发给某个进程的。
2、Signal 不会有返回,而 Method 一定会有返回(Method调用可以同步的或是异步的)。
从 C API 的层面来看,Member name 最大的作用就是在两个进程间共享 “发出的消息的类型信息”,DBus 只能以 Signal or Method 来进行消息通信。
dbus通用接口
dbus标准接口共有4个,分别是:
- org.freedesktop.DBus.Peer
- org.freedesktop.DBus.Introspectable
- org.freedesktop.DBus.Properties:用于获取和设置对象属性,并监听属性变化。
- org.freedesktop.DBus.ObjectManager:用于管理和监控 D-Bus 对象,通常用于获取所有对象及其接口和属性。
org.freedesktop.DBus.Properties
常用的方法有:
- Get:获取对象的特定属性值
- Set:设置对象的特定属性值
- GetAll:获取对象的所有属性值
常用的信号有:
- PropertiesChanged:当对象的属性发生变化时发出信号
org.freedesktop.DBus.ObjectManager
常用的方法有:
- GetManagedObjects:获取所有受管理的对象及其接口和属性
常用的信号有:
InterfacesAdded:当新对象添加时发出信号
InterfacesRemoved:当对象接口移除时发出信号
dbus for bluez接口
bluez dbus api 文档涉及到各个不同接口,主要关注这几个接口:
- org.bluez.Adapter1
- org.bluez.Device1
- org.bluez.Agent1
org.bluez.Adapter1
接口介绍:
这个接口用于管理蓝牙适配器,实现了蓝牙设备的搜索和管理功能。
1 | :Service: org.bluez |
方法介绍:
方法名称 | 功能 |
---|---|
void StartDiscovery() | 开始扫描设备 |
void StopDiscovery() | 结束扫描设备 |
void RemoveDevice(object device) | 移除蓝牙设备 |
void SetDiscoveryFilter(dict filter) | 设置扫描条件 |
array{string} GetDiscoveryFilters() | 获取扫描条件 |
object ConnectDevice(dict properties) [experimental] | 连接蓝牙设备 |
属性介绍:
属性名称 | 权限 | 说明 |
---|---|---|
string Address | [readonly] | 蓝牙设备MAC地址 |
string AddressType | [readonly] | 取值范围:public和random |
string Name | [readonly] | 蓝牙设备名称 |
string Alias | [readwrite] | 蓝牙设备别名 |
uint32 Class | [readonly] | 蓝牙设备类型 |
boolean Powered | [readwrite] | 打开或关闭适配器电源 |
string PowerState | [readonly, experimental] | 适配器电源状态 |
boolean Discoverable | [readwrite] (Default: false) | 打开或关闭适配器可被发现状态 |
boolean Pairable | [readwrite] (Default: true) | 打开或关闭适配可被配对状态 |
uint32 PairableTimeout | [readwrite] (Default: 0) | 配对超时时间(单位:秒) |
uint32 DiscoverableTimeout | [readwrite] (Default: 180) | 发现超时时间(单位:秒) |
boolean Discovering | [readonly] | 扫描执行状态 |
array{string} UUIDs | [readonly] | 128-bit UIIDs |
string Modalias | [readonly, optional] | 内核使用的本地设备ID |
array{string} Roles | [readonly] | 支持角色列表 |
array{string} ExperimentalFeatures | [readonly, optional] | 实验性质的128-bit UIIDs |
uint16 Manufacturer | [readonly] | 设备制造商 |
byte Version | [readonly] | 设备支持的蓝牙版本 |
org.bluez.Device1
接口介绍:
这个接口用于管理蓝牙设备,实现了设备的配对、连接和断开连接等功能。
1 | :Service: org.bluez |
方法介绍:
方法名称 | 功能 |
---|---|
void Connect() | 连接蓝牙设备 |
void Disconnect() | 断开蓝牙设备 |
void ConnectProfile(string uuid) | 连接配置文件 |
void DisconnectProfile(string uuid) | 断开配置文件 |
void Pair() | 配对蓝牙设备 |
void CancelPairing() | 取消配对请求 |
属性介绍:
属性名称 | 权限 | 说明 |
---|---|---|
string Address | [readonly] | 蓝牙设备MAC地址 |
string AddressType | [readonly] | 取值范围:public和random |
string Name | [readonly, optional] | 蓝牙设备名称 |
string Icon | [readonly, optional] | 蓝牙设备图标 |
uint32 Class | [readonly, optional] | 蓝牙设备类型 |
uint16 Appearance | [readonly, optional] | 蓝牙设备外观 |
array{string} UUIDs | [readonly, optional] | 128-bit UIIDs |
boolean Paired | [readonly] | 蓝牙设备配对状态 |
boolean Bonded | [readonly] | 蓝牙设备绑定状态 |
boolean Connected | [readonly] | 蓝牙设备连接状态 |
boolean Trusted | [readwrite] | 蓝牙设备信任状态 |
boolean Blocked | [readwrite] | 蓝牙设备阻塞状态 |
boolean WakeAllowed | [readwrite] | 蓝牙设备唤醒功能 |
string Alias | [readwrite] | 蓝牙设备别名 |
object Adapter | [readonly] | 蓝牙设备所属适配器对象 |
boolean LegacyPairing | [readonly] | 传统配对功能 |
string Modalias | [readonly, optional] | 内核使用的设备ID |
int16 RSSI | [readonly, optional] | 蓝牙设备信号强度 |
int16 TxPower | [readonly, optional] | 蓝牙广播发射功率电平 |
dict ManufacturerData | [readonly, optional] | 蓝牙制造商数据 |
dict ServiceData | [readonly, optional] | 蓝牙服务数据 |
bool ServicesResolved | [readonly] | 服务解析是否完成 |
array{byte} AdvertisingFlags | [readonly] | 蓝牙广播数据标志 |
dict AdvertisingData | [readonly] | 蓝牙广播数据 |
array{object, dict} Sets | [readonly, experimental] | 蓝牙设备所属对象集合 |
org.bluez.Agent1
接口介绍:
这个用于管理蓝牙代理,实现了蓝牙认证和授权功能。在进行蓝牙配对时,代理程序将被调用执行认证和授权操作。
1 | :Service: unique name |
方法介绍:
方法名称 | 功能 |
---|---|
void Release() | 释放任务 |
string RequestPinCode(object device) | 请求PIN码 |
void DisplayPinCode(object device, string pincode) | 显示PIN码 |
uint32 RequestPasskey(object device) | 请求秘钥 |
void DisplayPasskey(object device, uint32 passkey, uint16 entered) | 显示秘钥 |
void RequestConfirmation(object device, uint32 passkey) | 请求确认秘钥 |
void RequestAuthorization(object device) | 请求授权 |
void AuthorizeService(object device, string uuid) | 授权服务 |
void Cancel() | 取消请求 |
属性介绍:
无属性
gdbus接口
gio库提供了对于dbus接口的高级封装:gdbus,常用API总结,方便直接定位查寻:
描述:同步连接到bus_type指定的消息总线。若返回值为NULL,则错误发生;若返回值不为NULL,调用者有责任使用 g_object_unref()
释放返回值。当调用错误发生时,error != NULL
,可以通过error知道错误的原因,调用者有责任使用 g_error_free()
释放error。
1 | GDBusConnection* |
描述:创建一个新的GMainLoop结构体,调用者有责任去释放这个结构体。
1 | GMainLoop* |
描述:减少对象的参考计数。当其参考计数降至0时,对象将最终确定(即其内存已释放)。
1 | void |
描述:运行主循环,直到 g_main_loop_quit() 被调用才结束。
1 | void |
描述:创建一个代理
1 | void |
描述:异步调用代理上的 method_name 方法。
1 | void |
描述:同步调用method_name
上的方法proxy
,调用线程被阻塞,直到收到回复。若返回值为NULL,则错误发生;若返回值不为NULL,调用者有责任使用 g_variant_unref()
去释放返回值。
1 | GVariant* |
描述:同步调用bus_name 拥有的object_path 远程对象上interface_name D-Bus 接口上的method_name 方法。当返回值为NULL,则有错误发生。若返回值不为NULL,调用有责任通过 g_variant_unref() 去释放它。该函数被调用时,线程被阻塞,直到收到回复。
若 error != NULL
说明错误发生,调用者有责任去释放error。
1 | GVariant* |
描述:异步调用bus_name 拥有的object_path 远程对象上interface_name D-Bus 接口上的method_name 方法。
1 | void |
g_dbus_connection_call_finish()
描述:完成由 g_dbus_connection_call() 启动的操作。若返回值为NULL,则错误发生。当返回值不为NULL,调用者有责任通过 g_variant_unref() 去释放它。
1 | GVariant* |
g_dbus_connection_signal_subscribe()
描述:在connection上订阅信号,当信号被接收时调用回调函数。
1 | guint |
描述:解构一个GVariant
实例,其功能类似 scanf()
1 | void |
描述:从容器 GVariant 实例中读取子项。这包括variants、maybes、arrays、tuples和dictionary entries。在任何其他类型的 GVariant 上调用此函数都是错误的。调用者有责任使用 g_variant_unref() 去释放它(返回值)。
1 | GVariant* |
描述:初始化(不申请内存) GVariantIter 变量。返回值表示项目数量。
iter
在此调用之前可能完全未初始化;其旧值将被忽略。只要迭代器value
存在,它就会一直有效,并且不需要以任何方式释放。
1 | gsize |
描述:获取容器的下一项,并根据将其解包到变量参数列表中format_string
,返回TRUE。若返回值为FALSE,则该项没有值。
此函数的变量参数列表中给出的所有指针均假定指向未初始化的内存。调用者有责任释放解包过程返回的所有值。
1 | gboolean |
内存释放的案例
1 | // Iterates a dictionary of type 'a{sv}' |
注意:g_variant_iter_next()
和 g_variant_iter_loop()
的区别,其中比较重要的是变量释放的位置。
描述:获取容器中的下一项,并根据将其解包到变量参数列表中format_string
,返回 TRUE,若容器没有下一项,则返回 FALSE。
第一次调用此函数时,变量参数列表中出现的指针假定指向未初始化的内存。第二次及以后的调用时,假定将给出相同的指针,并且它们将指向上次调用此函数时设置的内存。这样可以酌情释放先前的值。
此函数旨在与 while 循环一起使用,此函数只能在迭代数组时使用。仅使用格式字符串的字符串常量调用此函数才有效,并且每次都必须使用相同的字符串常量。
1 | gboolean |
内存释放的案例:
1 | // Iterates a dictionary of type 'a{sv}' |
大多数情况下您应该使用 g_variant_iter_next()。如果您只循环简单的整数和字符串类型,g_variant_iter_next()
则绝对是首选。对于字符串类型,使用“&”前缀可以完全避免分配任何内存(从而也避免释放任何内容)。
此函数实际上仅在解包到GVariant
或 时有用,GVariantIter
以便允许您跳过对 g_variant_unref()
或 g_variant_iter_free()
的调用。
描述:创建一个新的GVariant变量实例,若返回值不为NULL,调用者有责任去通过 g_variant_unref
释放它。这个函数一般配合while循环一起使用,且只能在迭代数组时使用,仅使用格式字符串的字符串常量调用此函数才有效,每次都必须使用相同的字符串常量。
格式字符串的第一个字符不能是“*”、“?”、“@”或“r”;本质上,GVariant
此函数必须始终构造一个新的(而不是不加修改地传递它)。
1 | GVariant* |
描述:创建一个D-Bus对象路径的GVariant变量
1 | GVariant* |
描述:其作用类似 strstr()
1 | gchar* |
描述:其作用类似 tolower()
1 | gchar* |
描述:返回string的值。
1 | const gchar* |
描述:返回uint32的值
1 | guint32 |
描述:返回gboolean的值,取值范围:TRUE or FALSE
1 | gboolean |
描述:返回int16的值。
1 | gint16 |
描述:获取value值的类型字符串
1 | const gchar* |
描述:其作用类似 strcmp()
1 | int |
描述:确认value的类型
1 | const GVariantType* |
描述:检查值的类型是否与提供的类型匹配。
1 | gboolean |
描述:其作用类似 printf(),glib提供一套类似于 #include <string.h>
的函数库String Utilities,使用该函数库是必须显示的声明 #include <glib/gprintf.h>
。
1 | gint |
描述:将GVariant变量转换成字符串格式。若返回值不为NULL,调用者有责任释放返回值。
1 | gchar* |
描述:释放GError指向的内存
1 | void |
工具
在Ubuntu 24.04 LTS系统上可以下载 d-feet工具,可以UI形式显示协议文档中的那些接口,以便快速测试和实现蓝牙功能。
bluez
Linux系统使用 BlueZ 作为其官方蓝牙堆栈。BlueZ提供了对经典蓝牙和低功耗蓝牙的支持。目前最新的bluez版本是5.77,可以查看Github 开源项目 bluez。
bluez提供一个bluetoothd服务程序和其他的工具程序。工具程序包括:
- hcitool:主要是控制蓝牙模块的动作
- hciconfig:可以查看当前系统中的蓝牙适配器及其状态
- sdptool:主要是查看和添加服务
- rfcomm:主要用于连接和读写
- agent:主要用于配对
- hcidump:可以查看host和controler之间hci接口通信过程,可以用于调试
- l2ping:
- gatttool:主要针对BLE蓝牙模块的工具(bulez5后开始用bluetoothctl代替)
alsa-utils
alsa-utils
是一组用于配置和管理 Advanced Linux Sound Architecture (ALSA) 的实用程序。ALSA 是 Linux 内核的一部分,用于处理音频输入和输出。alsa-utils
提供了一系列命令行工具,用于控制和配置 ALSA 音频设备。该使用程序提供如下工具集:
amixer:控制音频混合器设备。可以用来调整音量、开关静音等。
aplay:用于播放 PCM 数据或 WAV 文件。
arecord:用于录制 PCM 数据或 WAV 文件。
alsactl:管理 ALSA 控制器状态。可以用来保存和恢复混音器设置。
hdajackretask:重新分配 HD Audio 接口的功能。例如,可以将麦克风插孔重新分配为线路输入。
alsa-info.sh:收集系统上的 ALSA 相关信息,包括硬件设备、驱动程序版本等
alsamixer:一个基于文本的图形界面混音器工具,提供更直观的方式来调整音量、切换输入输出设备等。
bluealsa
bluealsa
是一个用于将 ALSA(Advanced Linux Sound Architecture,高级Linux音频架构)与 Bluetooth 低功耗(即 BLE)音频协议相结合的解决方案。它使得 Linux 系统能够通过 BLE 连接蓝牙音频设备,并让蓝牙音频设备播放音频文件。
开发
根据前文预定的需求,逐个编写并测试功能API接口,最后完成自定义库的封装。
使用bluez进行蓝牙设备连接的流程:
1、蓝牙适配器设置为:power on
2、蓝牙适配器开始扫描
订阅 org.freedesktop.DBus.Properties.PropertiesChanged
信号监听接口属性变化
订阅 org.freedesktop.DBus.ObjectManager.InterfacesAdded
信号监听接口新增
订阅 org.freedesktop.DBus.ObjectManager.InterfacesRemoved
信号监听接口移除
调用 org.bluez.Adapter1.StartDiscovery
方法开始扫描附近蓝牙设备
3、蓝牙适配器配对蓝牙设备
在 InterfacesAdded
信号回调中,检查是否出现目标蓝牙设备
发现目标蓝牙设备后,调用 org.bluez.Adapter1.StopDiscovery
,蓝牙适配器停止扫描
调用 org.bluez.Device1.Pair
方法进行配对蓝牙设备
调用 org.freedesktop.DBus.ObjectManager.GetManagedObjects
方法获取适配器配对状态
4、连接设备
配对成功后,调用 org.bluez.Device1.Connect
方法连接蓝牙设备
调用 org.freedesktop.DBus.ObjectManager.GetManagedObjects
方法获取适配器连接状态
测试
测试目标平台:
- x86_64
- aarch64
- armv7l
开发完成后,需要测试接口稳定性,包括:打开/关闭、连接/断开。
x86_64平台
bluez版本:5.64
在x86_64平台测试连接蓝牙接口的稳定性,发现连接蓝牙设备后,使用 RemoveDevice
接口无法移除蓝牙设备,仅仅只是断开蓝牙设备连接。
aarch64平台
bluez版本:5.72
在aarch64平台测试连接蓝牙设备接口的稳定性,出现一个奇怪的现象:
当扫描到目标蓝牙设备后,立即执行连接操作,但是目标蓝牙设备立马就被删除了,导致连接操作超时。
很明显,目标蓝牙设备被bluetoothd过早移除了。查阅bluez官网文档,有这么一句话:
Once the discovery stops, devices neither connected to or paired will be automatically removed by bluetoothd within three minutes.
翻译:一旦扫描停止,没有连接或配对的设备将在3分钟内被bluetoothd自动移除。
armv7l平台
bluez版本:5.77
在armv7l平台测试连接蓝牙设备接口的稳定性:
目标蓝牙设备:JBL ROCK,测试次数100次,成功99次,失败1次。失败原因:连接蓝牙设备时,蓝牙设备被过早移除。
目标蓝牙设备:小爱触屏音箱,测试次数100次,成功93次,失败7次。失败原因:连接蓝牙设备时,蓝牙设备被过早移除,之后尽管被发现,还是会导致连接超时。
目标蓝牙设备:Mi Speaker,测试册数100次,成功93次,失败7次。失败原因:连接蓝牙设备时,蓝牙设备被过早移除。
关于“扫描到目标蓝牙设备后,执行连接操作,但是蓝牙设备立即被移除,导致连接超时” 这个问题,或许需要查看bluez项目中的bluetoothd源码,研究扫描到的蓝牙设备被bluetoothd自动移除的条件。
参考
2、bluez with Official Linux Bluetooth protocol stack
7、github.com/nkim-bitzap/bluetooth
9、Bluetooth Office Document: Assigned Numbers Document (PDF)