Skip to main content

kobo_core/device/
power.rs

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