DBus 入门 —— 获取蓝牙设备电量

DBus 入门 —— 获取蓝牙设备电量

RayAlto OP

以前一直在用 blueman 管理蓝牙设备,后来觉得它的 GUI 设计的不是特别好,操作快了经常卡住,索性直接用 bluetoothctl 管理蓝牙设备了,但没有 blueman 之后少了很多方便的功能,所以这次看看 blueman 背后干了什么。

参考:

1. 设备电量

blueman 会在设备连接时发送通知,显示设备电量:

Blueman 在设备连接时通知设备电量

其实 bluetoothctl 也可以看到电量:

1
bluetoothctl info <设备在这台电脑上的 MAC 地址> # Battery Percentage 一行表示电量

看看 bluetoothctl 来自于哪:

1
pacman -Fy bluetoothctl

结果是来自于 extra/bluez-utils 包,看 Bluez 的文档 Blueman 的源码 大致可以猜到 Bluez 通过 DBus 对外暴露 API ,所以去看了看。

2. DBus 速通

DBus 是一种分布式软总线进程间通信的解决方案,大概像这样:

D-Bus 精简版

简单来说就是系统运行着一个 DBus 服务端,然后想要通信的两个进程通过 DBus 进行通信,据说本质是 Socket 通信。

2.1. 设计

DBus 设计上突出了一种 OOP 风格,首先通信的服务端需要提供 DBus 名 (Bus/Service Name) ,类似 Java 的 package 名,然后在这个 DBus 名下可以有很多 Object ,这些 Object 可能实现了一些 Interface ,这些 Interface 可能会有一些 Property 、一些 Method ,在某些情况下还可能会产生一些 Signal

2.2. Glibmm

对于 C 语言, Freedesktop 推荐了 Glib 的封装,因为我在用 C++ ,所以选择 Glib 的 C++ 封装 Glibmm ,截至 2024.04.08 07:11:12.241 ,在 ArchLinux 下 Gtk4 对应的 C++ 封装为 gtkmm-4.0 ,与其对应的 Glibmm 为 glibmm-2.68 ,因为 DBus 部分的工具在 Gio 里,所以还要链接 Giomm , Giomm 就在 Glibmm 里,但用 Pkgconf 链接 Glibmm 时并不会同时链接 Giomm ( Glibmm 提供了 giomm-2.68.pcglibmm-2.68.pc 两个分开的文件),所以我觉得最好两个都链接一下,比如我在用 CMake 管理项目:

1
2
3
4
5
6
7
8
9
10
find_package(PkgConfig REQUIRED)
pkg_check_modules(GLIBMM REQUIRED glibmm-2.68)
pkg_check_modules(GIOMM REQUIRED giomm-2.68)

# ...

target_link_libraries(${PROJECT_NAME} PRIVATE ${GLIBMM_LIBRARIES})
target_include_directories(${PROJECT_NAME} PRIVATE ${GLIBMM_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} PRIVATE ${GIOMM_LIBRARIES})
target_include_directories(${PROJECT_NAME} PRIVATE ${GIOMM_INCLUDE_DIRS})

2.3. Hello World

去看 Glibmm 的一个 example ,里面用的大多是异步方法,感觉很别扭,又去 Glibmm 的 API Ref 找到了同步方法,改了一下变成这样:

