use zbus::fdo::{DBusProxy, ObjectManagerProxy, PropertiesProxy};
use super::prelude::*;
use crate::wrappers::RangeMap;
make_log_macro!(debug, "bluetooth");
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub mac: String,
#[serde(default)]
pub adapter_mac: Option<String>,
#[serde(default)]
pub format: FormatConfig,
#[serde(default)]
pub disconnected_format: FormatConfig,
#[serde(default)]
pub battery_state: Option<RangeMap<u8, State>>,
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[(MouseButton::Right, None, "toggle")])?;
let format = config.format.with_default(" $icon $name{ $percentage|} ")?;
let disconnected_format = config
.disconnected_format
.with_default(" $icon{ $name|} ")?;
let mut monitor = DeviceMonitor::new(config.mac.clone(), config.adapter_mac.clone()).await?;
let battery_states = config.battery_state.clone().unwrap_or_else(|| {
vec![
(0..=15, State::Critical),
(16..=30, State::Warning),
(31..=60, State::Info),
(61..=100, State::Good),
]
.into()
});
loop {
match monitor.get_device_info().await {
Some(device) => {
debug!("Device available, info: {device:?}");
let mut widget = Widget::new();
let values = map! {
"icon" => Value::icon(device.icon),
"name" => Value::text(device.name),
"available" => Value::flag(),
[if let Some(p) = device.battery_percentage] "percentage" => Value::percents(p),
[if let Some(p) = device.battery_percentage]
"battery_icon" => Value::icon_progression("bat", p as f64 / 100.0),
};
if device.connected {
widget.set_format(format.clone());
widget.state = battery_states
.get(&device.battery_percentage.unwrap_or(100))
.copied()
.unwrap_or(State::Good);
} else {
widget.set_format(disconnected_format.clone());
widget.state = State::Idle;
}
widget.set_values(values);
api.set_widget(widget)?;
}
None => {
debug!("Showing device as unavailable");
let mut widget = Widget::new().with_format(disconnected_format.clone());
widget.set_values(map!("icon" => Value::icon("bluetooth")));
api.set_widget(widget)?;
}
}
loop {
select! {
res = monitor.wait_for_change() => {
res?;
break;
},
Some(action) = actions.recv() => match action.as_ref() {
"toggle" => {
if let Some(dev) = &monitor.device
&& let Ok(connected) = dev.device.connected().await {
if connected {
let _ = dev.device.disconnect().await;
} else {
let _ = dev.device.connect().await;
}
break;
}
}
_ => (),
}
}
}
}
}
struct DeviceMonitor {
mac: String,
adapter_mac: Option<String>,
manager_proxy: ObjectManagerProxy<'static>,
device: Option<Device>,
}
struct Device {
props: PropertiesProxy<'static>,
device: Device1Proxy<'static>,
battery: Battery1Proxy<'static>,
}
#[derive(Debug)]
struct DeviceInfo {
connected: bool,
icon: &'static str,
name: String,
battery_percentage: Option<u8>,
}
impl DeviceMonitor {
async fn new(mac: String, adapter_mac: Option<String>) -> Result<Self> {
let dbus_conn = new_system_dbus_connection().await?;
let manager_proxy = ObjectManagerProxy::builder(&dbus_conn)
.destination("org.bluez")
.and_then(|x| x.path("/"))
.unwrap()
.build()
.await
.error("Failed to create ObjectManagerProxy")?;
let device = Device::try_find(&manager_proxy, &mac, adapter_mac.as_deref()).await?;
Ok(Self {
mac,
adapter_mac,
manager_proxy,
device,
})
}
async fn wait_for_change(&mut self) -> Result<()> {
match &mut self.device {
None => {
let mut interface_added = self
.manager_proxy
.receive_interfaces_added()
.await
.error("Failed to monitor interfaces")?;
loop {
interface_added
.next()
.await
.error("Stream ended unexpectedly")?;
if let Some(device) = Device::try_find(
&self.manager_proxy,
&self.mac,
self.adapter_mac.as_deref(),
)
.await?
{
self.device = Some(device);
debug!("Device has been added");
return Ok(());
}
}
}
Some(device) => {
let mut updates = device
.props
.receive_properties_changed()
.await
.error("Failed to receive updates")?;
let mut interface_added = self
.manager_proxy
.receive_interfaces_added()
.await
.error("Failed to monitor interfaces")?;
let mut interface_removed = self
.manager_proxy
.receive_interfaces_removed()
.await
.error("Failed to monitor interfaces")?;
let mut bluez_owner_changed =
DBusProxy::new(self.manager_proxy.inner().connection())
.await
.error("Failed to create DBusProxy")?
.receive_name_owner_changed_with_args(&[(0, "org.bluez")])
.await
.unwrap();
loop {
select! {
_ = updates.next_debounced() => {
debug!("Got update for device");
return Ok(());
}
Some(event) = interface_added.next() => {
let args = event.args().error("Failed to get the args")?;
if args.object_path() == device.device.inner().path() {
debug!("Interfaces added: {:?}", args.interfaces_and_properties().keys());
return Ok(());
}
}
Some(event) = interface_removed.next() => {
let args = event.args().error("Failed to get the args")?;
if args.object_path() == device.device.inner().path() {
self.device = None;
debug!("Device is no longer available");
return Ok(());
}
}
Some(event) = bluez_owner_changed.next() => {
let args = event.args().error("Failed to get the args")?;
if args.new_owner.is_none() {
self.device = None;
debug!("org.bluez disappeared");
return Ok(());
}
}
}
}
}
}
}
async fn get_device_info(&mut self) -> Option<DeviceInfo> {
let device = self.device.as_ref()?;
let Ok((connected, name)) =
tokio::try_join!(device.device.connected(), device.device.name(),)
else {
debug!("failed to fetch device info, assuming device or bluez disappeared");
self.device = None;
return None;
};
let icon: &str = match device.device.icon().await.ok().as_deref() {
Some("audio-card" | "audio-headset" | "audio-headphones") => "headphones",
Some("input-gaming") => "joystick",
Some("input-keyboard") => "keyboard",
Some("input-mouse") => "mouse",
_ => "bluetooth",
};
Some(DeviceInfo {
connected,
icon,
name,
battery_percentage: device.battery.percentage().await.ok(),
})
}
}
impl Device {
async fn try_find(
manager_proxy: &ObjectManagerProxy<'_>,
mac: &str,
adapter_mac: Option<&str>,
) -> Result<Option<Self>> {
let Ok(devices) = manager_proxy.get_managed_objects().await else {
debug!("could not get the list of managed objects");
return Ok(None);
};
debug!("all managed devices: {:?}", devices);
let root_object: Option<String> = match adapter_mac {
Some(adapter_mac) => {
let mut adapter_path = None;
for (path, interfaces) in &devices {
let adapter_interface = match interfaces.get("org.bluez.Adapter1") {
Some(i) => i,
None => continue, };
let addr: &str = adapter_interface
.get("Address")
.and_then(|a| a.downcast_ref().ok())
.unwrap();
if addr == adapter_mac {
adapter_path = Some(path);
break;
}
}
match adapter_path {
Some(path) => Some(format!("{}/", path.as_str())),
None => return Ok(None),
}
}
None => None,
};
debug!("root object: {:?}", root_object);
for (path, interfaces) in devices {
if let Some(root) = &root_object
&& !path.starts_with(root)
{
continue;
}
let Some(device_interface) = interfaces.get("org.bluez.Device1") else {
continue;
};
let addr: &str = device_interface
.get("Address")
.and_then(|a| a.downcast_ref().ok())
.unwrap();
if addr != mac {
continue;
}
debug!("Found device with path {:?}", path);
return Ok(Some(Self {
props: PropertiesProxy::builder(manager_proxy.inner().connection())
.destination("org.bluez")
.and_then(|x| x.path(path.clone()))
.unwrap()
.build()
.await
.error("Failed to create PropertiesProxy")?,
device: Device1Proxy::builder(manager_proxy.inner().connection())
.cache_properties(zbus::proxy::CacheProperties::No)
.path(path.clone())
.unwrap()
.build()
.await
.error("Failed to create Device1Proxy")?,
battery: Battery1Proxy::builder(manager_proxy.inner().connection())
.cache_properties(zbus::proxy::CacheProperties::No)
.path(path)
.unwrap()
.build()
.await
.error("Failed to create Battery1Proxy")?,
}));
}
debug!("No device found");
Ok(None)
}
}
#[zbus::proxy(interface = "org.bluez.Device1", default_service = "org.bluez")]
trait Device1 {
fn connect(&self) -> zbus::Result<()>;
fn disconnect(&self) -> zbus::Result<()>;
#[zbus(property)]
fn connected(&self) -> zbus::Result<bool>;
#[zbus(property)]
fn name(&self) -> zbus::Result<String>;
#[zbus(property)]
fn icon(&self) -> zbus::Result<String>;
}
#[zbus::proxy(interface = "org.bluez.Battery1", default_service = "org.bluez")]
trait Battery1 {
#[zbus(property)]
fn percentage(&self) -> zbus::Result<u8>;
}