#[cfg(test)]
mod tests;
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap};
use std::rc::Rc;
use std::sync::Once;
use crabshell_reactive::{ReadSignal, Signal};
use crabshell_ui::Cx;
use zbus::{Connection, Message, ObjectPath, OwnedObjectPath, Variant};
use crate::Shared;
use crate::dbus::{self, CALL_TIMEOUT, Daemon, OBJECT_MANAGER, PROPERTIES, Props, set_changed};
const BLUEZ: &str = "org.bluez";
const ADAPTER: &str = "org.bluez.Adapter1";
const DEVICE: &str = "org.bluez.Device1";
const BATTERY: &str = "org.bluez.Battery1";
const MAX_ADAPTERS: usize = 16;
const MAX_DEVICES: usize = 256;
const MAX_OBJECTS: usize = 4096;
const SLOW_CALL: std::time::Duration = std::time::Duration::from_secs(90);
fn read(interface: &str, name: &str) -> bool {
match interface {
ADAPTER => matches!(name, "Alias" | "Address" | "Powered" | "Discovering"),
DEVICE => matches!(
name,
"Address" | "Alias" | "Name" | "Icon" | "Connected" | "Paired" | "Trusted" | "Adapter"
),
BATTERY => name == "Percentage",
_ => false,
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BluetoothAdapter {
pub path: String,
pub name: String,
pub address: String,
pub powered: bool,
pub discovering: bool,
}
impl BluetoothAdapter {
fn from_props(path: String, props: &Props) -> BluetoothAdapter {
BluetoothAdapter {
path,
name: dbus::text(props, "Alias"),
address: dbus::text(props, "Address"),
powered: dbus::get(props, "Powered").unwrap_or(false),
discovering: dbus::get(props, "Discovering").unwrap_or(false),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BluetoothDevice {
pub path: String,
pub adapter: String,
pub address: String,
pub name: String,
pub icon: String,
pub connected: bool,
pub paired: bool,
pub trusted: bool,
pub battery: Option<u8>,
}
impl BluetoothDevice {
fn from_props(path: String, props: &Props, battery: Option<&Props>) -> BluetoothDevice {
let address = dbus::text(props, "Address");
let name = [dbus::text(props, "Alias"), dbus::text(props, "Name")]
.into_iter()
.find(|name| !name.is_empty())
.unwrap_or_else(|| address.clone());
BluetoothDevice {
path,
adapter: dbus::get::<&ObjectPath>(props, "Adapter")
.map_or(String::new(), |at| at.to_string()),
address,
name,
icon: dbus::text(props, "Icon"),
connected: dbus::get(props, "Connected").unwrap_or(false),
paired: dbus::get(props, "Paired").unwrap_or(false),
trusted: dbus::get(props, "Trusted").unwrap_or(false),
battery: battery
.and_then(|props| dbus::get::<u8>(props, "Percentage"))
.map(|percent| percent.min(100)),
}
}
}
#[derive(Clone)]
pub struct Bluetooth {
service: Option<Rc<BluetoothService>>,
adapters: ReadSignal<Vec<BluetoothAdapter>>,
devices: ReadSignal<Vec<BluetoothDevice>>,
}
impl Bluetooth {
pub fn adapters(&self) -> ReadSignal<Vec<BluetoothAdapter>> {
self.adapters
}
pub fn devices(&self) -> ReadSignal<Vec<BluetoothDevice>> {
self.devices
}
pub fn set_powered(&self, adapter: &str, powered: bool) {
let body = (ADAPTER, "Powered", Variant::from(powered));
self.call(adapter, PROPERTIES, "Set", body);
}
pub fn start_discovery(&self, adapter: &str) {
self.call(adapter, ADAPTER, "StartDiscovery", ());
}
pub fn stop_discovery(&self, adapter: &str) {
self.call(adapter, ADAPTER, "StopDiscovery", ());
}
pub fn connect(&self, device: &str) {
self.call(device, DEVICE, "Connect", ());
}
pub fn disconnect(&self, device: &str) {
self.call(device, DEVICE, "Disconnect", ());
}
pub fn pair(&self, device: &str) {
self.call(device, DEVICE, "Pair", ());
}
pub fn set_trusted(&self, device: &str, trusted: bool) {
let body = (DEVICE, "Trusted", Variant::from(trusted));
self.call(device, PROPERTIES, "Set", body);
}
pub fn forget(&self, device: &str) {
let adapter = self.service.as_ref().and_then(|service| {
let state = service.state.borrow();
let object = state.as_ref()?.objects.get(device)?;
dbus::get::<&ObjectPath>(object.device.as_ref()?, "Adapter").map(|at| at.to_string())
});
let (Some(adapter), Ok(path)) = (adapter, ObjectPath::try_from(device.to_owned())) else {
return tracing::info!(device, "no such Bluetooth device; nothing forgotten");
};
self.call(&adapter, ADAPTER, "RemoveDevice", path);
}
fn call<B>(&self, path: &str, interface: &'static str, method: &'static str, body: B)
where
B: serde::Serialize + zbus::DynamicType + 'static,
{
let Some(service) = &self.service else {
return;
};
let Some(connection) = service
.state
.borrow()
.as_ref()
.and_then(|state| state.connection.clone())
else {
return tracing::info!(method, "BlueZ isn't running; nothing sent");
};
let (shared, path) = (Rc::clone(&service.shared), path.to_owned());
service.shared.spawn(async move {
let call = connection.call_method(Some(BLUEZ), path.as_str(), Some(interface), method, &body);
let limit = if matches!(method, "Pair" | "Connect") {
SLOW_CALL
} else {
CALL_TIMEOUT
};
if let Err(error) = shared.timeout(limit, call).await {
tracing::warn!(%error, path, method, "BlueZ refused it");
}
});
}
}
pub(crate) fn bluetooth(cx: &Cx) -> Bluetooth {
let Some(service) = cx.service::<BluetoothService>() else {
static LOGGED: Once = Once::new();
LOGGED.call_once(|| {
tracing::warn!("cx.bluetooth() needs the services the shell provides; it stays empty");
});
return Bluetooth {
service: None,
adapters: cx.signal(Vec::new()).read_only(),
devices: cx.signal(Vec::new()).read_only(),
};
};
service.start()
}
pub(crate) struct BluetoothService {
shared: Rc<Shared>,
state: RefCell<Option<BluetoothState>>,
}
struct BluetoothState {
adapters: Signal<Vec<BluetoothAdapter>>,
devices: Signal<Vec<BluetoothDevice>>,
objects: BTreeMap<String, Object>,
connection: Option<Connection>,
generation: u64,
}
#[derive(Default)]
struct Object {
adapter: Option<Props>,
device: Option<Props>,
battery: Option<Props>,
}
impl Object {
fn slot(&mut self, interface: &str) -> Option<&mut Option<Props>> {
match interface {
ADAPTER => Some(&mut self.adapter),
DEVICE => Some(&mut self.device),
BATTERY => Some(&mut self.battery),
_ => None,
}
}
}
fn add(objects: &mut BTreeMap<String, Object>, path: String, interfaces: HashMap<String, Props>) {
if objects.len() >= MAX_OBJECTS && !objects.contains_key(&path) {
return tracing::debug!(path, "too many Bluetooth objects; leaving one out");
}
let mut object = objects.remove(&path).unwrap_or_default();
for (interface, props) in interfaces {
if let Some(slot) = object.slot(&interface) {
*slot = Some(props);
}
}
if object.adapter.is_some() || object.device.is_some() {
objects.insert(path, object);
}
}
impl BluetoothService {
pub(crate) fn new(shared: Rc<Shared>) -> BluetoothService {
BluetoothService {
shared,
state: RefCell::new(None),
}
}
fn start(self: &Rc<Self>) -> Bluetooth {
if let Some(state) = &*self.state.borrow() {
return Bluetooth {
service: Some(Rc::clone(self)),
adapters: state.adapters.read_only(),
devices: state.devices.read_only(),
};
}
let root = &self.shared.root;
let (adapters, devices) = (root.signal(Vec::new()), root.signal(Vec::new()));
*self.state.borrow_mut() = Some(BluetoothState {
adapters,
devices,
objects: BTreeMap::new(),
connection: None,
generation: 0,
});
match self.shared.config.system_bus.clone() {
Some(address) => {
let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
self.shared.spawn(follow);
}
None => tracing::info!("no system bus, so there is no Bluetooth"),
}
Bluetooth {
service: Some(Rc::clone(self)),
adapters: adapters.read_only(),
devices: devices.read_only(),
}
}
fn publish(&self) {
let shown = self.state.borrow().as_ref().map(|state| {
let objects = || {
state
.objects
.iter()
.map(|(path, object)| (path.clone(), object))
};
let adapters = objects()
.filter_map(|(path, object)| {
Some(BluetoothAdapter::from_props(path, object.adapter.as_ref()?))
})
.take(MAX_ADAPTERS)
.collect::<Vec<_>>();
let mut devices = objects()
.filter_map(|(path, object)| {
let device = object.device.as_ref()?;
Some(BluetoothDevice::from_props(
path,
device,
object.battery.as_ref(),
))
})
.collect::<Vec<_>>();
if devices.len() > MAX_DEVICES {
devices.sort_by_key(|device| (!device.connected, !device.paired));
devices.truncate(MAX_DEVICES);
devices.sort_by(|a, b| a.path.cmp(&b.path));
}
(state.adapters, adapters, state.devices, devices)
});
if let Some((adapters_signal, adapters, devices_signal, devices)) = shown {
set_changed(adapters_signal, adapters);
set_changed(devices_signal, devices);
}
}
}
impl Daemon for BluetoothService {
const NAME: &'static str = BLUEZ;
fn rules() -> Vec<String> {
let mut rules = vec![format!(
"type='signal',sender='{BLUEZ}',interface='{OBJECT_MANAGER}'"
)];
rules.extend([ADAPTER, DEVICE, BATTERY].map(|interface| {
format!(
"type='signal',sender='{BLUEZ}',interface='{PROPERTIES}',member='PropertiesChanged',\
arg0='{interface}'"
)
}));
rules
}
fn alive(&self) -> bool {
let state = self.state.borrow();
state
.as_ref()
.is_some_and(|state| state.adapters.try_with(|_| ()).is_some())
}
fn clear(&self) -> u64 {
let generation = {
let mut state = self.state.borrow_mut();
let Some(state) = state.as_mut() else {
return 0;
};
state.objects.clear();
state.connection = None;
state.generation += 1;
state.generation
};
self.publish();
generation
}
async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
let call = connection.call_method(
Some(BLUEZ),
"/",
Some(OBJECT_MANAGER),
"GetManagedObjects",
&(),
);
let read = async {
let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
reply
.body()
.deserialize::<HashMap<OwnedObjectPath, HashMap<String, Props>>>()
};
let managed = match read.await {
Ok(managed) => managed,
Err(error) => return tracing::warn!(%error, "couldn't read BlueZ"),
};
let managed = managed
.into_iter()
.map(|(path, interfaces)| (path.as_str().to_owned(), interfaces))
.collect::<BTreeMap<_, _>>();
{
let mut state = self.state.borrow_mut();
let Some(state) = state
.as_mut()
.filter(|state| state.generation == generation)
else {
return;
};
state.objects.clear();
for (path, interfaces) in managed {
add(&mut state.objects, path, interfaces);
}
state.connection = Some(connection);
}
self.publish();
}
fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
if let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) {
{
let mut state = self.state.borrow_mut();
let props = state
.as_mut()
.and_then(|state| state.objects.get_mut(&path))
.and_then(|object| object.slot(&interface)?.as_mut());
let Some(props) = props else {
return;
};
let shown = changed
.keys()
.chain(&dropped)
.any(|name| read(&interface, name));
dbus::apply(props, changed, dropped);
if !shown {
return;
}
}
return self.publish();
}
let header = message.header();
if header.interface().map(|interface| interface.as_str()) != Some(OBJECT_MANAGER) {
return;
}
let body = message.body();
{
let mut state = self.state.borrow_mut();
let Some(state) = state.as_mut() else {
return;
};
match header.member().map(|member| member.as_str()) {
Some("InterfacesAdded") => {
let Ok((path, interfaces)) =
body.deserialize::<(OwnedObjectPath, HashMap<String, Props>)>()
else {
return;
};
add(&mut state.objects, path.as_str().to_owned(), interfaces);
}
Some("InterfacesRemoved") => {
let Ok((path, interfaces)) = body.deserialize::<(OwnedObjectPath, Vec<String>)>() else {
return;
};
let Some(object) = state.objects.get_mut(path.as_str()) else {
return;
};
for interface in interfaces {
if let Some(slot) = object.slot(&interface) {
*slot = None;
}
}
if object.adapter.is_none() && object.device.is_none() {
state.objects.remove(path.as_str());
}
}
_ => return,
}
}
self.publish();
}
}