diff --git a/Cargo.toml b/Cargo.toml index 58a574e..fe90c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "switcheroo-control", "timedate", "upower", + "bluez", ] [workspace.dependencies] diff --git a/bluez/Cargo.toml b/bluez/Cargo.toml new file mode 100644 index 0000000..a531931 --- /dev/null +++ b/bluez/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bluez-zbus" +version = "0.1.0" +description = "dbus bindings for org.bluez with zbus" +repository = "https://github.com/pop-os/dbus-settings-bindings" +edition = "2021" +license = "MPL-2.0" +categories = ["os::linux-apis"] +keywords = ["dbus", "bluez", "zbus", "bluetooth"] + +[dependencies] +futures = "0.3.30" +zbus = "4.2.1" + +[dev-dependencies] +pico-args = "0.5.0" +tokio = { version = "1", features = ["full"] } diff --git a/bluez/examples/bluetoothctl.rs b/bluez/examples/bluetoothctl.rs new file mode 100644 index 0000000..e031e8c --- /dev/null +++ b/bluez/examples/bluetoothctl.rs @@ -0,0 +1,275 @@ +use std::process::ExitCode; + +use futures::StreamExt; + +#[tokio::main] +async fn main() -> Result> { + let connection = zbus::Connection::system().await?; + + let adapters = bluez_zbus::adapters(&connection).await?; + + // if adapters.is_empty() { + // eprintln!("No adapter found"); + // return Ok(ExitCode::FAILURE); + // } + + // if adapters.len() > 1 { + // eprintln!("More than one adapter found. Using the first one"); + // } + // let adapter = adapters.first().unwrap(); + + let mut parser = pico_args::Arguments::from_env(); + + match parser.subcommand()?.as_deref() { + Some("connected-devices") => match bluez_zbus::get_devices(&connection, None).await { + Err(why) => { + eprintln!("error: could not get devices: {why}"); + return Ok(ExitCode::FAILURE); + } + Ok(devices) => { + for proxy in devices { + if !proxy.device.connected().await? { + continue; + } + println!( + "{} ({})", + proxy + .device + .name() + .await + .unwrap_or(proxy.device.address().await.unwrap()), + proxy + .device + .inner() + .get_property::("Icon") + .await + .unwrap_or("unknown".to_owned()) + ); + } + } + }, + + Some("paired-devices") => match bluez_zbus::get_devices(&connection, None).await { + Err(why) => { + eprintln!("error: could not get devices: {why}"); + return Ok(ExitCode::FAILURE); + } + Ok(devices) => { + for proxy in devices { + if !proxy.device.paired().await? { + continue; + } + println!( + "{} ({})", + proxy + .device + .name() + .await + .unwrap_or(proxy.device.address().await.unwrap()), + proxy + .device + .inner() + .get_property::("Icon") + .await + .unwrap_or("unknown".to_owned()) + ); + } + } + }, + + Some("nearby-devices") => { + futures::future::join_all(adapters.iter().map(|adapter| adapter.start_discovery())) + .await; + match bluez_zbus::get_devices(&connection, None).await { + Err(why) => { + eprintln!("error: could not get devices: {why}"); + return Ok(ExitCode::FAILURE); + } + Ok(devices) => { + for proxy in devices { + if proxy.device.paired().await? { + continue; + } + println!( + "{} ({})", + proxy + .device + .name() + .await + .unwrap_or(proxy.device.address().await.unwrap()), + proxy + .device + .inner() + .get_property::("Icon") + .await + .unwrap_or("unknown".to_owned()) + ); + } + } + } + } + + Some("connect") => match parser.free_from_str::().ok().as_deref() { + Some(addr_or_alias) => match bluez_zbus::get_devices(&connection, None).await { + Err(why) => { + eprintln!("error: could not get devices: {why}"); + return Ok(ExitCode::FAILURE); + } + Ok(devices) => { + let devices: Vec = + futures::future::join_all(devices.into_iter().map(|proxy| async { + match (proxy.device.name().await, proxy.device.address().await) { + (Ok(alias), _) if alias == addr_or_alias => Some(proxy), + (_, Ok(addr)) if addr == addr_or_alias => Some(proxy), + _ => None, + } + })) + .await + .into_iter() + .flatten() + .collect(); + if devices.is_empty() { + eprintln!("No device found"); + return Ok(ExitCode::FAILURE); + } + + if devices.len() > 1 { + eprintln!("More than one one found. Use the address."); + return Ok(ExitCode::FAILURE); + } + let proxy = devices.first().unwrap(); + + if proxy.device.connected().await? { + eprintln!("Device already connected."); + return Ok(ExitCode::FAILURE); + } + + if let Err(why) = proxy.device.connect().await { + eprintln!("error: could not connect: {why}"); + return Ok(ExitCode::FAILURE); + } + } + }, + + None => { + eprintln!("error: device address or alias missing"); + return Ok(ExitCode::FAILURE); + } + }, + + Some("disconnect") => match parser.free_from_str::().ok().as_deref() { + Some(addr_or_alias) => match bluez_zbus::get_devices(&connection, None).await { + Err(why) => { + eprintln!("error: could not get devices: {why}"); + return Ok(ExitCode::FAILURE); + } + Ok(devices) => { + let devices: Vec = + futures::future::join_all(devices.into_iter().map(|proxy| async { + match (proxy.device.name().await, proxy.device.address().await) { + (Ok(alias), _) if alias == addr_or_alias => Some(proxy), + (_, Ok(addr)) if addr == addr_or_alias => Some(proxy), + _ => None, + } + })) + .await + .into_iter() + .flatten() + .collect(); + if devices.is_empty() { + eprintln!("No device found"); + return Ok(ExitCode::FAILURE); + } + + if devices.len() > 1 { + eprintln!("More than one one found. Use the address."); + return Ok(ExitCode::FAILURE); + } + let proxy = devices.first().unwrap(); + + if !proxy.device.connected().await? { + eprintln!("Device not connected."); + return Ok(ExitCode::FAILURE); + } + + if let Err(why) = proxy.device.disconnect().await { + eprintln!("error: could not disconnect: {why}"); + return Ok(ExitCode::FAILURE); + } + } + }, + + None => { + eprintln!("error: device address or alias missing"); + return Ok(ExitCode::FAILURE); + } + }, + + Some("forget") => match parser.free_from_str::().ok().as_deref() { + Some(addr_or_alias) => match bluez_zbus::get_devices(&connection, None).await { + Err(why) => { + eprintln!("error: could not get devices: {why}"); + return Ok(ExitCode::FAILURE); + } + Ok(devices) => { + let devices: Vec = + futures::future::join_all(devices.into_iter().map(|proxy| async { + match (proxy.device.name().await, proxy.device.address().await) { + (Ok(alias), _) if alias == addr_or_alias => Some(proxy), + (_, Ok(addr)) if addr == addr_or_alias => Some(proxy), + _ => None, + } + })) + .await + .into_iter() + .flatten() + .collect(); + if devices.is_empty() { + eprintln!("No device found"); + return Ok(ExitCode::FAILURE); + } + + if devices.len() > 1 { + eprintln!("More than one one found. Use the address."); + return Ok(ExitCode::FAILURE); + } + let proxy = devices.first().unwrap(); + + if !proxy.device.paired().await? { + eprintln!("Device not connected."); + return Ok(ExitCode::FAILURE); + } + + if proxy.device.connected().await? { + eprintln!("Cannot remove a connected proxy.device."); + return Ok(ExitCode::FAILURE); + } + } + }, + + None => { + eprintln!("error: device address or alias missing"); + return Ok(ExitCode::FAILURE); + } + }, + _ => print_help(), + } + + Ok(ExitCode::SUCCESS) +} + +fn print_help() { + println!( + "\ +bluetoothctl + +USAGE: + bluetoothctl connected-devices + bluetoothctl paired-devices + bluetoothctl nearby-devices + bluetoothctl connect ADDRESS + bluetoothctl disconnect ADDRESS + bluetoothctl forget ADDRESS +" + ); +} diff --git a/bluez/src/adapter1.rs b/bluez/src/adapter1.rs new file mode 100644 index 0000000..accbb2d --- /dev/null +++ b/bluez/src/adapter1.rs @@ -0,0 +1,119 @@ +//! # D-Bus interface proxy for: `org.bluez.Adapter1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy(interface = "org.bluez.Adapter1", default_service = "org.bluez")] +trait Adapter1 { + /// ConnectDevice method + fn connect_device( + &self, + properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// GetDiscoveryFilters method + fn get_discovery_filters(&self) -> zbus::Result>; + + /// RemoveDevice method + fn remove_device(&self, device: &zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// SetDiscoveryFilter method + fn set_discovery_filter( + &self, + properties: std::collections::HashMap<&str, &zbus::zvariant::Value<'_>>, + ) -> zbus::Result<()>; + + /// StartDiscovery method + fn start_discovery(&self) -> zbus::Result<()>; + + /// StopDiscovery method + fn stop_discovery(&self) -> zbus::Result<()>; + + /// Address property + #[zbus(property)] + fn address(&self) -> zbus::Result; + + /// AddressType property + #[zbus(property)] + fn address_type(&self) -> zbus::Result; + + /// Alias property + #[zbus(property)] + fn alias(&self) -> zbus::Result; + #[zbus(property)] + fn set_alias(&self, value: &str) -> zbus::Result<()>; + + /// Class property + #[zbus(property)] + fn class(&self) -> zbus::Result; + + /// Discoverable property + #[zbus(property)] + fn discoverable(&self) -> zbus::Result; + #[zbus(property)] + fn set_discoverable(&self, value: bool) -> zbus::Result<()>; + + /// DiscoverableTimeout property + #[zbus(property)] + fn discoverable_timeout(&self) -> zbus::Result; + #[zbus(property)] + fn set_discoverable_timeout(&self, value: u32) -> zbus::Result<()>; + + /// Discovering property + #[zbus(property)] + fn discovering(&self) -> zbus::Result; + + /// ExperimentalFeatures property + #[zbus(property)] + fn experimental_features(&self) -> zbus::Result>; + + /// Modalias property + #[zbus(property)] + fn modalias(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Pairable property + #[zbus(property)] + fn pairable(&self) -> zbus::Result; + #[zbus(property)] + fn set_pairable(&self, value: bool) -> zbus::Result<()>; + + /// PairableTimeout property + #[zbus(property)] + fn pairable_timeout(&self) -> zbus::Result; + #[zbus(property)] + fn set_pairable_timeout(&self, value: u32) -> zbus::Result<()>; + + /// Powered property + #[zbus(property)] + fn powered(&self) -> zbus::Result; + #[zbus(property)] + fn set_powered(&self, value: bool) -> zbus::Result<()>; + + /// Roles property + #[zbus(property)] + fn roles(&self) -> zbus::Result>; + + /// UUIDs property + #[zbus(property, name = "UUIDs")] + fn uuids(&self) -> zbus::Result>; +} diff --git a/bluez/src/battery1.rs b/bluez/src/battery1.rs new file mode 100644 index 0000000..5aed142 --- /dev/null +++ b/bluez/src/battery1.rs @@ -0,0 +1,31 @@ +//! # D-Bus interface proxy for: `org.bluez.Battery1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0/dev_14_3F_A6_A8_16_68' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy(interface = "org.bluez.Battery1", default_service = "org.bluez")] +trait Battery1 { + /// Percentage property + #[zbus(property)] + fn percentage(&self) -> zbus::Result; + + /// Source property + #[zbus(property)] + fn source(&self) -> zbus::Result; +} diff --git a/bluez/src/device1.rs b/bluez/src/device1.rs new file mode 100644 index 0000000..daf480e --- /dev/null +++ b/bluez/src/device1.rs @@ -0,0 +1,147 @@ +//! # D-Bus interface proxy for: `org.bluez.Device1` +//! +//! This code was generated by `zbus-xmlgen` `4.1.0` from D-Bus introspection data. +//! Source: `Interface '/org/bluez/hci0/dev_14_3F_A6_A8_16_68' from service 'org.bluez' on system bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the [Writing a client proxy] section of the zbus +//! documentation. +//! +//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the +//! following zbus API can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! Consequently `zbus-xmlgen` did not generate code for the above interfaces. +//! +//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html +//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, +use zbus::proxy; +#[proxy(interface = "org.bluez.Device1", default_service = "org.bluez")] +trait Device1 { + /// CancelPairing method + fn cancel_pairing(&self) -> zbus::Result<()>; + + /// Connect method + fn connect(&self) -> zbus::Result<()>; + + /// ConnectProfile method + fn connect_profile(&self, UUID: &str) -> zbus::Result<()>; + + /// Disconnect method + fn disconnect(&self) -> zbus::Result<()>; + + /// DisconnectProfile method + fn disconnect_profile(&self, UUID: &str) -> zbus::Result<()>; + + /// Pair method + fn pair(&self) -> zbus::Result<()>; + + /// Adapter property + #[zbus(property)] + fn adapter(&self) -> zbus::Result; + + /// Address property + #[zbus(property)] + fn address(&self) -> zbus::Result; + + /// AddressType property + #[zbus(property)] + fn address_type(&self) -> zbus::Result; + + /// AdvertisingData property + #[zbus(property)] + fn advertising_data( + &self, + ) -> zbus::Result>; + + /// AdvertisingFlags property + #[zbus(property)] + fn advertising_flags(&self) -> zbus::Result>; + + /// Alias property + #[zbus(property)] + fn alias(&self) -> zbus::Result; + #[zbus(property)] + fn set_alias(&self, value: &str) -> zbus::Result<()>; + + /// Appearance property + #[zbus(property)] + fn appearance(&self) -> zbus::Result; + + /// Blocked property + #[zbus(property)] + fn blocked(&self) -> zbus::Result; + #[zbus(property)] + fn set_blocked(&self, value: bool) -> zbus::Result<()>; + + /// Class property + #[zbus(property)] + fn class(&self) -> zbus::Result; + + /// Connected property + #[zbus(property)] + fn connected(&self) -> zbus::Result; + + /// Icon property + #[zbus(property)] + fn icon(&self) -> zbus::Result; + + /// LegacyPairing property + #[zbus(property)] + fn legacy_pairing(&self) -> zbus::Result; + + /// ManufacturerData property + #[zbus(property)] + fn manufacturer_data( + &self, + ) -> zbus::Result>; + + /// Modalias property + #[zbus(property)] + fn modalias(&self) -> zbus::Result; + + /// Name property + #[zbus(property)] + fn name(&self) -> zbus::Result; + + /// Paired property + #[zbus(property)] + fn paired(&self) -> zbus::Result; + + /// RSSI property + #[zbus(property, name = "RSSI")] + fn rssi(&self) -> zbus::Result; + + /// ServiceData property + #[zbus(property)] + fn service_data( + &self, + ) -> zbus::Result>; + + /// ServicesResolved property + #[zbus(property)] + fn services_resolved(&self) -> zbus::Result; + + /// Trusted property + #[zbus(property)] + fn trusted(&self) -> zbus::Result; + #[zbus(property)] + fn set_trusted(&self, value: bool) -> zbus::Result<()>; + + /// TxPower property + #[zbus(property)] + fn tx_power(&self) -> zbus::Result; + + /// UUIDs property + #[zbus(property, name = "UUIDs")] + fn uuids(&self) -> zbus::Result>; + + /// WakeAllowed property + #[zbus(property)] + fn wake_allowed(&self) -> zbus::Result; + #[zbus(property)] + fn set_wake_allowed(&self, value: bool) -> zbus::Result<()>; +} diff --git a/bluez/src/lib.rs b/bluez/src/lib.rs new file mode 100644 index 0000000..2de39a5 --- /dev/null +++ b/bluez/src/lib.rs @@ -0,0 +1,173 @@ +use std::collections::HashMap; + +use futures::join; + +pub mod adapter1; +pub mod battery1; +pub mod device1; + +pub async fn get_adapters<'a>( + connection: &zbus::Connection, +) -> zbus::Result>> { + let managed_object_proxy = + zbus::fdo::ObjectManagerProxy::new(connection, "org.bluez", "/").await?; + let managed_object: zbus::fdo::ManagedObjects = + managed_object_proxy.get_managed_objects().await?; + let adapter_addresses: Vec = managed_object + .into_iter() + .filter_map(move |(path, interfaces)| { + interfaces + .contains_key("org.bluez.Adapter1") + .then_some(path.to_owned()) + }) + .collect(); + let adapters: Vec< + zbus::Result<(zbus::zvariant::OwnedObjectPath, adapter1::Adapter1Proxy<'a>)>, + > = futures::future::join_all(adapter_addresses.into_iter().map(|path| async { + Ok(( + path.clone(), + adapter1::Adapter1Proxy::new(connection, path).await?, + )) + })) + .await; + + let errors = adapters.iter().filter(|device| device.is_err()); + if errors.count() > 0 { + let mut errors: Vec = adapters + .into_iter() + .filter_map(std::result::Result::err) + .collect(); + if errors.len() > 1 { + eprintln!("Multiple errors occurs when fetching connected device: {errors:?}. Only the last one will be returned."); + } + return Err(errors.pop().unwrap()); + } + Ok(adapters + .into_iter() + .filter_map(std::result::Result::ok) + .collect()) +} + +#[derive(Debug)] +pub struct BluetoothDevice<'a> { + pub device: device1::Device1Proxy<'a>, + pub battery: Option>, +} + +impl<'a> BluetoothDevice<'a> { + pub async fn new<'b: 'a>( + connection: &zbus::Connection, + path: zbus::zvariant::ObjectPath<'b>, + ) -> zbus::Result { + let (device, battery) = join!( + device1::Device1Proxy::builder(connection) + .path(&path)? + .build(), + battery1::Battery1Proxy::builder(connection) + .path(path)? + .build() + ); + + match (device, battery) { + (Ok(device), Ok(battery)) if battery.percentage().await.is_err() => Ok(Self { + device, + battery: None, + }), + (Ok(device), Ok(battery)) => Ok(Self { + device, + battery: Some(battery), + }), + (Ok(device), Err(zbus::Error::InterfaceNotFound)) => Ok(Self { + device, + battery: None, + }), + (Err(why), _) => Err(why), + (_, Err(why)) => Err(why), + } + } + + pub async fn icon(&self) -> String { + self.device + .inner() + .get_property::("Icon") + .await + .unwrap_or("unknown".to_owned()) + } + + pub fn path(&self) -> zbus::zvariant::OwnedObjectPath { + self.device.inner().path().to_owned().into() + } +} + +pub async fn get_device<'a>( + connection: &zbus::Connection, + device_path: zbus::zvariant::OwnedObjectPath, +) -> zbus::Result> { + BluetoothDevice::new( + connection, + device_path.into(), + ) + .await +} + +pub async fn get_adapter<'a>( + connection: &zbus::Connection, + adapter_path: impl TryInto>, +) -> zbus::Result> { + adapter1::Adapter1Proxy::builder(connection) + .path( + adapter_path + .try_into() + .map_err(|_| zbus::Error::Failure("Invalid adapter path".to_owned()))?, + )? + .build() + .await +} + +pub async fn get_devices<'a>( + connection: &zbus::Connection, + adapter: Option<&str>, +) -> zbus::Result>> { + let managed_object_proxy = + zbus::fdo::ObjectManagerProxy::new(connection, "org.bluez", "/").await?; + let managed_object: zbus::fdo::ManagedObjects = + managed_object_proxy.get_managed_objects().await?; + let device_addresses: Vec = managed_object + .into_iter() + .filter_map(move |(path, interfaces)| { + if matches!( + adapter.map(|adapter| path.as_str().starts_with(&format!("{}/", adapter))), + None | Some(true) + ) { + return interfaces + .contains_key("org.bluez.Device1") + .then_some(path.to_owned()); + } + None + }) + .collect(); + let devices: Vec)>> = + futures::future::join_all(device_addresses.into_iter().map(|path| async { + Ok(( + path.clone(), + BluetoothDevice::new(connection, path.into()).await?, + )) + })) + .await; + + let errors = devices.iter().filter(|device| device.is_err()); + if errors.count() > 0 { + let mut errors: Vec = devices + .into_iter() + .filter_map(std::result::Result::err) + .collect(); + if errors.len() > 1 { + eprintln!("Multiple errors occurs when fetching connected device: {errors:?}. Only the last one will be returned."); + } + return Err(errors.pop().unwrap()); + } + Ok(devices + .into_iter() + .filter_map(std::result::Result::ok) + .collect()) +}