feat: add bindings for BlueZ

This commit is contained in:
Antoine C 2024-09-06 19:49:32 +01:00 committed by Michael Aaron Murphy
parent e0d6a04d6e
commit 8059e6bdaa
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
7 changed files with 763 additions and 0 deletions

View file

@ -12,6 +12,7 @@ members = [
"switcheroo-control",
"timedate",
"upower",
"bluez",
]
[workspace.dependencies]

17
bluez/Cargo.toml Normal file
View file

@ -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"] }

View file

@ -0,0 +1,275 @@
use std::process::ExitCode;
use futures::StreamExt;
#[tokio::main]
async fn main() -> Result<ExitCode, Box<dyn std::error::Error>> {
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::<String>("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::<String>("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::<String>("Icon")
.await
.unwrap_or("unknown".to_owned())
);
}
}
}
}
Some("connect") => match parser.free_from_str::<String>().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<bluez_zbus::BluetoothDevice> =
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::<String>().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<bluez_zbus::BluetoothDevice> =
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::<String>().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<bluez_zbus::BluetoothDevice> =
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
"
);
}

119
bluez/src/adapter1.rs Normal file
View file

@ -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<Vec<String>>;
/// 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<String>;
/// AddressType property
#[zbus(property)]
fn address_type(&self) -> zbus::Result<String>;
/// Alias property
#[zbus(property)]
fn alias(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_alias(&self, value: &str) -> zbus::Result<()>;
/// Class property
#[zbus(property)]
fn class(&self) -> zbus::Result<u32>;
/// Discoverable property
#[zbus(property)]
fn discoverable(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn set_discoverable(&self, value: bool) -> zbus::Result<()>;
/// DiscoverableTimeout property
#[zbus(property)]
fn discoverable_timeout(&self) -> zbus::Result<u32>;
#[zbus(property)]
fn set_discoverable_timeout(&self, value: u32) -> zbus::Result<()>;
/// Discovering property
#[zbus(property)]
fn discovering(&self) -> zbus::Result<bool>;
/// ExperimentalFeatures property
#[zbus(property)]
fn experimental_features(&self) -> zbus::Result<Vec<String>>;
/// Modalias property
#[zbus(property)]
fn modalias(&self) -> zbus::Result<String>;
/// Name property
#[zbus(property)]
fn name(&self) -> zbus::Result<String>;
/// Pairable property
#[zbus(property)]
fn pairable(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn set_pairable(&self, value: bool) -> zbus::Result<()>;
/// PairableTimeout property
#[zbus(property)]
fn pairable_timeout(&self) -> zbus::Result<u32>;
#[zbus(property)]
fn set_pairable_timeout(&self, value: u32) -> zbus::Result<()>;
/// Powered property
#[zbus(property)]
fn powered(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn set_powered(&self, value: bool) -> zbus::Result<()>;
/// Roles property
#[zbus(property)]
fn roles(&self) -> zbus::Result<Vec<String>>;
/// UUIDs property
#[zbus(property, name = "UUIDs")]
fn uuids(&self) -> zbus::Result<Vec<String>>;
}

31
bluez/src/battery1.rs Normal file
View file

@ -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<u8>;
/// Source property
#[zbus(property)]
fn source(&self) -> zbus::Result<String>;
}

147
bluez/src/device1.rs Normal file
View file

@ -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<zbus::zvariant::OwnedObjectPath>;
/// Address property
#[zbus(property)]
fn address(&self) -> zbus::Result<String>;
/// AddressType property
#[zbus(property)]
fn address_type(&self) -> zbus::Result<String>;
/// AdvertisingData property
#[zbus(property)]
fn advertising_data(
&self,
) -> zbus::Result<std::collections::HashMap<u8, zbus::zvariant::OwnedValue>>;
/// AdvertisingFlags property
#[zbus(property)]
fn advertising_flags(&self) -> zbus::Result<Vec<u8>>;
/// Alias property
#[zbus(property)]
fn alias(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_alias(&self, value: &str) -> zbus::Result<()>;
/// Appearance property
#[zbus(property)]
fn appearance(&self) -> zbus::Result<u16>;
/// Blocked property
#[zbus(property)]
fn blocked(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn set_blocked(&self, value: bool) -> zbus::Result<()>;
/// Class property
#[zbus(property)]
fn class(&self) -> zbus::Result<u32>;
/// Connected property
#[zbus(property)]
fn connected(&self) -> zbus::Result<bool>;
/// Icon property
#[zbus(property)]
fn icon(&self) -> zbus::Result<String>;
/// LegacyPairing property
#[zbus(property)]
fn legacy_pairing(&self) -> zbus::Result<bool>;
/// ManufacturerData property
#[zbus(property)]
fn manufacturer_data(
&self,
) -> zbus::Result<std::collections::HashMap<u16, zbus::zvariant::OwnedValue>>;
/// Modalias property
#[zbus(property)]
fn modalias(&self) -> zbus::Result<String>;
/// Name property
#[zbus(property)]
fn name(&self) -> zbus::Result<String>;
/// Paired property
#[zbus(property)]
fn paired(&self) -> zbus::Result<bool>;
/// RSSI property
#[zbus(property, name = "RSSI")]
fn rssi(&self) -> zbus::Result<i16>;
/// ServiceData property
#[zbus(property)]
fn service_data(
&self,
) -> zbus::Result<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>;
/// ServicesResolved property
#[zbus(property)]
fn services_resolved(&self) -> zbus::Result<bool>;
/// Trusted property
#[zbus(property)]
fn trusted(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn set_trusted(&self, value: bool) -> zbus::Result<()>;
/// TxPower property
#[zbus(property)]
fn tx_power(&self) -> zbus::Result<i16>;
/// UUIDs property
#[zbus(property, name = "UUIDs")]
fn uuids(&self) -> zbus::Result<Vec<String>>;
/// WakeAllowed property
#[zbus(property)]
fn wake_allowed(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn set_wake_allowed(&self, value: bool) -> zbus::Result<()>;
}

173
bluez/src/lib.rs Normal file
View file

@ -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<HashMap<zbus::zvariant::OwnedObjectPath, adapter1::Adapter1Proxy<'a>>> {
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<zbus::zvariant::OwnedObjectPath> = 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<zbus::Error> = 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<battery1::Battery1Proxy<'a>>,
}
impl<'a> BluetoothDevice<'a> {
pub async fn new<'b: 'a>(
connection: &zbus::Connection,
path: zbus::zvariant::ObjectPath<'b>,
) -> zbus::Result<Self> {
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::<String>("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<'a>> {
BluetoothDevice::new(
connection,
device_path.into(),
)
.await
}
pub async fn get_adapter<'a>(
connection: &zbus::Connection,
adapter_path: impl TryInto<zbus::zvariant::ObjectPath<'a>>,
) -> zbus::Result<adapter1::Adapter1Proxy<'a>> {
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<HashMap<zbus::zvariant::OwnedObjectPath, BluetoothDevice<'a>>> {
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<zbus::zvariant::OwnedObjectPath> = 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<zbus::Result<(zbus::zvariant::OwnedObjectPath, BluetoothDevice<'a>)>> =
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<zbus::Error> = 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())
}