cosmic-applets/cosmic-applet-bluetooth/src/bluetooth.rs
2026-03-31 22:34:59 +02:00

882 lines
34 KiB
Rust

// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use rustc_hash::FxHashMap;
use std::{
fmt::Debug,
hash::Hash,
sync::{
Arc, LazyLock,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use bluer::{
Adapter, AdapterProperty, Address, Session, Uuid,
agent::{Agent, AgentHandle},
};
use cosmic::{
iced::{
self, Subscription,
futures::{SinkExt, StreamExt},
},
iced_futures::stream,
};
use futures::{FutureExt, stream::FuturesUnordered};
use tokio::{
spawn,
sync::{
Mutex, RwLock,
mpsc::{Receiver, Sender, channel},
},
task::JoinHandle,
};
static TICK: LazyLock<RwLock<Duration>> = LazyLock::new(|| RwLock::new(Duration::from_secs(10)));
static DISCOVERY: AtomicBool = AtomicBool::new(false);
pub fn set_discovery(v: bool) {
DISCOVERY.store(v, Ordering::Relaxed);
}
pub async fn set_tick(duration: Duration) {
let mut guard = TICK.write().await;
*guard = duration;
}
pub async fn tick(interval: &mut tokio::time::Interval) {
let guard = TICK.read().await;
if *guard != interval.period() {
*interval = tokio::time::interval(*guard);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
}
interval.tick().await;
}
// Copied from https://github.com/bluez/bluez/blob/39467578207889fd015775cbe81a3db9dd26abea/src/dbus-common.c#L53
#[inline]
fn device_type_to_icon(device_type: &str) -> &'static str {
match device_type {
"computer" => "laptop-symbolic",
"phone" => "smartphone-symbolic",
"network-wireless" => "network-wireless-symbolic",
"audio-headset" => "audio-headset-symbolic",
"audio-headphones" => "audio-headphones-symbolic",
"camera-video" => "camera-video-symbolic",
"audio-card" => "audio-card-symbolic",
"input-gaming" => "input-gaming-symbolic",
"input-keyboard" => "input-keyboard-symbolic",
"input-tablet" => "input-tablet-symbolic",
"input-mouse" => "input-mouse-symbolic",
"printer" => "printer-network-symbolic",
"camera-photo" => "camera-photo-symbolic",
_ => DEFAULT_DEVICE_ICON,
}
}
// In some distros, rfkill is only in sbin, which isn't normally in PATH
// TODO: Directly access `/dev/rfkill`
fn rfkill_path_var() -> std::ffi::OsString {
let mut path = std::env::var_os("PATH").unwrap_or_default();
path.push(":/usr/sbin");
path
}
#[inline]
pub fn bluetooth_subscription<I: 'static + Hash + Copy + Send + Sync + Debug>(
id: I,
) -> iced::Subscription<BluerEvent> {
Subscription::run_with(id, |_| {
stream::channel(
50,
move |mut output: futures::channel::mpsc::Sender<BluerEvent>| async move {
let mut retry_count = 0u32;
// Initialize connection.
let mut session_state = loop {
if let Ok(session) = Session::new().await {
if let Ok(state) = BluerSessionState::new(session).await {
break state;
}
}
retry_count = retry_count.saturating_add(1);
() = tokio::time::sleep(Duration::from_millis(
2_u64.saturating_pow(retry_count).min(68719476734),
))
.await;
};
let state = bluer_state(&session_state.adapter).await;
// reconnect to paired and trusted devices
if state.bluetooth_enabled {
for d in &state.devices {
if d.paired_and_trusted()
&& !matches!(d.status, BluerDeviceStatus::Connected)
{
_ = session_state
.req_tx
.send(BluerRequest::ConnectDevice(d.address))
.await;
}
}
}
_ = output
.send(BluerEvent::Init {
sender: session_state.req_tx.clone(),
state: state.clone(),
})
.await;
let mut event_handler = async |event| {
let message = match event {
BluerSessionEvent::ChangesProcessed(state) => {
BluerEvent::DevicesChanged { state }
}
BluerSessionEvent::RequestResponse {
req,
state,
err_msg,
} => BluerEvent::RequestResponse {
req,
state,
err_msg,
},
BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e),
_ => return,
};
_ = output.send(message).await;
};
let mut interval = tokio::time::interval(Duration::from_secs(10));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
let Some(mut session_rx) = session_state.rx.take() else {
break;
};
if let Some(event) = session_rx.recv().await {
event_handler(event).await;
// Consume any additional available events.
let mut count = 0;
while let Ok(event) = session_rx.try_recv() {
event_handler(event).await;
count += 1;
if count == 100 {
break;
}
}
} else {
break;
}
session_state.rx = Some(session_rx);
interval.tick().await;
}
_ = output.send(BluerEvent::Finished).await;
futures::future::pending().await
},
)
})
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum BluerRequest {
SetBluetoothEnabled(bool),
PairDevice(Address),
ConnectDevice(Address),
DisconnectDevice(Address),
CancelConnect(Address),
}
#[derive(Debug, Clone)]
pub enum BluerEvent {
RequestResponse {
req: BluerRequest,
state: BluerState,
err_msg: Option<String>,
},
Init {
sender: Sender<BluerRequest>,
state: BluerState,
},
DevicesChanged {
state: BluerState,
},
AgentEvent(BluerAgentEvent),
Finished,
}
#[derive(Debug, Clone, Default)]
pub struct BluerState {
pub devices: Vec<BluerDevice>,
pub bluetooth_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum BluerDeviceStatus {
Connected,
Connecting,
Paired,
/// Pairing is in progress, maybe with a passkey or pincode
/// passkey or pincode will be 000000 - 999999
Pairing,
Disconnected,
Disconnecting,
}
#[derive(Debug, Clone)]
pub struct BluerDevice {
pub name: String,
pub icon: &'static str,
pub address: Address,
pub status: BluerDeviceStatus,
pub battery_percent: Option<u8>,
pub is_paired: bool,
pub is_trusted: bool,
}
impl Eq for BluerDevice {}
impl Ord for BluerDevice {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.status.cmp(&other.status) {
std::cmp::Ordering::Equal => self.name.to_lowercase().cmp(&other.name.to_lowercase()),
o => o,
}
}
}
impl PartialOrd for BluerDevice {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for BluerDevice {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.address == other.address
}
}
const DEFAULT_DEVICE_ICON: &str = "bluetooth-symbolic";
impl BluerDevice {
#[inline(never)]
pub async fn from_device(device: &bluer::Device) -> Self {
let (mut name, is_paired, is_trusted, is_connected, battery_percent, icon) = futures::join!(
device
.name()
.map(|res| res.ok().flatten().unwrap_or(device.address().to_string())),
device.is_paired().map(Result::unwrap_or_default),
device.is_trusted().map(Result::unwrap_or_default),
device.is_connected().map(Result::unwrap_or_default),
device.battery_percentage().map(|res| res.ok().flatten()),
device
.icon()
.map(|res| device_type_to_icon(&res.ok().flatten().unwrap_or_default()))
);
if name.is_empty() {
name = device.address().to_string();
}
let status = if is_connected {
BluerDeviceStatus::Connected
} else if is_paired {
BluerDeviceStatus::Paired
} else {
BluerDeviceStatus::Disconnected
};
Self {
name,
icon,
address: device.address(),
status,
battery_percent,
is_paired,
is_trusted,
}
}
#[inline]
fn paired_and_trusted(&self) -> bool {
self.is_paired && self.is_trusted
}
#[inline]
#[must_use]
pub fn is_known_device_type(&self) -> bool {
self.icon != DEFAULT_DEVICE_ICON
}
#[inline]
#[must_use]
pub fn has_name(&self) -> bool {
self.name != self.address.to_string()
}
}
#[derive(Debug, Clone)]
pub enum BluerSessionEvent {
RequestResponse {
req: BluerRequest,
state: BluerState,
err_msg: Option<String>,
},
ChangesProcessed(BluerState),
AgentEvent(BluerAgentEvent),
}
#[derive(Debug, Clone)]
pub enum BluerAgentEvent {
DisplayPinCode(BluerDevice, String),
DisplayPasskey(BluerDevice, String),
RequestPinCode(BluerDevice),
RequestPasskey(BluerDevice),
RequestConfirmation(BluerDevice, String, Sender<bool>), // Note mpsc channel is used bc the sender must be cloned in the iced Message machinery
RequestDeviceAuthorization(BluerDevice, Sender<bool>),
RequestServiceAuthorization(BluerDevice, Uuid, Sender<bool>),
}
pub struct BluerSessionState {
_session: Session,
_agent_handle: AgentHandle,
pub adapter: Adapter,
pub rx: Option<Receiver<BluerSessionEvent>>,
pub req_tx: Sender<BluerRequest>,
wake_up_discover_tx: Sender<()>,
wake_up_discover_rx: Option<Receiver<()>>,
tx: Sender<BluerSessionEvent>,
active_requests: Arc<Mutex<FxHashMap<BluerRequest, JoinHandle<anyhow::Result<()>>>>>,
}
impl BluerSessionState {
async fn new(session: Session) -> anyhow::Result<Self> {
let adapter = session.default_adapter().await?;
let (tx, rx) = tokio::sync::mpsc::channel(100);
let (req_tx, req_rx) = channel(100);
let tx_clone_1 = tx.clone();
let tx_clone_2 = tx.clone();
let tx_clone_3 = tx.clone();
let tx_clone_4 = tx.clone();
let tx_clone_5 = tx.clone();
let tx_clone_6 = tx.clone();
let tx_clone_7 = tx.clone();
let adapter_clone_1 = adapter.clone();
let adapter_clone_2 = adapter.clone();
let adapter_clone_3 = adapter.clone();
let adapter_clone_4 = adapter.clone();
let adapter_clone_5 = adapter.clone();
let adapter_clone_6 = adapter.clone();
let adapter_clone_7 = adapter.clone();
let _agent = Agent {
request_default: false, // TODO which agent should eventually become the default? Maybe the one in the settings app?
request_pin_code: Some(Box::new(move |req| {
let agent_clone = adapter_clone_1.clone();
let tx_clone = tx_clone_1.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::RequestPinCode(
BluerDevice::from_device(&device).await,
),
))
.await;
let pin_code = fastrand::u32(0..999999);
Ok(format!("{pin_code:06}"))
})
})),
display_pin_code: Some(Box::new(move |req| {
let agent_clone = adapter_clone_2.clone();
let tx_clone = tx_clone_2.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::DisplayPinCode(
BluerDevice::from_device(&device).await,
req.pincode,
),
))
.await;
Ok(())
})
})),
request_passkey: Some(Box::new(move |req| {
let agent_clone = adapter_clone_3.clone();
let tx_clone = tx_clone_3.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::RequestPasskey(
BluerDevice::from_device(&device).await,
),
))
.await;
let pin_code = fastrand::u32(0..999999);
Ok(pin_code)
})
})),
display_passkey: Some(Box::new(move |req| {
let agent_clone = adapter_clone_4.clone();
let tx_clone = tx_clone_4.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::DisplayPasskey(
BluerDevice::from_device(&device).await,
format!("{:06}", req.passkey),
),
))
.await;
Ok(())
})
})),
request_confirmation: Some(Box::new(move |req| {
let agent_clone = adapter_clone_5.clone();
let tx_clone = tx_clone_5.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let (tx, mut rx) = channel(1);
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::RequestConfirmation(
BluerDevice::from_device(&device).await,
format!("{:06}", req.passkey),
tx,
),
))
.await;
let res = rx.recv().await;
match res {
Some(res) if res => Ok(()),
_ => Err(bluer::agent::ReqError::Rejected),
}
})
})),
request_authorization: Some(Box::new(move |req| {
let agent_clone = adapter_clone_6.clone();
let tx_clone = tx_clone_6.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let (tx, mut rx) = channel(1);
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::RequestDeviceAuthorization(
BluerDevice::from_device(&device).await,
tx,
),
))
.await;
let res = rx.recv().await;
match res {
Some(res) if res => Ok(()),
_ => Err(bluer::agent::ReqError::Rejected),
}
})
})),
authorize_service: Some(Box::new(move |req| {
let agent_clone = adapter_clone_7.clone();
let tx_clone = tx_clone_7.clone();
Box::pin(async move {
let Ok(device) = agent_clone.device(req.device) else {
return Err(bluer::agent::ReqError::Rejected);
};
let (tx, mut rx) = channel(1);
// TODO better describe the service to the user
let _ = tx_clone
.send(BluerSessionEvent::AgentEvent(
BluerAgentEvent::RequestServiceAuthorization(
BluerDevice::from_device(&device).await,
req.service,
tx,
),
))
.await;
let res = rx.recv().await;
match res {
Some(res) if res => Ok(()),
_ => Err(bluer::agent::ReqError::Rejected),
}
})
})),
_non_exhaustive: (),
};
let _agent_handle = session.register_agent(_agent).await?;
let (wake_up_discover_tx, wake_up_discover_rx) = channel(10);
let mut self_ = Self {
_agent_handle,
_session: session,
adapter,
rx: Some(rx),
req_tx,
wake_up_discover_rx: Some(wake_up_discover_rx),
wake_up_discover_tx,
tx,
active_requests: Default::default(),
};
self_.process_requests(req_rx);
self_.process_changes();
self_.listen_bluetooth_power_changes();
Ok(self_)
}
#[inline]
fn listen_bluetooth_power_changes(&self) {
let tx = self.tx.clone();
let req_tx = self.req_tx.clone();
let adapter_clone = self.adapter.clone();
let wake_up_discover_tx = self.wake_up_discover_tx.clone();
let _handle: JoinHandle<anyhow::Result<()>> = spawn(async move {
let mut status = adapter_clone.is_powered().await.unwrap_or_default();
let mut interval = tokio::time::interval(Duration::from_secs(10));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut devices = Vec::new();
loop {
tick(&mut interval).await;
let new_status = adapter_clone.is_powered().await.unwrap_or_default();
devices = build_device_list(devices, &adapter_clone).await;
if new_status != status {
status = new_status;
let state = BluerState {
devices: devices.clone(),
bluetooth_enabled: status,
};
if state.bluetooth_enabled {
for d in &state.devices {
if d.paired_and_trusted() {
_ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await;
}
}
}
_ = wake_up_discover_tx.send(()).await;
let _ = tx.send(BluerSessionEvent::ChangesProcessed(state)).await;
}
}
});
}
#[inline]
fn process_changes(&mut self) {
let req_tx = self.req_tx.clone();
let tx = self.tx.clone();
let Some(mut wake_up) = self.wake_up_discover_rx.take() else {
tracing::error!("Failed to take wake up channel");
return;
};
let adapter_clone = self.adapter.clone();
_ = spawn(async move {
let mut is_powered = adapter_clone.is_powered().await.unwrap_or_default();
let mut devices: Vec<BluerDevice> = Vec::new();
let mut interval = tokio::time::interval(Duration::from_secs(1));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
let wakeup_fut = wake_up.recv();
let listener_fut = async {
if DISCOVERY.load(Ordering::SeqCst) || devices.is_empty() {
let mut interval = tokio::time::interval(Duration::from_secs(10));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let Ok(mut change_stream) =
adapter_clone.discover_devices_with_changes().await
else {
tick(&mut interval).await;
return;
};
loop {
let Some(adapter_event) = change_stream.next().await else {
break;
};
match adapter_event {
bluer::AdapterEvent::PropertyChanged(AdapterProperty::Powered(
v,
)) => {
is_powered = v;
}
e => {
match e {
bluer::AdapterEvent::DeviceAdded(address)
if !devices.iter().any(|d| d.address == address) =>
{
devices =
build_device_list(Vec::new(), &adapter_clone).await;
for d in devices.iter().filter(|d| {
d.paired_and_trusted()
&& !matches!(
d.status,
BluerDeviceStatus::Connected
)
}) {
_ = req_tx
.send(BluerRequest::ConnectDevice(d.address))
.await;
}
}
bluer::AdapterEvent::DeviceRemoved(address)
if devices.iter().any(|d| d.address == address) =>
{
// Remove the device from new_devices if it exists
devices.retain(|d| d.address != address);
}
bluer::AdapterEvent::PropertyChanged(p) => {
tracing::info!("property change ignored {p:?}");
interval.tick().await;
continue;
}
bluer::AdapterEvent::DeviceAdded(address)
| bluer::AdapterEvent::DeviceRemoved(address) => {
tracing::info!(
"device change that is already handled {address}"
);
interval.tick().await;
continue;
}
}
}
}
let _ = tx
.send(BluerSessionEvent::ChangesProcessed(BluerState {
devices: devices.clone(),
bluetooth_enabled: is_powered,
}))
.await;
interval.tick().await;
if !DISCOVERY.load(Ordering::SeqCst) && !devices.is_empty() {
break;
}
}
} else {
loop {
if DISCOVERY.load(Ordering::SeqCst) || devices.is_empty() {
break;
}
interval.tick().await;
}
}
};
futures::pin_mut!(listener_fut);
futures::pin_mut!(wakeup_fut);
futures::future::select(listener_fut, wakeup_fut).await;
}
});
}
#[inline]
fn process_requests(&mut self, request_rx: Receiver<BluerRequest>) {
let active_requests = self.active_requests.clone();
let adapter = self.adapter.clone();
let tx = self.tx.clone();
let wake_up_tx = self.wake_up_discover_tx.clone();
let _handle: JoinHandle<anyhow::Result<()>> = spawn(async move {
let mut request_rx = request_rx;
while let Some(req) = request_rx.recv().await {
let req_clone = req.clone();
let req_clone_2 = req.clone();
let active_requests_clone = active_requests.clone();
let tx_clone = tx.clone();
let adapter_clone = adapter.clone();
let wake_up_tx = wake_up_tx.clone();
let handle = spawn(async move {
let mut err_msg = None;
match &req_clone {
BluerRequest::SetBluetoothEnabled(enabled) => {
if let Err(e) = adapter_clone.set_powered(*enabled).await {
tracing::error!("Failed to power off bluetooth adapter. {e:?}");
}
// rfkill will be persisted after reboot
let name = adapter_clone.name();
if let Some(id) = tokio::process::Command::new("rfkill")
.env("PATH", rfkill_path_var())
.arg("list")
.arg("-n")
.arg("--output")
.arg("ID,DEVICE")
.output()
.await
.ok()
.and_then(|o| {
let lines = String::from_utf8(o.stdout).ok()?;
lines.split('\n').into_iter().find_map(|row| {
let (id, cname) = row.trim().split_once(' ')?;
(name == cname).then_some(id.to_string())
})
})
{
if let Err(err) = tokio::process::Command::new("rfkill")
.env("PATH", rfkill_path_var())
.arg(if *enabled { "unblock" } else { "block" })
.arg(id)
.output()
.await
{
tracing::error!(
"Failed to set bluetooth state using rfkill. {err:?}"
);
}
}
if *enabled {
_ = wake_up_tx.send(()).await;
}
}
BluerRequest::PairDevice(address) => {
let res = adapter_clone.device(*address);
if let Err(err) = res {
err_msg = Some(err.to_string());
} else if let Ok(device) = res {
let res = device.pair().await;
if let Err(err) = res {
err_msg = Some(err.to_string());
} else {
if let Err(err) = device.set_trusted(true).await {
tracing::error!(?err, "Failed to trust device.");
}
}
}
}
BluerRequest::ConnectDevice(address) => {
let res = adapter_clone.device(*address);
if let Err(err) = res {
err_msg = Some(err.to_string());
} else if let Ok(device) = res {
let res = device.connect().await;
if let Err(err) = res {
err_msg = Some(err.to_string());
} else {
if let Err(err) = device.set_trusted(true).await {
tracing::error!(?err, "Failed to trust device.");
}
}
}
}
BluerRequest::DisconnectDevice(address) => {
let res = adapter_clone.device(*address);
if let Err(err) = res {
err_msg = Some(err.to_string());
} else if let Ok(device) = res {
let res = device.disconnect().await;
if let Err(err) = res {
err_msg = Some(err.to_string());
}
}
}
BluerRequest::CancelConnect(_) => {
if let Some(handle) = active_requests_clone.lock().await.get(&req_clone)
{
handle.abort();
} else {
err_msg = Some("No active connection request found".to_string());
}
}
}
let _ = tx_clone
.send(BluerSessionEvent::RequestResponse {
req: req_clone,
state: bluer_state(&adapter_clone).await,
err_msg,
})
.await;
active_requests_clone.lock().await.remove(&req_clone_2);
Ok(())
});
active_requests.lock().await.insert(req, handle);
}
Ok(())
});
}
}
#[inline]
async fn bluer_state(adapter: &Adapter) -> BluerState {
let (devices, bluetooth_enabled) = futures::join!(
build_device_list(Vec::new(), adapter),
// TODO is this a proper way of checking if bluetooth is enabled?
adapter.is_powered().map(Result::unwrap_or_default),
);
BluerState {
devices,
bluetooth_enabled,
}
}
#[inline(never)]
async fn build_device_list(mut devices: Vec<BluerDevice>, adapter: &Adapter) -> Vec<BluerDevice> {
let addrs: Vec<Address> = adapter
.device_addresses()
.await
.unwrap_or_default()
.into_iter()
.filter(|addr| !devices.iter().any(|d| d.address == *addr))
.collect();
devices.clear();
if addrs.len() > devices.capacity() {
devices.reserve(addrs.len() - devices.capacity());
}
// Concurrently collect bluer devices from each address.
let mut device_stream = addrs
.into_iter()
.filter_map(|address| adapter.device(address).ok())
.map(async move |device| BluerDevice::from_device(&device).await)
.collect::<FuturesUnordered<_>>();
while let Some(device) = device_stream.next().await {
devices.push(device);
}
devices.sort_unstable();
devices
}