kobo_core/device/bt/
discover.rs1use std::fs;
7use std::sync::Mutex;
8
9use super::{dbus_cmd, default_bt_device, DBUS_OBJECT_MANAGER};
10
11pub struct PairedDevice {
12 pub path: String,
13 pub connected: bool,
14}
15
16static CACHED_BT_DEVICE: Mutex<Option<String>> = Mutex::new(None);
17
18static LAST_OK_DEVICE: Mutex<Option<String>> = Mutex::new(None);
19
20fn get_cached_bt_device() -> Option<String> {
21 CACHED_BT_DEVICE.lock().ok().and_then(|g| g.clone())
22}
23
24pub(super) fn set_cached_bt_device(dev: &str) {
25 if let Ok(mut guard) = CACHED_BT_DEVICE.lock() {
26 *guard = Some(dev.to_string());
27 }
28}
29
30pub(super) fn clear_cached_bt_device() {
31 if let Ok(mut guard) = CACHED_BT_DEVICE.lock() {
32 *guard = None;
33 }
34}
35
36pub(super) fn set_last_ok_device(dev: &str) {
37 if let Ok(mut guard) = LAST_OK_DEVICE.lock() {
38 *guard = Some(dev.to_string());
39 }
40}
41
42fn get_last_ok_device() -> Option<String> {
43 LAST_OK_DEVICE.lock().ok().and_then(|g| g.clone())
44}
45
46fn parse_managed_objects(text: &str) -> Vec<PairedDevice> {
47 let mut result: Vec<PairedDevice> = Vec::new();
48 let mut current_path: Option<String> = None;
49 let mut in_device_iface = false;
50 let mut is_paired = false;
51 let mut is_connected = false;
52 let mut expect_paired_val = false;
53 let mut expect_connected_val = false;
54 for line in text.lines() {
55 let t = line.trim();
56 if let Some(p) = t
57 .strip_prefix("object path \"")
58 .and_then(|s| s.strip_suffix('"'))
59 {
60 if in_device_iface && is_paired {
61 if let Some(path) = current_path.take() {
62 result.push(PairedDevice {
63 path,
64 connected: is_connected,
65 });
66 }
67 }
68 current_path = Some(p.to_string());
69 in_device_iface = false;
70 is_paired = false;
71 is_connected = false;
72 expect_paired_val = false;
73 expect_connected_val = false;
74 continue;
75 }
76 if t == "string \"org.bluez.Device1\"" {
77 in_device_iface = true;
78 continue;
79 }
80 if t == "string \"Paired\"" {
81 expect_paired_val = true;
82 continue;
83 }
84 if t == "string \"Connected\"" {
85 expect_connected_val = true;
86 continue;
87 }
88 if expect_paired_val {
89 expect_paired_val = false;
90 if t.contains("boolean true") && in_device_iface {
91 is_paired = true;
92 }
93 } else if expect_connected_val {
94 expect_connected_val = false;
95 if t.contains("boolean true") && in_device_iface {
96 is_connected = true;
97 }
98 }
99 }
100 if in_device_iface && is_paired {
101 if let Some(path) = current_path.take() {
102 result.push(PairedDevice {
103 path,
104 connected: is_connected,
105 });
106 }
107 }
108 result
109}
110
111pub fn discover_paired_devices() -> Vec<PairedDevice> {
112 let out = match dbus_cmd().args(["/", DBUS_OBJECT_MANAGER]).output() {
113 Ok(o) => o,
114 Err(_) => return Vec::new(),
115 };
116 let text = String::from_utf8_lossy(&out.stdout);
117 let result = parse_managed_objects(&text);
118 result
119}
120
121pub(super) fn discover_connected_paired_device() -> Option<String> {
122 discover_paired_devices()
123 .into_iter()
124 .find(|d| d.connected)
125 .map(|d| d.path)
126}
127
128pub fn bt_target_device() -> Option<String> {
133 if let Some(d) = get_cached_bt_device() {
134 return Some(d);
135 }
136 let devices = discover_paired_devices();
137 let found = devices
138 .iter()
139 .find(|d| d.connected)
140 .map(|d| d.path.clone())
141 .or_else(|| get_last_ok_device())
142 .or_else(|| {
143 fs::read_to_string(crate::device::paths::BT_CONFIG_FILE)
144 .ok()
145 .and_then(|c| default_bt_device(&c))
146 })
147 .or_else(|| devices.first().map(|d| d.path.clone()))?;
148 set_cached_bt_device(&found);
149 Some(found)
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn parse_managed_objects_finds_paired_with_connected() {
158 let dbus_output = "\
159object path \"/org/bluez/hci0\"\n\
160 dict entry(\n\
161 string \"org.bluez.Adapter1\"\n\
162 dict entry(\n\
163 string \"Powered\"\n\
164 variant boolean true\n\
165 )\n\
166 )\n\
167object path \"/org/bluez/hci0/dev_11_22_33_44_55_66\"\n\
168 dict entry(\n\
169 string \"org.bluez.Device1\"\n\
170 dict entry(\n\
171 string \"Paired\"\n\
172 variant boolean true\n\
173 )\n\
174 dict entry(\n\
175 string \"Connected\"\n\
176 variant boolean true\n\
177 )\n\
178 )\n\
179object path \"/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF\"\n\
180 dict entry(\n\
181 string \"org.bluez.Device1\"\n\
182 dict entry(\n\
183 string \"Paired\"\n\
184 variant boolean true\n\
185 )\n\
186 dict entry(\n\
187 string \"Connected\"\n\
188 variant boolean false\n\
189 )\n\
190 )\n";
191 let devices = parse_managed_objects(dbus_output);
192 assert_eq!(devices.len(), 2);
193 assert_eq!(devices[0].path, "/org/bluez/hci0/dev_11_22_33_44_55_66");
194 assert!(devices[0].connected);
195 assert_eq!(devices[1].path, "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF");
196 assert!(!devices[1].connected);
197 }
198
199 #[test]
200 fn parse_managed_objects_skips_unpaired() {
201 let dbus_output = "\
202object path \"/org/bluez/hci0/dev_FF_EE_DD_CC_BB_AA\"\n\
203 dict entry(\n\
204 string \"org.bluez.Device1\"\n\
205 dict entry(\n\
206 string \"Paired\"\n\
207 variant boolean false\n\
208 )\n\
209 dict entry(\n\
210 string \"Connected\"\n\
211 variant boolean true\n\
212 )\n\
213 )\n";
214 let devices = parse_managed_objects(dbus_output);
215 assert!(devices.is_empty());
216 }
217
218 #[test]
219 fn parse_managed_objects_empty_input() {
220 assert!(parse_managed_objects("").is_empty());
221 }
222}