Krolyxon
Commit 828a2c7

Branches

Refs →

budctlcratesbudctl-coresrcbluez.rs

159 lines · 5.3 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::bt::BdAddr;
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
19/// A device BlueZ has paired, as BlueZ describes it.
20#[derive(Debug, Clone)]
21pub struct PairedDevice {
22 pub address: BdAddr,
23 /// The name the device advertises. Device modules recognize models by this.
24 pub name: String,
25 /// The name the user gave it (BlueZ falls back to `name`). Shown to people.
26 pub alias: String,
27 pub connected: bool,
28 /// Service UUIDs BlueZ found, lowercase and hyphenated.
29 pub uuids: Vec<String>,
30}
31
32type Properties = HashMap<String, OwnedValue>;
33type ManagedObjects = HashMap<OwnedObjectPath, HashMap<String, Properties>>;
34
35fn bluez_error(error: impl std::fmt::Display) -> Error {
36 Error::Bluez(error.to_string())
37}
38
39/// Every paired device BlueZ knows, sorted by address.
40pub fn paired_devices() -> Result<Vec<PairedDevice>> {
41 let stream =
42 UnixStream::connect(SYSTEM_BUS_SOCKET).map_err(|e| Error::Bluez(format!("{SYSTEM_BUS_SOCKET}: {e}")))?;
43 let connection = Builder::async_io_unix_stream(stream).build().map_err(bluez_error)?;
44 let reply = connection
45 .call_method(
46 Some("org.bluez"),
47 "/",
48 Some("org.freedesktop.DBus.ObjectManager"),
49 "GetManagedObjects",
50 &(),
51 )
52 .map_err(bluez_error)?;
53 let objects: ManagedObjects = reply.body().deserialize().map_err(bluez_error)?;
54
55 let mut devices: Vec<PairedDevice> = objects
56 .values()
57 .filter_map(|interfaces| interfaces.get("org.bluez.Device1"))
58 .filter_map(device)
59 .collect();
60 devices.sort_by_key(|d| d.address);
61 Ok(devices)
62}
63
64/// A paired device from its `org.bluez.Device1` properties; `None` if it is not paired or has
65/// no usable address.
66fn device(properties: &Properties) -> Option<PairedDevice> {
67 if !flag(properties, "Paired") {
68 return None;
69 }
70 let address = text(properties, "Address")?.parse().ok()?;
71 let name = text(properties, "Name").unwrap_or_default();
72 let alias = text(properties, "Alias").unwrap_or_else(|| name.clone());
73 let uuids = properties
74 .get("UUIDs")
75 .and_then(|v| match &**v {
76 Value::Array(array) => Some(
77 array
78 .iter()
79 .filter_map(|item| match item {
80 Value::Str(s) => Some(s.as_str().to_ascii_lowercase()),
81 _ => None,
82 })
83 .collect(),
84 ),
85 _ => None,
86 })
87 .unwrap_or_default();
88 Some(PairedDevice {
89 address,
90 name,
91 alias,
92 connected: flag(properties, "Connected"),
93 uuids,
94 })
95}
96
97fn text(properties: &Properties, key: &str) -> Option<String> {
98 match &**properties.get(key)? {
99 Value::Str(s) => Some(s.as_str().to_owned()),
100 _ => None,
101 }
102}
103
104fn flag(properties: &Properties, key: &str) -> bool {
105 matches!(properties.get(key).map(|v| &**v), Some(Value::Bool(true)))
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn properties(entries: Vec<(&str, Value<'static>)>) -> Properties {
113 entries
114 .into_iter()
115 .map(|(k, v)| (k.to_owned(), OwnedValue::try_from(v).unwrap()))
116 .collect()
117 }
118
119 #[test]
120 fn paired_device_from_properties() {
121 let props = properties(vec![
122 ("Address", Value::from("AA:BB:CC:DD:EE:FF")),
123 ("Name", Value::from("realme Buds T310")),
124 ("Alias", Value::from("my buds")),
125 ("Paired", Value::from(true)),
126 ("Connected", Value::from(true)),
127 ("UUIDs", Value::from(vec!["0000110B-0000-1000-8000-00805F9B34FB"])),
128 ]);
129 let device = device(&props).unwrap();
130 assert_eq!(device.address.to_string(), "AA:BB:CC:DD:EE:FF");
131 assert_eq!(device.name, "realme Buds T310");
132 assert_eq!(device.alias, "my buds");
133 assert!(device.connected);
134 assert_eq!(device.uuids, ["0000110b-0000-1000-8000-00805f9b34fb"]);
135 }
136
137 #[test]
138 fn unpaired_or_unaddressed_devices_are_skipped() {
139 let unpaired = properties(vec![
140 ("Address", Value::from("AA:BB:CC:DD:EE:FF")),
141 ("Paired", Value::from(false)),
142 ]);
143 assert!(device(&unpaired).is_none());
144 let no_address = properties(vec![("Paired", Value::from(true))]);
145 assert!(device(&no_address).is_none());
146 }
147
148 #[test]
149 fn alias_falls_back_to_name() {
150 let props = properties(vec![
151 ("Address", Value::from("AA:BB:CC:DD:EE:FF")),
152 ("Name", Value::from("realme Buds T310")),
153 ("Paired", Value::from(true)),
154 ]);
155 let device = device(&props).unwrap();
156 assert_eq!(device.alias, "realme Buds T310");
157 assert!(!device.connected);
158 }
159}