1#[cfg(test)]
4mod tests;
5
6use std::cell::RefCell;
7use std::collections::{BTreeMap, HashMap};
8use std::rc::Rc;
9use std::sync::Once;
10
11use crabshell_reactive::{ReadSignal, Signal};
12use crabshell_ui::Cx;
13use zbus::{Connection, Message, ObjectPath, OwnedObjectPath, Variant};
14
15use crate::Shared;
16use crate::dbus::{self, CALL_TIMEOUT, Daemon, OBJECT_MANAGER, PROPERTIES, Props, set_changed};
17
18const BLUEZ: &str = "org.bluez";
19const ADAPTER: &str = "org.bluez.Adapter1";
20const DEVICE: &str = "org.bluez.Device1";
21const BATTERY: &str = "org.bluez.Battery1";
22
23const MAX_ADAPTERS: usize = 16;
25const MAX_DEVICES: usize = 256;
27const MAX_OBJECTS: usize = 4096;
29const SLOW_CALL: std::time::Duration = std::time::Duration::from_secs(90);
32
33fn read(interface: &str, name: &str) -> bool {
37 match interface {
38 ADAPTER => matches!(name, "Alias" | "Address" | "Powered" | "Discovering"),
39 DEVICE => matches!(
40 name,
41 "Address" | "Alias" | "Name" | "Icon" | "Connected" | "Paired" | "Trusted" | "Adapter"
42 ),
43 BATTERY => name == "Percentage",
44 _ => false,
45 }
46}
47
48#[derive(Clone, Debug, PartialEq)]
50pub struct BluetoothAdapter {
51 pub path: String,
53 pub name: String,
55 pub address: String,
57 pub powered: bool,
59 pub discovering: bool,
61}
62
63impl BluetoothAdapter {
64 fn from_props(path: String, props: &Props) -> BluetoothAdapter {
65 BluetoothAdapter {
66 path,
67 name: dbus::text(props, "Alias"),
68 address: dbus::text(props, "Address"),
69 powered: dbus::get(props, "Powered").unwrap_or(false),
70 discovering: dbus::get(props, "Discovering").unwrap_or(false),
71 }
72 }
73}
74
75#[derive(Clone, Debug, PartialEq)]
77pub struct BluetoothDevice {
78 pub path: String,
80 pub adapter: String,
82 pub address: String,
84 pub name: String,
86 pub icon: String,
88 pub connected: bool,
90 pub paired: bool,
92 pub trusted: bool,
94 pub battery: Option<u8>,
96}
97
98impl BluetoothDevice {
99 fn from_props(path: String, props: &Props, battery: Option<&Props>) -> BluetoothDevice {
100 let address = dbus::text(props, "Address");
101 let name = [dbus::text(props, "Alias"), dbus::text(props, "Name")]
102 .into_iter()
103 .find(|name| !name.is_empty())
104 .unwrap_or_else(|| address.clone());
105 BluetoothDevice {
106 path,
107 adapter: dbus::get::<&ObjectPath>(props, "Adapter")
108 .map_or(String::new(), |at| at.to_string()),
109 address,
110 name,
111 icon: dbus::text(props, "Icon"),
112 connected: dbus::get(props, "Connected").unwrap_or(false),
113 paired: dbus::get(props, "Paired").unwrap_or(false),
114 trusted: dbus::get(props, "Trusted").unwrap_or(false),
115 battery: battery
116 .and_then(|props| dbus::get::<u8>(props, "Percentage"))
117 .map(|percent| percent.min(100)),
118 }
119 }
120}
121
122#[derive(Clone)]
124pub struct Bluetooth {
125 service: Option<Rc<BluetoothService>>,
126 adapters: ReadSignal<Vec<BluetoothAdapter>>,
127 devices: ReadSignal<Vec<BluetoothDevice>>,
128}
129
130impl Bluetooth {
131 pub fn adapters(&self) -> ReadSignal<Vec<BluetoothAdapter>> {
133 self.adapters
134 }
135
136 pub fn devices(&self) -> ReadSignal<Vec<BluetoothDevice>> {
139 self.devices
140 }
141
142 pub fn set_powered(&self, adapter: &str, powered: bool) {
144 let body = (ADAPTER, "Powered", Variant::from(powered));
145 self.call(adapter, PROPERTIES, "Set", body);
146 }
147
148 pub fn start_discovery(&self, adapter: &str) {
154 self.call(adapter, ADAPTER, "StartDiscovery", ());
155 }
156
157 pub fn stop_discovery(&self, adapter: &str) {
159 self.call(adapter, ADAPTER, "StopDiscovery", ());
160 }
161
162 pub fn connect(&self, device: &str) {
164 self.call(device, DEVICE, "Connect", ());
165 }
166
167 pub fn disconnect(&self, device: &str) {
169 self.call(device, DEVICE, "Disconnect", ());
170 }
171
172 pub fn pair(&self, device: &str) {
177 self.call(device, DEVICE, "Pair", ());
178 }
179
180 pub fn set_trusted(&self, device: &str, trusted: bool) {
182 let body = (DEVICE, "Trusted", Variant::from(trusted));
183 self.call(device, PROPERTIES, "Set", body);
184 }
185
186 pub fn forget(&self, device: &str) {
188 let adapter = self.service.as_ref().and_then(|service| {
189 let state = service.state.borrow();
190 let object = state.as_ref()?.objects.get(device)?;
191 dbus::get::<&ObjectPath>(object.device.as_ref()?, "Adapter").map(|at| at.to_string())
192 });
193 let (Some(adapter), Ok(path)) = (adapter, ObjectPath::try_from(device.to_owned())) else {
194 return tracing::info!(device, "no such Bluetooth device; nothing forgotten");
195 };
196 self.call(&adapter, ADAPTER, "RemoveDevice", path);
197 }
198
199 fn call<B>(&self, path: &str, interface: &'static str, method: &'static str, body: B)
201 where
202 B: serde::Serialize + zbus::DynamicType + 'static,
203 {
204 let Some(service) = &self.service else {
205 return;
206 };
207 let Some(connection) = service
208 .state
209 .borrow()
210 .as_ref()
211 .and_then(|state| state.connection.clone())
212 else {
213 return tracing::info!(method, "BlueZ isn't running; nothing sent");
214 };
215 let (shared, path) = (Rc::clone(&service.shared), path.to_owned());
216 service.shared.spawn(async move {
217 let call = connection.call_method(Some(BLUEZ), path.as_str(), Some(interface), method, &body);
218 let limit = if matches!(method, "Pair" | "Connect") {
219 SLOW_CALL
220 } else {
221 CALL_TIMEOUT
222 };
223 if let Err(error) = shared.timeout(limit, call).await {
224 tracing::warn!(%error, path, method, "BlueZ refused it");
225 }
226 });
227 }
228}
229
230pub(crate) fn bluetooth(cx: &Cx) -> Bluetooth {
232 let Some(service) = cx.service::<BluetoothService>() else {
233 static LOGGED: Once = Once::new();
234 LOGGED.call_once(|| {
235 tracing::warn!("cx.bluetooth() needs the services the shell provides; it stays empty");
236 });
237 return Bluetooth {
238 service: None,
239 adapters: cx.signal(Vec::new()).read_only(),
240 devices: cx.signal(Vec::new()).read_only(),
241 };
242 };
243 service.start()
244}
245
246pub(crate) struct BluetoothService {
248 shared: Rc<Shared>,
249 state: RefCell<Option<BluetoothState>>,
250}
251
252struct BluetoothState {
253 adapters: Signal<Vec<BluetoothAdapter>>,
254 devices: Signal<Vec<BluetoothDevice>>,
255 objects: BTreeMap<String, Object>,
257 connection: Option<Connection>,
259 generation: u64,
260}
261
262#[derive(Default)]
265struct Object {
266 adapter: Option<Props>,
267 device: Option<Props>,
268 battery: Option<Props>,
269}
270
271impl Object {
272 fn slot(&mut self, interface: &str) -> Option<&mut Option<Props>> {
275 match interface {
276 ADAPTER => Some(&mut self.adapter),
277 DEVICE => Some(&mut self.device),
278 BATTERY => Some(&mut self.battery),
279 _ => None,
280 }
281 }
282}
283
284fn add(objects: &mut BTreeMap<String, Object>, path: String, interfaces: HashMap<String, Props>) {
287 if objects.len() >= MAX_OBJECTS && !objects.contains_key(&path) {
288 return tracing::debug!(path, "too many Bluetooth objects; leaving one out");
289 }
290 let mut object = objects.remove(&path).unwrap_or_default();
291 for (interface, props) in interfaces {
292 if let Some(slot) = object.slot(&interface) {
293 *slot = Some(props);
294 }
295 }
296 if object.adapter.is_some() || object.device.is_some() {
297 objects.insert(path, object);
298 }
299}
300
301impl BluetoothService {
302 pub(crate) fn new(shared: Rc<Shared>) -> BluetoothService {
303 BluetoothService {
304 shared,
305 state: RefCell::new(None),
306 }
307 }
308
309 fn start(self: &Rc<Self>) -> Bluetooth {
310 if let Some(state) = &*self.state.borrow() {
311 return Bluetooth {
312 service: Some(Rc::clone(self)),
313 adapters: state.adapters.read_only(),
314 devices: state.devices.read_only(),
315 };
316 }
317 let root = &self.shared.root;
318 let (adapters, devices) = (root.signal(Vec::new()), root.signal(Vec::new()));
319 *self.state.borrow_mut() = Some(BluetoothState {
320 adapters,
321 devices,
322 objects: BTreeMap::new(),
323 connection: None,
324 generation: 0,
325 });
326 match self.shared.config.system_bus.clone() {
327 Some(address) => {
328 let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
329 self.shared.spawn(follow);
330 }
331 None => tracing::info!("no system bus, so there is no Bluetooth"),
332 }
333 Bluetooth {
334 service: Some(Rc::clone(self)),
335 adapters: adapters.read_only(),
336 devices: devices.read_only(),
337 }
338 }
339
340 fn publish(&self) {
342 let shown = self.state.borrow().as_ref().map(|state| {
344 let objects = || {
345 state
346 .objects
347 .iter()
348 .map(|(path, object)| (path.clone(), object))
349 };
350 let adapters = objects()
351 .filter_map(|(path, object)| {
352 Some(BluetoothAdapter::from_props(path, object.adapter.as_ref()?))
353 })
354 .take(MAX_ADAPTERS)
355 .collect::<Vec<_>>();
356 let mut devices = objects()
357 .filter_map(|(path, object)| {
358 let device = object.device.as_ref()?;
359 Some(BluetoothDevice::from_props(
360 path,
361 device,
362 object.battery.as_ref(),
363 ))
364 })
365 .collect::<Vec<_>>();
366 if devices.len() > MAX_DEVICES {
369 devices.sort_by_key(|device| (!device.connected, !device.paired));
370 devices.truncate(MAX_DEVICES);
371 devices.sort_by(|a, b| a.path.cmp(&b.path));
372 }
373 (state.adapters, adapters, state.devices, devices)
374 });
375 if let Some((adapters_signal, adapters, devices_signal, devices)) = shown {
376 set_changed(adapters_signal, adapters);
377 set_changed(devices_signal, devices);
378 }
379 }
380}
381
382impl Daemon for BluetoothService {
383 const NAME: &'static str = BLUEZ;
384
385 fn rules() -> Vec<String> {
389 let mut rules = vec![format!(
390 "type='signal',sender='{BLUEZ}',interface='{OBJECT_MANAGER}'"
391 )];
392 rules.extend([ADAPTER, DEVICE, BATTERY].map(|interface| {
393 format!(
394 "type='signal',sender='{BLUEZ}',interface='{PROPERTIES}',member='PropertiesChanged',\
395 arg0='{interface}'"
396 )
397 }));
398 rules
399 }
400
401 fn alive(&self) -> bool {
402 let state = self.state.borrow();
403 state
404 .as_ref()
405 .is_some_and(|state| state.adapters.try_with(|_| ()).is_some())
406 }
407
408 fn clear(&self) -> u64 {
409 let generation = {
410 let mut state = self.state.borrow_mut();
411 let Some(state) = state.as_mut() else {
412 return 0;
413 };
414 state.objects.clear();
415 state.connection = None;
416 state.generation += 1;
417 state.generation
418 };
419 self.publish();
420 generation
421 }
422
423 async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
424 let call = connection.call_method(
425 Some(BLUEZ),
426 "/",
427 Some(OBJECT_MANAGER),
428 "GetManagedObjects",
429 &(),
430 );
431 let read = async {
432 let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
433 reply
434 .body()
435 .deserialize::<HashMap<OwnedObjectPath, HashMap<String, Props>>>()
436 };
437 let managed = match read.await {
438 Ok(managed) => managed,
439 Err(error) => return tracing::warn!(%error, "couldn't read BlueZ"),
440 };
441 let managed = managed
443 .into_iter()
444 .map(|(path, interfaces)| (path.as_str().to_owned(), interfaces))
445 .collect::<BTreeMap<_, _>>();
446 {
447 let mut state = self.state.borrow_mut();
448 let Some(state) = state
449 .as_mut()
450 .filter(|state| state.generation == generation)
451 else {
452 return;
453 };
454 state.objects.clear();
455 for (path, interfaces) in managed {
456 add(&mut state.objects, path, interfaces);
457 }
458 state.connection = Some(connection);
459 }
460 self.publish();
461 }
462
463 fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
464 if let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) {
465 {
466 let mut state = self.state.borrow_mut();
467 let props = state
468 .as_mut()
469 .and_then(|state| state.objects.get_mut(&path))
470 .and_then(|object| object.slot(&interface)?.as_mut());
471 let Some(props) = props else {
472 return;
473 };
474 let shown = changed
475 .keys()
476 .chain(&dropped)
477 .any(|name| read(&interface, name));
478 dbus::apply(props, changed, dropped);
479 if !shown {
480 return;
481 }
482 }
483 return self.publish();
484 }
485 let header = message.header();
486 if header.interface().map(|interface| interface.as_str()) != Some(OBJECT_MANAGER) {
487 return;
488 }
489 let body = message.body();
490 {
491 let mut state = self.state.borrow_mut();
492 let Some(state) = state.as_mut() else {
493 return;
494 };
495 match header.member().map(|member| member.as_str()) {
496 Some("InterfacesAdded") => {
497 let Ok((path, interfaces)) =
498 body.deserialize::<(OwnedObjectPath, HashMap<String, Props>)>()
499 else {
500 return;
501 };
502 add(&mut state.objects, path.as_str().to_owned(), interfaces);
503 }
504 Some("InterfacesRemoved") => {
505 let Ok((path, interfaces)) = body.deserialize::<(OwnedObjectPath, Vec<String>)>() else {
506 return;
507 };
508 let Some(object) = state.objects.get_mut(path.as_str()) else {
509 return;
510 };
511 for interface in interfaces {
512 if let Some(slot) = object.slot(&interface) {
513 *slot = None;
514 }
515 }
516 if object.adapter.is_none() && object.device.is_none() {
517 state.objects.remove(path.as_str());
518 }
519 }
520 _ => return,
521 }
522 }
523 self.publish();
524 }
525}