Skip to main content

cadmus_core/device/power/kobo/
mod.rs

1//! Kobo Power Manager implementation.
2//!
3//! This module provides low-level control over Kobo hardware power states.
4//! It handles touch screen power state transitions, filesystem buffer flushes,
5//! and writing to kernel sysfs nodes to trigger suspend to RAM.
6
7use crate::device::Model;
8use crate::device::power::error::PowerError;
9use crate::device::power::manager::PowerManager;
10use std::fs;
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13use std::thread;
14use std::time::Duration;
15
16const STATE_EXTENDED_PATH: &str = "/sys/power/state-extended";
17const STATE_PATH: &str = "/sys/power/state";
18const NEOCMD_PATH: &str = "/sys/devices/virtual/input/input1/neocmd";
19
20/// Kobo-specific power manager.
21///
22/// Manages the low-level hardware sleep/wake cycle on Kobo e-reader devices.
23/// It interacts directly with the Linux sysfs interface to manage screen power
24/// and RAM suspension.
25///
26/// # Example
27///
28/// ```ignore
29/// use cadmus_core::device::Model;
30/// use cadmus_core::device::power::PowerManager;
31///
32/// // Access via the global device singleton
33/// if let Ok(power) = CURRENT_DEVICE.power_manager() {
34///     power.suspend().ok();
35/// }
36/// ```
37pub struct KoboPowerManager {
38    model: Model,
39    initial_cpu_states: Mutex<Vec<PathBuf>>,
40}
41
42impl KoboPowerManager {
43    /// Creates a new `KoboPowerManager` for the specified device model.
44    pub fn new(model: Model) -> Self {
45        KoboPowerManager {
46            model,
47            initial_cpu_states: Mutex::new(Vec::new()),
48        }
49    }
50}
51
52impl PowerManager for KoboPowerManager {
53    /// Suspends the Kobo device.
54    ///
55    /// This method performs a sequenced hardware shutdown:
56    /// 1. Deactivates the touch screen to prevent phantom touches on wake up.
57    /// 2. Sleeps for 2 seconds to allow pending sysfs writes to finalize safely.
58    /// 3. Synchronizes filesystem buffers (`sync()`).
59    /// 4. Writes `"mem"` to `/sys/power/state` to trigger low-power RAM suspension.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`PowerError::Io`] if writing to any of the sysfs control nodes fails.
64    fn suspend(&self) -> Result<(), PowerError> {
65        tracing::info!("Suspending device to RAM");
66        tracing::debug!(path = %STATE_EXTENDED_PATH, value = "1", "Deactivating touch screen");
67
68        fs::write(STATE_EXTENDED_PATH, "1").map_err(|e| {
69            tracing::error!(error = %e, path = %STATE_EXTENDED_PATH, "Failed to deactivate touch screen");
70
71            PowerError::Io(e)
72        })?;
73
74        tracing::debug!("Sleeping to prevent write errors");
75        thread::sleep(Duration::from_secs(2));
76        tracing::debug!("Synchronizing file system buffers");
77
78        nix::unistd::sync();
79
80        tracing::debug!(path = %STATE_PATH, value = "mem", "Triggering low power state");
81
82        fs::write(STATE_PATH, "mem").map_err(|e| {
83            tracing::error!(error = %e, path = %STATE_PATH, "Failed to write suspend trigger");
84
85            PowerError::Io(e)
86        })?;
87
88        Ok(())
89    }
90
91    /// Resumes the Kobo device.
92    ///
93    /// This method performs the following wakeup tasks:
94    /// 1. Reactivates the touch screen by writing `"0"` to the state-extended node.
95    /// 2. If the model is a `GloHD` or `AuraH2O`, writes `"a"` to the `neocmd` node
96    ///    to re-initialize the touch controller.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`PowerError::Io`] if writing to any of the sysfs wake up nodes fails.
101    fn resume(&self) -> Result<(), PowerError> {
102        tracing::info!("Resuming device");
103        tracing::debug!(path = %STATE_EXTENDED_PATH, value = "0", "Reactivating touch screen");
104
105        fs::write(STATE_EXTENDED_PATH, "0").map_err(|e| {
106            tracing::error!(error = %e, path = %STATE_EXTENDED_PATH, "Failed to reactivate touch screen");
107
108            PowerError::Io(e)
109        })?;
110
111        match self.model {
112            Model::GloHD | Model::AuraH2O => {
113                tracing::debug!(path = %NEOCMD_PATH, value = "a", "Reinitializing touch controller");
114
115                fs::write(NEOCMD_PATH, "a").map_err(|e| {
116                    tracing::warn!(error = %e, path = %NEOCMD_PATH, "Failed to write neocmd");
117
118                    PowerError::Io(e)
119                })?;
120            }
121            _ => {}
122        }
123
124        Ok(())
125    }
126
127    fn init_cores(&self) -> Result<(), PowerError> {
128        let cpu_dir = Path::new("/sys/devices/system/cpu");
129        let discovered = super::discover_cores(cpu_dir).map_err(|e| {
130            tracing::warn!(error = %e, "Failed to discover CPU cores");
131            e
132        })?;
133
134        let mut modified_cores = Vec::new();
135        let mut first_error: Option<PowerError> = None;
136
137        for (online_path, initial_state) in discovered {
138            if initial_state == "0" {
139                tracing::info!(path = ?online_path, "Enabling offline CPU core");
140                match fs::write(&online_path, "1") {
141                    Ok(()) => modified_cores.push(online_path),
142                    Err(e) => {
143                        tracing::error!(path = ?online_path, error = %e, "Failed to enable CPU core");
144                        if first_error.is_none() {
145                            first_error = Some(PowerError::Io(e));
146                        }
147                    }
148                }
149            }
150        }
151
152        let mut states = self
153            .initial_cpu_states
154            .lock()
155            .map_err(|_| PowerError::LockPoisoned)?;
156        *states = modified_cores;
157
158        match first_error {
159            Some(e) => Err(e),
160            None => Ok(()),
161        }
162    }
163
164    fn restore_cores(&self) -> Result<(), PowerError> {
165        let states = self
166            .initial_cpu_states
167            .lock()
168            .map_err(|_| PowerError::LockPoisoned)?;
169
170        let mut first_error: Option<PowerError> = None;
171
172        for path in states.iter() {
173            tracing::info!(path = ?path, "Disabling CPU core on exit");
174            if let Err(e) = fs::write(path, "0") {
175                tracing::error!(path = ?path, error = %e, "Failed to restore CPU core state");
176                if first_error.is_none() {
177                    first_error = Some(PowerError::Io(e));
178                }
179            }
180        }
181
182        match first_error {
183            Some(e) => Err(e),
184            None => Ok(()),
185        }
186    }
187}
188
189/// Creates a Kobo PowerManager instance.
190///
191/// This factory function instantiates a box-wrapped `KoboPowerManager` implementing
192/// the [`PowerManager`] trait.
193pub fn create_power_manager(model: Model) -> Result<Box<dyn PowerManager>, PowerError> {
194    Ok(Box::new(KoboPowerManager::new(model)))
195}