uconsole_sleep/hardware/
rf.rs

1//! RF (rfkill) helpers
2use std::{
3    fs,
4    path::{Path, PathBuf},
5};
6
7use log::{debug, info, warn};
8
9pub const RFKILL_PATH_BT: &str = "/sys/class/rfkill/rfkill0";
10pub const RFKILL_PATH_WIFI: &str = "/sys/class/rfkill/rfkill1";
11
12pub fn rfkill_state_path(path: &std::path::Path) -> PathBuf {
13    path.join("state")
14}
15
16pub fn write_rfkill_state(path: &Path, block: bool, dry_run: bool) {
17    let state = rfkill_state_path(path);
18    if dry_run {
19        debug!(
20            "DRY-RUN: would write '{}' to {}",
21            if block { "0" } else { "1" },
22            state.display()
23        );
24        return;
25    }
26    let _ = std::fs::write(&state, if block { "0" } else { "1" });
27    info!(
28        "WiFi: {} via {}",
29        if block { "blocked" } else { "unblocked" },
30        state.display()
31    );
32}
33
34pub fn find_default_rfkill_path() -> Option<PathBuf> {
35    let p = PathBuf::from(RFKILL_PATH_WIFI);
36    if p.exists() { Some(p) } else { None }
37}
38
39pub fn find_default_rfkill_path_bt() -> Option<PathBuf> {
40    let p = PathBuf::from(RFKILL_PATH_BT);
41    if p.exists() { Some(p) } else { None }
42}
43
44/// Enter power-saving mode: turn display off and reduce CPU freq (order matters)
45/// RF toggling configuration
46#[derive(Clone, Debug)]
47pub struct WifiConfig {
48    pub enabled: bool,
49    pub rfkill_path: Option<PathBuf>,
50}
51
52impl WifiConfig {
53    pub fn new(enabled: bool, rfkill_path: Option<PathBuf>) -> Self {
54        let mut p = rfkill_path;
55        if enabled && p.is_none() {
56            p = Some(PathBuf::from(RFKILL_PATH_WIFI));
57        }
58        WifiConfig {
59            enabled,
60            rfkill_path: p,
61        }
62    }
63
64    pub fn block(&self, dry_run: bool) {
65        if !self.enabled {
66            return;
67        }
68        if let Some(path) = &self.rfkill_path {
69            let state = path.join("state");
70            if dry_run {
71                debug!("DRY-RUN: would write '0' to {}", state.display());
72                return;
73            }
74            let _ = fs::write(&state, "0");
75            debug!("WiFi: blocked via {}", state.display());
76        } else {
77            warn!("WiFi toggling enabled but no rfkill path provided");
78        }
79    }
80
81    pub fn unblock(&self, dry_run: bool) {
82        if !self.enabled {
83            return;
84        }
85        if let Some(path) = &self.rfkill_path {
86            let state = path.join("state");
87            if dry_run {
88                debug!("DRY-RUN: would write '1' to {}", state.display());
89                return;
90            }
91            let _ = fs::write(&state, "1");
92            debug!("WiFi: unblocked via {}", state.display());
93        } else {
94            warn!("WiFi toggling enabled but no rfkill path provided");
95        }
96    }
97}
98
99/// Bluetooth (BT) toggling configuration
100#[derive(Clone, Debug)]
101pub struct BTConfig {
102    pub enabled: bool,
103    pub rfkill_path: Option<PathBuf>,
104}
105
106impl BTConfig {
107    pub fn new(enabled: bool, rfkill_path: Option<PathBuf>) -> Self {
108        let mut p = rfkill_path;
109        if enabled && p.is_none() {
110            p = Some(PathBuf::from(RFKILL_PATH_BT));
111        }
112        BTConfig {
113            enabled,
114            rfkill_path: p,
115        }
116    }
117
118    pub fn block(&self, dry_run: bool) {
119        if !self.enabled {
120            return;
121        }
122        if let Some(path) = &self.rfkill_path {
123            let state = path.join("state");
124            if dry_run {
125                debug!("DRY-RUN: would write '0' to {}", state.display());
126                return;
127            }
128            let _ = fs::write(&state, "0");
129            debug!("BT: blocked via {}", state.display());
130        } else {
131            warn!("BT toggling enabled but no rfkill path provided");
132        }
133    }
134
135    pub fn unblock(&self, dry_run: bool) {
136        if !self.enabled {
137            return;
138        }
139        if let Some(path) = &self.rfkill_path {
140            let state = path.join("state");
141            if dry_run {
142                debug!("DRY-RUN: would write '1' to {}", state.display());
143                return;
144            }
145            let _ = fs::write(&state, "1");
146            debug!("BT: unblocked via {}", state.display());
147        } else {
148            warn!("BT toggling enabled but no rfkill path provided");
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use std::env;
157    use std::fs;
158
159    #[test]
160    fn test_find_default_rfkill_path() {
161        let _ = find_default_rfkill_path();
162    }
163
164    #[test]
165    fn test_find_default_rfkill_path_bt() {
166        let _ = find_default_rfkill_path_bt();
167    }
168
169    #[test]
170    fn test_write_rfkill_state_dry_run() {
171        let tmp = env::temp_dir().join(format!(
172            "uconsole_wifi_{}",
173            std::time::SystemTime::now()
174                .duration_since(std::time::UNIX_EPOCH)
175                .unwrap()
176                .as_millis()
177        ));
178        let _ = fs::create_dir_all(&tmp);
179        fs::write(tmp.join("state"), "0").unwrap();
180        write_rfkill_state(&tmp, true, true);
181        // dry run should not change
182        let s = fs::read_to_string(tmp.join("state")).unwrap();
183        assert_eq!(s, "0");
184    }
185
186    #[test]
187    fn test_write_rfkill_state_dry_run_bt() {
188        let tmp = env::temp_dir().join(format!(
189            "uconsole_bt_{}",
190            std::time::SystemTime::now()
191                .duration_since(std::time::UNIX_EPOCH)
192                .unwrap()
193                .as_millis()
194        ));
195        let _ = fs::create_dir_all(&tmp);
196        fs::write(tmp.join("state"), "0").unwrap();
197        write_rfkill_state(&tmp, true, true);
198        // dry run should not change
199        let s = fs::read_to_string(tmp.join("state")).unwrap();
200        assert_eq!(s, "0");
201    }
202}