Krolyxon
Commit 090b358

Branches

Refs →

budctlcratesbudctl-coresrcbluez.rs

146 lines · 4.9 KB · Rust

1//! Paired devices, from BlueZ over the system D-Bus.
2//!
3//! The only use of D-Bus in budctl is listing paired devices. budctl
4//! connects to the system bus socket at its fixed path itself, so no environment variable can
5//! point the connection anywhere else (such as a TCP address).
6
7use std::collections::HashMap;
8use std::os::unix::net::UnixStream;
9
10use zbus::blocking::connection::Builder;
11use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
12
13use crate::PairedDevice;
14use crate::error::{Error, Result};
15
16/// The system bus. `DBUS_SYSTEM_BUS_ADDRESS` is deliberately ignored.
17const SYSTEM_BUS_SOCKET: &str = "/var/run/dbus/system_bus_socket";
18
19type Properties = HashMap<String, OwnedValue>;
20type ManagedObjects = HashMap<OwnedObjectPath, HashMap<String, Properties>>;
21
22fn bluez_error(error: impl std::fmt::Display) -> Error {
23 Error::Bluez(error.to_string())
24}
25
26/// Every paired device BlueZ knows, sorted by address.
27pub fn paired_devices() -> Result<Vec<PairedDevice>> {
28 let stream =
29 UnixStream::connect(SYSTEM_BUS_SOCKET).map_err(|e| Error::Bluez(format!("{SYSTEM_BUS_SOCKET}: {e}")))?;
30 let connection = Builder::async_io_unix_stream(stream).build().map_err(bluez_error)?;
31 let reply = connection
32 .call_method(
33 Some("org.bluez"),
34 "/",
35 Some("org.freedesktop.DBus.ObjectManager"),
36 "GetManagedObjects",
37 &(),
38 )
39 .map_err(bluez_error)?;
40 let objects: ManagedObjects = reply.body().deserialize().map_err(bluez_error)?;
41
42 let mut devices: Vec<PairedDevice> = objects
43 .values()
44 .filter_map(|interfaces| interfaces.get("org.bluez.Device1"))
45 .filter_map(device)
46 .collect();
47 devices.sort_by_key(|d| d.address);
48 Ok(devices)
49}
50
51/// A paired device from its `org.bluez.Device1` properties; `None` if it is not paired or has
52/// no usable address.
53fn device(properties: &Properties) -> Option<PairedDevice> {
54 if !flag(properties, "Paired") {
55 return None;
56 }
57 let address = text(properties, "Address")?.parse().ok()?;
58 let name = text(properties, "Name").unwrap_or_default();
59 let alias = text(properties, "Alias").unwrap_or_else(|| name.clone());
60 let uuids = properties
61 .get("UUIDs")
62 .and_then(|v| match &**v {
63 Value::Array(array) => Some(
64 array
65 .iter()
66 .filter_map(|item| match item {
67 Value::Str(s) => Some(s.as_str().to_ascii_lowercase()),
68 _ => None,
69 })
70 .collect(),
71 ),
72 _ => None,
73 })
74 .unwrap_or_default();
75 Some(PairedDevice {
76 address,
77 name,
78 alias,
79 connected: flag(properties, "Connected"),
80 uuids,
81 })
82}
83
84fn text(properties: &Properties, key: &str) -> Option<String> {
85 match &**properties.get(key)? {
86 Value::Str(s) => Some(s.as_str().to_owned()),
87 _ => None,
88 }
89}
90
91fn flag(properties: &Properties, key: &str) -> bool {
92 matches!(properties.get(key).map(|v| &**v), Some(Value::Bool(true)))
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 fn properties(entries: Vec<(&str, Value<'static>)>) -> Properties {
100 entries
101 .into_iter()
102 .map(|(k, v)| (k.to_owned(), OwnedValue::try_from(v).unwrap()))
103 .collect()
104 }
105
106 #[test]
107 fn paired_device_from_properties() {
108 let props = properties(vec![
109 ("Address", Value::from("AA:BB:CC:DD:EE:FF")),
110 ("Name", Value::from("realme Buds T310")),
111 ("Alias", Value::from("my buds")),
112 ("Paired", Value::from(true)),
113 ("Connected", Value::from(true)),
114 ("UUIDs", Value::from(vec!["0000110B-0000-1000-8000-00805F9B34FB"])),
115 ]);
116 let device = device(&props).unwrap();
117 assert_eq!(device.address.to_string(), "AA:BB:CC:DD:EE:FF");
118 assert_eq!(device.name, "realme Buds T310");
119 assert_eq!(device.alias, "my buds");
120 assert!(device.connected);
121 assert_eq!(device.uuids, ["0000110b-0000-1000-8000-00805f9b34fb"]);
122 }
123
124 #[test]
125 fn unpaired_or_unaddressed_devices_are_skipped() {
126 let unpaired = properties(vec![
127 ("Address", Value::from("AA:BB:CC:DD:EE:FF")),
128 ("Paired", Value::from(false)),
129 ]);
130 assert!(device(&unpaired).is_none());
131 let no_address = properties(vec![("Paired", Value::from(true))]);
132 assert!(device(&no_address).is_none());
133 }
134
135 #[test]
136 fn alias_falls_back_to_name() {
137 let props = properties(vec![
138 ("Address", Value::from("AA:BB:CC:DD:EE:FF")),
139 ("Name", Value::from("realme Buds T310")),
140 ("Paired", Value::from(true)),
141 ]);
142 let device = device(&props).unwrap();
143 assert_eq!(device.alias, "realme Buds T310");
144 assert!(!device.connected);
145 }
146}