cranpose_services/
power.rs1use std::sync::{Arc, Mutex, OnceLock, atomic::Ordering};
11
12use cranpose_core::{State, rememberEventStream};
13use portable_atomic::AtomicU64;
14
15use crate::registry::ServiceRegistry;
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
19pub enum ThermalState {
20 #[default]
22 Normal,
23 Light,
25 Moderate,
27 Severe,
29 Critical,
31 Emergency,
33 Shutdown,
35}
36
37impl ThermalState {
38 pub fn should_pause_work(self) -> bool {
40 self >= ThermalState::Severe
41 }
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct BatteryStatus {
47 pub percent: u8,
49 pub charging: bool,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum PowerReading<T> {
60 Known(T),
62 Unsupported,
64 Unknown,
66}
67
68impl<T> PowerReading<T> {
69 pub fn known(self) -> Option<T> {
71 match self {
72 PowerReading::Known(value) => Some(value),
73 _ => None,
74 }
75 }
76
77 pub fn is_supported(&self) -> bool {
79 !matches!(self, PowerReading::Unsupported)
80 }
81
82 pub fn unwrap_or(self, fallback: T) -> T {
84 self.known().unwrap_or(fallback)
85 }
86}
87
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub struct PowerCapabilities {
91 pub thermal: bool,
93 pub battery: bool,
95 pub background_restriction: bool,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct PowerState {
102 pub thermal: PowerReading<ThermalState>,
104 pub battery: PowerReading<BatteryStatus>,
106 pub unrestricted_background_work: PowerReading<bool>,
108}
109
110impl PowerState {
111 pub const fn unsupported() -> Self {
113 Self {
114 thermal: PowerReading::Unsupported,
115 battery: PowerReading::Unsupported,
116 unrestricted_background_work: PowerReading::Unsupported,
117 }
118 }
119
120 pub fn should_pause_work(&self) -> bool {
123 matches!(self.thermal, PowerReading::Known(level) if level.should_pause_work())
124 }
125}
126
127pub trait PowerMonitor: Send + Sync {
129 fn capabilities(&self) -> PowerCapabilities {
131 PowerCapabilities::default()
132 }
133
134 fn thermal_state(&self) -> PowerReading<ThermalState> {
136 PowerReading::Unsupported
137 }
138
139 fn battery_status(&self) -> PowerReading<BatteryStatus> {
141 PowerReading::Unsupported
142 }
143
144 fn unrestricted_background_work(&self) -> PowerReading<bool> {
146 PowerReading::Unsupported
147 }
148
149 fn request_unrestricted_background_work(&self) {}
151}
152
153pub type PowerMonitorRef = Arc<dyn PowerMonitor>;
155
156struct DefaultPowerMonitor;
157impl PowerMonitor for DefaultPowerMonitor {}
158
159static PLATFORM_POWER_MONITOR: ServiceRegistry<dyn PowerMonitor> = ServiceRegistry::new();
160
161pub fn set_platform_power_monitor(monitor: PowerMonitorRef) {
163 PLATFORM_POWER_MONITOR.set(monitor);
164 publish_power_state(power_state());
165}
166
167pub fn clear_platform_power_monitor() {
169 PLATFORM_POWER_MONITOR.clear();
170 if let Ok(mut observers) = power_observers().lock() {
171 observers.clear();
172 }
173}
174
175pub fn power_monitor() -> PowerMonitorRef {
177 PLATFORM_POWER_MONITOR
178 .get()
179 .unwrap_or_else(|| Arc::new(DefaultPowerMonitor))
180}
181
182pub fn power_state() -> PowerState {
184 let monitor = power_monitor();
185 PowerState {
186 thermal: monitor.thermal_state(),
187 battery: monitor.battery_status(),
188 unrestricted_background_work: monitor.unrestricted_background_work(),
189 }
190}
191
192pub fn power_capabilities() -> PowerCapabilities {
194 power_monitor().capabilities()
195}
196
197type PowerObserver = Arc<dyn Fn(PowerState) + Send + Sync>;
198
199fn power_observers() -> &'static Mutex<Vec<(u64, PowerObserver)>> {
200 static SLOT: OnceLock<Mutex<Vec<(u64, PowerObserver)>>> = OnceLock::new();
201 SLOT.get_or_init(|| Mutex::new(Vec::new()))
202}
203
204static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
205
206pub struct PowerObserverRegistration {
208 id: u64,
209}
210
211impl Drop for PowerObserverRegistration {
212 fn drop(&mut self) {
213 if let Ok(mut observers) = power_observers().lock() {
214 observers.retain(|(id, _)| *id != self.id);
215 }
216 }
217}
218
219pub fn observe_power_state(
222 observer: impl Fn(PowerState) + Send + Sync + 'static,
223) -> PowerObserverRegistration {
224 let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
225 if let Ok(mut observers) = power_observers().lock() {
226 observers.push((id, Arc::new(observer)));
227 }
228 PowerObserverRegistration { id }
229}
230
231pub fn publish_power_state(state: PowerState) {
234 let observers = power_observers()
235 .lock()
236 .map(|observers| {
237 observers
238 .iter()
239 .map(|(_, observer)| Arc::clone(observer))
240 .collect::<Vec<_>>()
241 })
242 .unwrap_or_default();
243 for observer in observers {
244 observer(state);
245 }
246}
247
248#[expect(non_snake_case)]
251#[track_caller]
252pub fn rememberPowerState() -> State<PowerState> {
253 let updates = rememberEventStream((), |sender| {
254 observe_power_state(move |state| sender.send(state))
255 });
256 cranpose_core::collectAsState(updates, (), power_state())
257}
258
259#[cfg(test)]
260#[path = "tests/power_tests.rs"]
261mod tests;