kobo_core/device/
power.rs1use 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 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 let _ = fs::write(WAKE_UNLOCK_PATH, WAKE_LOCK_NAME);
28 }
29 }
30}
31
32pub fn known_brightness_path(fl_cfg: &crate::device::config::FrontlightConfig) -> PathBuf {
34 PathBuf::from(&fl_cfg.brightness_path)
35}
36
37pub 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 let _ = fs::write(path, val.to_string());
73}
74
75pub const FL_RESTORE_RETRIES: u32 = 5;
79pub const FL_RESTORE_INTERVAL_MS: u64 = 30;
80
81pub 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 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 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}