147 lines
4.8 KiB
Rust
147 lines
4.8 KiB
Rust
// Copyright 2024 System76 <info@system76.com>
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
use futures::{FutureExt, StreamExt};
|
|
use iced_futures::Subscription;
|
|
use std::collections::HashMap;
|
|
|
|
pub fn subscription() -> iced_futures::Subscription<bool> {
|
|
struct MyId;
|
|
|
|
Subscription::run_with(std::any::TypeId::of::<MyId>(), |_| {
|
|
async {
|
|
match rfkill::rfkill_updates() {
|
|
Ok(updates) => updates.filter_map(|state| async {
|
|
match state {
|
|
Ok(state) => Some(is_airplane_mode(&state)),
|
|
Err(err) => {
|
|
log::error!("Failed to read rfkill: {}", err);
|
|
None
|
|
}
|
|
}
|
|
}),
|
|
Err(err) => {
|
|
log::error!("Failed to monitor rfkill: {}", err);
|
|
futures::future::pending().await
|
|
}
|
|
}
|
|
}
|
|
.flatten_stream()
|
|
})
|
|
}
|
|
|
|
// Test that:
|
|
// - There is at least one device
|
|
// - All devices have either a hard or soft block active
|
|
fn is_airplane_mode(rfkill_state: &HashMap<u32, rfkill::DeviceState>) -> bool {
|
|
!rfkill_state.is_empty()
|
|
&& rfkill_state
|
|
.values()
|
|
.all(|device_state| device_state.hard || device_state.soft)
|
|
}
|
|
|
|
mod rfkill {
|
|
use futures::stream::Stream;
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
use std::{collections::HashMap, fs, io, mem, slice};
|
|
use tokio::io::unix::AsyncFd;
|
|
|
|
// /usr/include/linux/rfkill.h
|
|
// https://www.kernel.org/doc/html/latest/driver-api/rfkill.html#id5
|
|
//
|
|
// The preferred way to get rfkill events is by reading /dev/rfkill. We can
|
|
// simply poll the file descriptor (using tokio's async reactor) and reading
|
|
// one event per `read` system call.
|
|
|
|
const RFKILL_OP_ADD: u8 = 0;
|
|
const RFKILL_OP_DEL: u8 = 1;
|
|
const RFKILL_OP_CHANGE: u8 = 2;
|
|
|
|
#[repr(C)]
|
|
#[derive(Debug, Copy, Clone, Default)]
|
|
#[allow(non_camel_case_types)]
|
|
pub struct rfkill_event {
|
|
pub idx: u32,
|
|
pub type_: u8,
|
|
pub op: u8,
|
|
pub soft: u8,
|
|
pub hard: u8,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
#[allow(dead_code)]
|
|
pub struct DeviceState {
|
|
pub type_: u8,
|
|
pub soft: bool,
|
|
pub hard: bool,
|
|
}
|
|
|
|
pub fn rfkill_updates()
|
|
-> io::Result<impl Stream<Item = io::Result<HashMap<u32, DeviceState>>> + Unpin> {
|
|
struct State {
|
|
file: AsyncFd<fs::File>,
|
|
devices: HashMap<u32, DeviceState>,
|
|
}
|
|
|
|
let file = fs::File::options()
|
|
.read(true)
|
|
.custom_flags(rustix::fs::OFlags::NONBLOCK.bits() as _)
|
|
.open("/dev/rfkill")?;
|
|
|
|
let state = State {
|
|
file: AsyncFd::new(file)?,
|
|
devices: HashMap::new(),
|
|
};
|
|
|
|
Ok(futures::stream::unfold(state, |mut state| {
|
|
Box::pin(async {
|
|
let mut guard = match state.file.readable().await {
|
|
Ok(guard) => guard,
|
|
Err(err) => {
|
|
return Some((Err(err), state));
|
|
}
|
|
};
|
|
let mut event = rfkill_event::default();
|
|
// Read as many events as we can until it returns `EWOULDBLOCK`,
|
|
// then yield new state after these updates.
|
|
loop {
|
|
match read_event(guard.get_inner(), &mut event) {
|
|
Ok(()) => (),
|
|
Err(rustix::io::Errno::WOULDBLOCK) => {
|
|
break;
|
|
}
|
|
Err(err) => {
|
|
return Some((Err(err.into()), state));
|
|
}
|
|
};
|
|
match event.op {
|
|
RFKILL_OP_ADD | RFKILL_OP_CHANGE => {
|
|
state.devices.insert(
|
|
event.idx,
|
|
DeviceState {
|
|
type_: event.type_,
|
|
soft: event.soft != 0,
|
|
hard: event.hard != 0,
|
|
},
|
|
);
|
|
}
|
|
RFKILL_OP_DEL => {
|
|
state.devices.remove(&event.idx);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
guard.clear_ready();
|
|
Some((Ok(state.devices.clone()), state))
|
|
})
|
|
}))
|
|
}
|
|
|
|
fn read_event(dev: &fs::File, event: &mut rfkill_event) -> rustix::io::Result<()> {
|
|
let bytes = unsafe {
|
|
slice::from_raw_parts_mut(event as *mut _ as *mut u8, mem::size_of::<rfkill_event>())
|
|
};
|
|
rustix::io::read(dev, bytes)?;
|
|
Ok(())
|
|
}
|
|
}
|