1
2
3
4
5
6
7
8
9
10
11
// 连接到 Session DBus
Glib::RefPtr<Gio::DBus::Connection> connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SESSION);
// 创建 Proxy
Glib::RefPtr<Gio::DBus::Proxy> proxy = Gio::DBus::Proxy::create_sync(
connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus");
Glib::Variant<std::vector<Glib::ustring>> bus_names;
// 调用 ListNames
proxy->call_sync("ListNames").get_child(bus_names);
for (const Glib::ustring& name : bus_names.get()) {
std::cout << name << '\n';
}

先连接到 Session DBus ,然后创建了一个 Proxy ,这个 Proxy 被指向到 DBus Name org.freedesktop.DBus 下的 Object /org/freedesktop/DBus ,而且特指这个对象的 org.freedesktop.DBus Interface

通过这个 Proxy 可以调用里面的所有 Method ,也可以监听所有 Signal ,但貌似不能直接读写 Property (需要调用 Object 的 org.freedesktop.DBus.Properties Interface 下的 Get 方法)

然后通过这个 Proxy 调用了 ListNames Method , ListNames 会输出当前 DBus 的所有 DBus Name ,在我的电脑上的一部分输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
org.freedesktop.DBus
:1.1
:1.2
ca.desrt.dconf
fr.arouillard.waybar
org.a11y.Bus
org.fcitx.Fcitx5
org.freedesktop.IBus
org.freedesktop.IBus.Panel
org.freedesktop.ReserveDevice1.Audio0
org.freedesktop.ReserveDevice1.Audio1
org.freedesktop.impl.portal.desktop.gtk
org.freedesktop.impl.portal.desktop.hyprland
org.freedesktop.portal.Desktop
org.freedesktop.portal.Fcitx
org.freedesktop.portal.IBus
org.freedesktop.systemd1
org.lxde.lxpolkit
org.mozilla.firefox.ZGVmYXVsdC1yZWxlYXNl
org.pulseaudio.Server

2.4. 类型系统

DBus 调用 Method 时的参数、返回值,以及 Interface 下的 Property 等都是不确定类型的, Glib 对于这种数据的解决方式是 Variant 类型,在 Glibmm 中为 Glib::Variant* ,常用的类型的关系:

  • Glib::VariantBase
    • Glib::Variant<bool>
    • Glib::Variant<std::int32_t>
    • Glib::VariantContainerBase
      • Glib::Variant<std::vector<T>>
      • Glib::Variant<std::map<K, V>>
    • Glib::VariantStringBase
      • Glib::Variant<Glib::ustring>

比如上面的 proxy->call_sync 的返回值是一个 Glib::VariantContainerBase ,可以通过 Glib::VariantBase::get_type_string(this) 拿到类型描述字符串,被 Freedesktop 成为 Type Signature ,具体文档在 D-Bus Specification ,比如某个方法返回的 Glib::VariantContainerBase 类型签名为 (a{oa{sa{sv}}}) ,解析步骤如下:

D-Bus 类型签名的解析过程

获取时可以像这样:

1
2
std::map<Glib::DBusObjectPathString, std::map<Glib::ustring, std::map<Glib::ustring, Glib::VariantBase>>> v;
proxy->call_sync(/* params */).get_child(v);

带有参数的 Method 在调用时也需要构造对应类型的值:

1
2
3
4
5
6
7
8
proxy->call_sync("Method xxx",
Glib::VariantContainerBase::create_tuple(
Glib::Variant<std::map<
Glib::DBusObjectPathString, std::map<Glib::ustring,
std::map<Glib::ustring, Glib::VariantBase>
>
>>::create({/* params */})
);

有时返回的 Glib::VariantContainerBase 的 Type Signature 为 (v) ,具体是什么不能通过这个 Type Signature 得知,而且这种 Glib::VariantContainerBase 不可以像上面一样通过 get_child(v) 直接转换成具体类型(会抛出异常),这时可以使用 Glib::VariantContainerBase::print() 先把返回值整个转换成字符串,看看是什么样的,比如我某个 (v) 转换为字符串是 (<byte 0x5a>,) ,可以看出 Glib::VariantContainerBase 里面是一个 Tuple (Glib::VariantContainerBase) ,在里面是 Byte (Glib::Variant<unsigned char>) ,也就是说 (v) 实际上是 ((y)) ,获取具体值可以像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注释格式为:当前值类型,实际类型
// (v), ((y))
Glib::VariantContainerBase ret_0 = properties->call_sync(/* params */);
// v, (y)
Glib::VariantBase ret_1 = ret_0.get_child();
// (v), (y)
Glib::VariantContainerBase ret_2 = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(ret_1);
// v, y
Glib::VariantBase ret_3 = ret_2.get_child();
// y, y
Glib::Variant<unsigned char> ret_4 = Glib::VariantBase::cast_dynamic<Glib::Variant<unsigned char>>(ret_3);

unsigned char ret = ret_4.get();

3. Bluez 速通

Bluez 貌似只通过 DBus 对外暴露 API , ArchLinux 下的 bluez-utils 包里面有相关的文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
rayalto@RayAltoXL ~$ sudo pacman -Ql bluez-utils | grep man5
bluez-utils /usr/share/man/man5/
bluez-utils /usr/share/man/man5/org.bluez.Adapter.5.gz
bluez-utils /usr/share/man/man5/org.bluez.AdminPolicySet.5.gz
bluez-utils /usr/share/man/man5/org.bluez.AdminPolicyStatus.5.gz
bluez-utils /usr/share/man/man5/org.bluez.AdvertisementMonitor.5.gz
bluez-utils /usr/share/man/man5/org.bluez.AdvertisementMonitorManager.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Agent.5.gz
bluez-utils /usr/share/man/man5/org.bluez.AgentManager.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Battery.5.gz
bluez-utils /usr/share/man/man5/org.bluez.BatteryProvider.5.gz
bluez-utils /usr/share/man/man5/org.bluez.BatteryProviderManager.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Device.5.gz
bluez-utils /usr/share/man/man5/org.bluez.DeviceSet.5.gz
bluez-utils /usr/share/man/man5/org.bluez.GattCharacteristic.5.gz
bluez-utils /usr/share/man/man5/org.bluez.GattDescriptor.5.gz
bluez-utils /usr/share/man/man5/org.bluez.GattManager.5.gz
bluez-utils /usr/share/man/man5/org.bluez.GattProfile.5.gz
bluez-utils /usr/share/man/man5/org.bluez.GattService.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Input.5.gz
bluez-utils /usr/share/man/man5/org.bluez.LEAdvertisement.5.gz
bluez-utils /usr/share/man/man5/org.bluez.LEAdvertisingManager.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Media.5.gz
bluez-utils /usr/share/man/man5/org.bluez.MediaControl.5.gz
bluez-utils /usr/share/man/man5/org.bluez.MediaEndpoint.5.gz
bluez-utils /usr/share/man/man5/org.bluez.MediaFolder.5.gz
bluez-utils /usr/share/man/man5/org.bluez.MediaItem.5.gz
bluez-utils /usr/share/man/man5/org.bluez.MediaPlayer.5.gz
bluez-utils /usr/share/man/man5/org.bluez.MediaTransport.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Network.5.gz
bluez-utils /usr/share/man/man5/org.bluez.NetworkServer.5.gz
bluez-utils /usr/share/man/man5/org.bluez.Profile.5.gz
bluez-utils /usr/share/man/man5/org.bluez.ProfileManager.5.gz

3.1. Gatt

简而言之 Bluez 会在 / 路径下实现 org.freedesktop.DBus.ObjectManager Interface ,通过这个 Interface 可以获取 Bluez 提供的所有 Object :

1
2
3
4
5
6
Glib::RefPtr<Gio::DBus::ObjectManagerClient> object_manager
= Gio::DBus::ObjectManagerClient::create_for_bus_sync(Gio::DBus::BusType::SYSTEM, "org.bluez", "/");

for (Glib::RefPtr<Gio::DBus::Object>& object : object_manager->get_objects()) {
// ...
}

3.2. 电量

刚刚获取的 Object 不一定都是蓝牙设备,可以通过检验 Object 是否实现了 org.bluez.Device1 Interface 判断,在此基础上,实现了 org.bluez.Battery1 Interface 的 Object 即为带有电池的蓝牙设备:

1
2
3
4
5
6
7
8
for (Glib::RefPtr<Gio::DBus::Object>& object : object_manager->get_objects()) {
if (object->get_interface("org.bluez.Device1")) {
// 是蓝牙设备
if (object->get_interface("org.bluez.Battery1")) {
// 而且带有电池
}
}
}

Glib 貌似没有提供一个方便地获取 Interface 下 Property 值的方式,所以需要通过 Object 的 org.freedesktop.DBus.Properties 接口的 Get Method 获取:

1
2
3
4
5
6
7
Glib::RefPtr<Gio::DBus::Proxy> properties
= std::dynamic_pointer_cast<Gio::DBus::Proxy>(object->get_interface("org.freedesktop.DBus.Properties"));

Glib::VariantContainerBase battery_percentage = properties->call_sync(
"Get",
Glib::VariantContainerBase::create_tuple({Glib::Variant<Glib::ustring>::create("org.bluez.Battery1"),
Glib::Variant<Glib::ustring>::create("Percentage")}));

获取实际值时需要像上面介绍的 Type Signature 为 (v) 情况的方式

1
2
3
4
5
6
7
int prcentage = static_cast<int>(
Glib::VariantBase::cast_dynamic<Glib::Variant<unsigned char>>(
Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(
battery_percentage.get_child()
).get_child()
).get()
);

3.3. 设备名

设备名也可以像电量一样通过 org.freedesktop.DBus.Properties 接口获取, Bluez 建议使用 org.bluez.Device1 Interface 的 Alias Property 作为设备名:

1
2
3
4
5
6
7
8
9
10
Glib::ustring name = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(
Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(
properties ->call_sync(
"Get",
Glib::VariantContainerBase::create_tuple({
Glib::Variant<Glib::ustring>::create("org.bluez.Device1"),
Glib::Variant<Glib::ustring>::create("Alias")})
).get_child()
).get_child()
).get();

总结

想用 Giomm 发送 Freedesktop 标准 Notification 非常非常麻烦,需要创建 Gio::Application ,不如直接使用 Glibmm 的 D-Bus 封装自己实现一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Glib::RefPtr<Gio::DBus::Proxy> proxy = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::SESSION,
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications"
);
proxy->call_sync(
"Notify",
Glib::VariantContainerBase::create_tuple({
Glib::Variant<Glib::ustring>::create("pro.rayalto.dbus.test"),
Glib::Variant<std::uint32_t>::create(4),
Glib::Variant<Glib::ustring>::create("mpv"),
Glib::Variant<Glib::ustring>::create("summary"),
Glib::Variant<Glib::ustring>::create("body"),
Glib::Variant<std::vector<Glib::ustring>>::create({"default", "on click"}),
Glib::Variant<std::map<Glib::ustring, Glib::VariantBase>>::create({}),
Glib::Variant<std::int32_t>::create(-1)
})
);

总之 D-Bus 有很多有趣的玩法。