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.
|
//! Bounded, off-UI-thread adapters. A missing service is `None`, never a fake state.
|
||||||
|
mod bluetooth;
|
||||||
mod network;
|
mod network;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
@@ -337,7 +338,7 @@ impl StatusBus {
|
|||||||
fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
fn poll(system_bus: &mut StatusBus, session_bus: &mut StatusBus) -> Snapshot {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
network: system_bus.query(network::read).flatten(),
|
network: system_bus.query(network::read).flatten(),
|
||||||
bluetooth: bluetooth(),
|
bluetooth: system_bus.query(bluetooth::read).flatten(),
|
||||||
audio: audio(),
|
audio: audio(),
|
||||||
battery: battery(),
|
battery: battery(),
|
||||||
brightness: brightness(),
|
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)> {
|
pub fn parse_audio(value: &str) -> Option<(u8, bool)> {
|
||||||
let volume = value
|
let volume = value
|
||||||
.strip_prefix("Volume: ")?
|
.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