Read Bluetooth status directly from BlueZ
This commit is contained in:
1 parent
e448d433fc
commit
4251b5d084
2 files changed
+164
-23
No files matched your search
@@ -1,4 +1,5 @@
|
||||
//! Bounded, off-UI-thread adapters. A missing service is `None`, never a fake state.
|
||||
mod bluetooth;
|
||||
mod network;
|
||||
|
||||
use std::{
|
||||
@@ -337,7 +338,7 @@ impl StatusBus {
|
||||
fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
||||
Snapshot {
|
||||
network: system_bus.query(network::read).flatten(),
|
||||
bluetooth: bluetooth(),
|
||||
bluetooth: system_bus.query(bluetooth::read).flatten(),
|
||||
audio: audio(),
|
||||
battery: battery(),
|
||||
brightness: brightness(),
|
||||
@@ -348,28 +349,6 @@ fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
||||
}
|
||||
}
|
||||
|
||||
fn bluetooth() -> Option<Bluetooth> {
|
||||
let info = run("bluetoothctl", &["show"]).ok()?;
|
||||
let enabled = info
|
||||
.lines()
|
||||
.find_map(|line| line.trim().strip_prefix("Powered: "))?
|
||||
== "yes";
|
||||
let devices = if enabled {
|
||||
run("bluetoothctl", &["devices", "Connected"])
|
||||
.ok()?
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
line.strip_prefix("Device ")
|
||||
.and_then(|s| s.split_once(' '))
|
||||
.map(|(_, name)| name.to_owned())
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Some(Bluetooth { enabled, devices })
|
||||
}
|
||||
|
||||
pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
|
||||
let volume = value
|
||||
.strip_prefix("Volume: ")?
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//! BlueZ status from its object graph; never starts discovery or pairing.
|
||||
use super::Bluetooth;
|
||||
use zbus::{blocking::Connection, fdo::ManagedObjects};
|
||||
|
||||
pub(super) fn read(connection: &Connection) -> zbus::Result<Option<Bluetooth>> {
|
||||
let objects: ManagedObjects = connection
|
||||
.call_method(
|
||||
Some("org.bluez"),
|
||||
"/",
|
||||
Some("org.freedesktop.DBus.ObjectManager"),
|
||||
"GetManagedObjects",
|
||||
&(),
|
||||
)?
|
||||
.body()
|
||||
.deserialize()?;
|
||||
Ok(snapshot(&objects))
|
||||
}
|
||||
|
||||
fn snapshot(objects: &ManagedObjects) -> Option<Bluetooth> {
|
||||
// bluetoothctl's fresh process uses its first controller. Prefer hci0 and
|
||||
// keep the selection stable when the object-manager dictionary is reordered.
|
||||
let (adapter, properties) = objects
|
||||
.iter()
|
||||
.filter_map(|(path, interfaces)| interfaces.get("org.bluez.Adapter1").map(|p| (path, p)))
|
||||
.min_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()))?;
|
||||
let enabled = bool::try_from(properties.get("Powered")?).ok()?;
|
||||
let mut devices = Vec::new();
|
||||
if enabled {
|
||||
for interfaces in objects.values() {
|
||||
let Some(device) = interfaces.get("org.bluez.Device1") else {
|
||||
continue;
|
||||
};
|
||||
let owner = <&zbus::zvariant::ObjectPath>::try_from(device.get("Adapter")?).ok()?;
|
||||
if owner.as_str() != adapter.as_str() {
|
||||
continue;
|
||||
}
|
||||
if bool::try_from(device.get("Connected")?).ok()? {
|
||||
let alias = <&str>::try_from(device.get("Alias")?).ok()?;
|
||||
devices.push(alias.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
devices.sort();
|
||||
Some(Bluetooth { enabled, devices })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Str};
|
||||
fn graph() -> ManagedObjects {
|
||||
let mut graph = ManagedObjects::new();
|
||||
for (path, powered) in [("/org/bluez/hci0", true), ("/org/bluez/hci1", false)] {
|
||||
graph.insert(
|
||||
OwnedObjectPath::try_from(path).unwrap(),
|
||||
[(
|
||||
"org.bluez.Adapter1".try_into().unwrap(),
|
||||
[("Powered".into(), OwnedValue::from(powered))].into(),
|
||||
)]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
for (path, adapter, connected, alias) in [
|
||||
(
|
||||
"/org/bluez/hci0/dev_a",
|
||||
"/org/bluez/hci0",
|
||||
true,
|
||||
"Headphones",
|
||||
),
|
||||
(
|
||||
"/org/bluez/hci0/dev_b",
|
||||
"/org/bluez/hci0",
|
||||
false,
|
||||
"Keyboard",
|
||||
),
|
||||
(
|
||||
"/org/bluez/hci1/dev_a",
|
||||
"/org/bluez/hci1",
|
||||
true,
|
||||
"Other adapter",
|
||||
),
|
||||
] {
|
||||
graph.insert(
|
||||
OwnedObjectPath::try_from(path).unwrap(),
|
||||
[(
|
||||
"org.bluez.Device1".try_into().unwrap(),
|
||||
[
|
||||
(
|
||||
"Adapter".into(),
|
||||
OwnedValue::try_from(zbus::zvariant::Value::from(
|
||||
OwnedObjectPath::try_from(adapter).unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
),
|
||||
("Connected".into(), OwnedValue::from(connected)),
|
||||
("Alias".into(), OwnedValue::from(Str::from(alias))),
|
||||
]
|
||||
.into(),
|
||||
)]
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
graph
|
||||
}
|
||||
#[test]
|
||||
fn selects_one_adapter_and_only_its_connected_devices() {
|
||||
let mut objects = graph();
|
||||
let value = snapshot(&objects).unwrap();
|
||||
assert!(value.enabled);
|
||||
assert_eq!(value.devices, ["Headphones"]);
|
||||
let adapter = objects
|
||||
.get_mut(&OwnedObjectPath::try_from("/org/bluez/hci0").unwrap())
|
||||
.unwrap()
|
||||
.get_mut("org.bluez.Adapter1")
|
||||
.unwrap();
|
||||
adapter.insert("Powered".into(), false.into());
|
||||
let value = snapshot(&objects).unwrap();
|
||||
assert!(!value.enabled && value.devices.is_empty());
|
||||
adapter_removed(&mut objects);
|
||||
assert!(!snapshot(&objects).unwrap().enabled);
|
||||
assert!(snapshot(&ManagedObjects::new()).is_none());
|
||||
}
|
||||
fn adapter_removed(objects: &mut ManagedObjects) {
|
||||
objects.retain(|path, _| !path.as_str().starts_with("/org/bluez/hci0"));
|
||||
}
|
||||
#[test]
|
||||
fn incomplete_adapter_does_not_invent_power_state() {
|
||||
let mut objects = graph();
|
||||
for interfaces in objects.values_mut() {
|
||||
if let Some(adapter) = interfaces.get_mut("org.bluez.Adapter1") {
|
||||
adapter.remove("Powered");
|
||||
}
|
||||
}
|
||||
assert!(snapshot(&objects).is_none());
|
||||
}
|
||||
struct Manager;
|
||||
#[zbus::interface(name = "org.freedesktop.DBus.ObjectManager")]
|
||||
impl Manager {
|
||||
fn get_managed_objects(&self) -> ManagedObjects {
|
||||
graph()
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires dbus-daemon; uses a private test bus"]
|
||||
fn bluetooth_bus_roundtrip_and_service_recovery() {
|
||||
let bus = crate::status::tests::TestBus::new();
|
||||
let server = zbus::blocking::connection::Builder::address(bus.address.as_str())
|
||||
.unwrap()
|
||||
.name("org.bluez")
|
||||
.unwrap()
|
||||
.serve_at("/", Manager)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let client = bus.connect();
|
||||
assert_eq!(read(&client).unwrap().unwrap().devices, ["Headphones"]);
|
||||
server.release_name("org.bluez").unwrap();
|
||||
assert!(read(&client).is_err());
|
||||
server.request_name("org.bluez").unwrap();
|
||||
assert!(read(&client).unwrap().unwrap().enabled);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user