Skip to main content

kobo_core/device/
power.rs

1use std::fs;
2use std::path::Path;
3use std::path::PathBuf;
4
5use log::debug;
6
7const WAKE_LOCK_PATH: &str = "/sys/power/wake_lock";
8const WAKE_UNLOCK_PATH: &str = "/sys/power/wake_unlock";
9const WAKE_LOCK_NAME: &str = "dev_probe";
10
11pub struct WakeLock {
12    held: bool,
13}
14
15impl WakeLock {
16    pub fn acquire() -> Self {
17        // best-effort: sysfs write; wakelock is advisory
18        let _ = fs::write(WAKE_LOCK_PATH, WAKE_LOCK_NAME);
19        debug!("pwr: wakelock ACQUIRED");
20        WakeLock { held: true }
21    }
22}
23
24impl Drop for WakeLock {
25    fn drop(&mut self) {
26        if self.held {
27            // best-effort: sysfs write on process exit
28            let _ = fs::write(WAKE_UNLOCK_PATH, WAKE_LOCK_NAME);
29        }
30    }
31}
32
33/// Pure: the configured brightness-path for a frontlight config.
34pub fn known_brightness_path(fl_cfg: &crate::device::config::FrontlightConfig) -> PathBuf {
35    PathBuf::from(&fl_cfg.brightness_path)
36}
37
38/// Pure: given scanned (path, current_value) candidates, pick the highest-valued
39/// entry whose value is > 0, or None. Mirrors the selection in frontlight_path.
40pub fn pick_backlight(entries: &[(PathBuf, u32)]) -> Option<PathBuf> {
41    entries
42        .iter()
43        .filter(|(_, v)| *v > 0)
44        .max_by_key(|(_, v)| *v)
45        .map(|(p, _)| p.clone())
46}
47
48pub fn frontlight_path(fl_cfg: &crate::device::config::FrontlightConfig) -> Option<PathBuf> {
49    let known = known_brightness_path(fl_cfg);
50    if known.exists() {
51        return Some(known);
52    }
53    let mut candidates: Vec<(PathBuf, u32)> = Vec::new();
54    for entry in fs::read_dir("/sys/class/backlight").ok()?.flatten() {
55        let path = entry.path().join("brightness");
56        if path.exists() {
57            if let Some(val) = frontlight_get(&path) {
58                candidates.push((path, val));
59            }
60        }
61    }
62    pick_backlight(&candidates)
63}
64
65pub fn frontlight_get(path: &Path) -> Option<u32> {
66    fs::read_to_string(path)
67        .ok()
68        .and_then(|s| s.trim().parse().ok())
69}
70
71pub fn frontlight_set(path: &Path, val: u32) {
72    // best-effort: sysfs write; backlight driver may be asleep
73    let _ = fs::write(path, val.to_string());
74}
75
76/// Restore the frontlight to `brightness`, retrying: after a long sleep the I2C
77/// bus may need a moment to accept the write. Shared by the book-view and
78/// library-view wake paths so both illuminate identically.
79pub const FL_RESTORE_RETRIES: u32 = 5;
80pub const FL_RESTORE_INTERVAL_MS: u64 = 30;
81
82/// Pure retry-loop: write `brightness`, confirm it took, retry up to `retries`
83/// times. Returns the number of `set` calls made (1 on immediate success, up
84/// to `retries` on persistent failure). `sleep` is injected so tests run with
85/// zero real delay.
86pub fn restore_frontlight_with(
87    fl: &dyn crate::device::traits::Frontlight,
88    brightness: u32,
89    retries: u32,
90    sleep: impl Fn(),
91) -> u32 {
92    let mut attempts = 0u32;
93    for attempt in 0..retries {
94        attempts += 1;
95        fl.set(brightness);
96        sleep();
97        if fl.get() == brightness {
98            debug!(
99                "pwr: frontlight restored to {} (attempt {})",
100                brightness,
101                attempt + 1
102            );
103            break;
104        }
105        debug!(
106            "pwr: frontlight retry {}/{} , got {}",
107            attempt + 1,
108            retries,
109            fl.get()
110        );
111    }
112    attempts
113}
114
115struct PathFrontlight<'a> {
116    path: &'a Path,
117}
118
119impl<'a> crate::device::traits::Frontlight for PathFrontlight<'a> {
120    fn set(&self, val: u32) {
121        frontlight_set(self.path, val);
122    }
123    fn get(&self) -> u32 {
124        frontlight_get(self.path).unwrap_or(0)
125    }
126    fn restore(&self, brightness: u32) {
127        restore_frontlight(self.path, brightness);
128    }
129}
130
131pub fn restore_frontlight(path: &Path, brightness: u32) {
132    let fl = PathFrontlight { path };
133    restore_frontlight_with(&fl, brightness, FL_RESTORE_RETRIES, || {
134        std::thread::sleep(std::time::Duration::from_millis(FL_RESTORE_INTERVAL_MS));
135    });
136}
137
138#[allow(dead_code)]
139pub fn kernel_suspend(state: &str) {
140    debug!("pwr: entering kernel suspend ({})", state);
141    // best-effort: write may fail if the kernel rejects the requested state
142    let _ = fs::write("/sys/power/state", state);
143    debug!("pwr: resumed from kernel suspend");
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::device::config::FrontlightConfig;
150
151    #[test]
152    fn known_brightness_path_returns_configured() {
153        let cfg = FrontlightConfig {
154            brightness_path: "/sys/class/backlight/lm3630a_led/brightness".into(),
155            mixer_path: None,
156            nl_min: 0,
157            nl_max: 100,
158            nl_inverted: false,
159        };
160        assert_eq!(
161            known_brightness_path(&cfg),
162            PathBuf::from(&cfg.brightness_path)
163        );
164    }
165
166    #[test]
167    fn pick_backlight_picks_highest_nonzero() {
168        let e = [
169            (PathBuf::from("/sys/a"), 0),
170            (PathBuf::from("/sys/b"), 42),
171            (PathBuf::from("/sys/c"), 99),
172            (PathBuf::from("/sys/d"), 7),
173        ];
174        assert_eq!(pick_backlight(&e), Some(PathBuf::from("/sys/c")));
175    }
176
177    #[test]
178    fn pick_backlight_none_when_all_zero() {
179        let e = [(PathBuf::from("/sys/a"), 0), (PathBuf::from("/sys/b"), 0)];
180        assert_eq!(pick_backlight(&e), None);
181        assert_eq!(pick_backlight(&[]), None);
182    }
183
184    use std::cell::{Cell, RefCell};
185
186    /// Mock frontlight: records every `set` value; `get` returns the value at
187    /// the current scripted index (repeating the last once exhausted).
188    struct MockFl {
189        sets: RefCell<Vec<u32>>,
190        gets: Cell<usize>,
191        scripted: Vec<u32>,
192    }
193
194    impl MockFl {
195        fn new(gets: Vec<u32>) -> Self {
196            MockFl {
197                sets: RefCell::new(Vec::new()),
198                gets: Cell::new(0),
199                scripted: gets,
200            }
201        }
202    }
203
204    impl crate::device::traits::Frontlight for &MockFl {
205        fn set(&self, val: u32) {
206            self.sets.borrow_mut().push(val);
207        }
208        fn get(&self) -> u32 {
209            let i = self.gets.get();
210            let v = self
211                .scripted
212                .get(i)
213                .copied()
214                .unwrap_or_else(|| self.scripted.last().copied().unwrap_or(0));
215            self.gets.set(i + 1);
216            v
217        }
218        fn restore(&self, brightness: u32) {
219            self.set(brightness);
220        }
221    }
222
223    #[test]
224    fn restore_frontlight_succeeds_first_try() {
225        let fl = MockFl::new(vec![60]);
226        let attempts = restore_frontlight_with(&(&fl), 60, FL_RESTORE_RETRIES, || {});
227        assert_eq!(attempts, 1, "converged on the first attempt");
228        assert_eq!(*fl.sets.borrow(), vec![60]);
229    }
230
231    #[test]
232    fn restore_frontlight_retries_until_convergence() {
233        let fl = MockFl::new(vec![0, 0, 60]);
234        let attempts = restore_frontlight_with(&(&fl), 60, FL_RESTORE_RETRIES, || {});
235        assert_eq!(attempts, 3, "two failures then success");
236        assert_eq!(
237            *fl.sets.borrow(),
238            vec![60, 60, 60],
239            "set retried each attempt"
240        );
241    }
242
243    #[test]
244    fn restore_frontlight_gives_up_after_max_retries() {
245        let fl = MockFl::new(vec![0]);
246        let attempts = restore_frontlight_with(&(&fl), 60, FL_RESTORE_RETRIES, || {});
247        assert_eq!(
248            attempts, FL_RESTORE_RETRIES,
249            "never converges -> retries exhausted"
250        );
251        assert_eq!(fl.sets.borrow().len(), FL_RESTORE_RETRIES as usize);
252    }
253
254    #[test]
255    fn restore_frontlight_zero_retries_is_single_attempt() {
256        let fl = MockFl::new(vec![60]);
257        let attempts = restore_frontlight_with(&(&fl), 60, 0, || {});
258        assert_eq!(attempts, 0, "zero retries means the loop never runs");
259        assert!(fl.sets.borrow().is_empty());
260    }
261}