以前一直在用 blueman 管理蓝牙设备,后来觉得它的 GUI 设计的不是特别好,操作快了经常卡住,索性直接用 bluetoothctl 管理蓝牙设备了,但没有 blueman 之后少了很多方便的功能,所以这次看看 blueman 背后干了什么。
参考:
1. 设备电量
blueman 会在设备连接时发送通知,显示设备电量:

其实 bluetoothctl 也可以看到电量:
1
| bluetoothctl info <设备在这台电脑上的 MAC 地址>
|
看看 bluetoothctl 来自于哪:
结果是来自于 extra/bluez-utils 包,看 Bluez 的文档和 Blueman 的源码大致可以猜到 Bluez 通过 DBus 对外暴露 API ,所以去看了看。
2. DBus 速通
DBus 是一种分布式软总线进程间通信的解决方案,大概像这样:

简单来说就是系统运行着一个 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.pc 和 glibmm-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
| Glib::RefPtr<Gio::DBus::Connection> connection = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SESSION);
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;
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::VariantBaseGlib::Variant<bool>Glib::Variant<std::int32_t>- …
Glib::VariantContainerBaseGlib::Variant<std::vector<T>>Glib::Variant<std::map<K, V>>- …
Glib::VariantStringBaseGlib::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}}}) ,解析步骤如下:

获取时可以像这样:
1 2
| std::map<Glib::DBusObjectPathString, std::map<Glib::ustring, std::map<Glib::ustring, Glib::VariantBase>>> v; proxy->call_sync().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({}) );
|
有时返回的 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
|
Glib::VariantContainerBase ret_0 = properties->call_sync();
Glib::VariantBase ret_1 = ret_0.get_child();
Glib::VariantContainerBase ret_2 = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(ret_1);
Glib::VariantBase ret_3 = ret_2.get_child();
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 有很多有趣的玩法。