cranpose_services/
power.rs1use std::sync::{
11 atomic::{AtomicU64, Ordering},
12 Arc, Mutex, OnceLock,
13};
14
15use cranpose_core::{rememberEventStream, State};
16
17use crate::registry::ServiceRegistry;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
21pub enum ThermalState {
22 #[default]
24 Normal,
25 Light,
27 Moderate,
29 Severe,
31 Critical,
33 Emergency,
35 Shutdown,
37}
38
39impl ThermalState {
40 pub fn should_pause_work(self) -> bool {
42 self >= ThermalState::Severe
43 }
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct BatteryStatus {
49 pub percent: u8,
51 pub charging: bool,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum PowerReading<T> {
62 Known(T),
64 Unsupported,
66 Unknown,
68}
69
70impl<T> PowerReading<T> {
71 pub fn known(self) -> Option<T> {
73 match self {
74 PowerReading::Known(value) => Some(value),
75 _ => None,
76 }
77 }
78
79 pub fn is_supported(&self) -> bool {
81 !matches!(self, PowerReading::Unsupported)
82 }
83
84 pub fn unwrap_or(self, fallback: T) -> T {
86 self.known().unwrap_or(fallback)
87 }
88}
89
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub struct PowerCapabilities {
93 pub thermal: bool,
95 pub battery: bool,
97 pub background_restriction: bool,
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub struct PowerState {
104 pub thermal: PowerReading<ThermalState>,
106 pub battery: PowerReading<BatteryStatus>,
108 pub unrestricted_background_work: PowerReading<bool>,
110}
111
112impl PowerState {
113 pub const fn unsupported() -> Self {
115 Self {
116 thermal: PowerReading::Unsupported,
117 battery: PowerReading::Unsupported,
118 unrestricted_background_work: PowerReading::Unsupported,
119 }
120 }
121
122 pub fn should_pause_work(&self) -> bool {
125 matches!(self.thermal, PowerReading::Known(level) if level.should_pause_work())
126 }
127}
128
129pub trait PowerMonitor: Send + Sync {
131 fn capabilities(&self) -> PowerCapabilities {
133 PowerCapabilities::default()
134 }
135
136 fn thermal_state(&self) -> PowerReading<ThermalState> {
138 PowerReading::Unsupported
139 }
140
141 fn battery_status(&self) -> PowerReading<BatteryStatus> {
143 PowerReading::Unsupported
144 }
145
146 fn unrestricted_background_work(&self) -> PowerReading<bool> {
148 PowerReading::Unsupported
149 }
150
151 fn request_unrestricted_background_work(&self) {}
153}
154
155pub type PowerMonitorRef = Arc<dyn PowerMonitor>;
157
158struct DefaultPowerMonitor;
159impl PowerMonitor for DefaultPowerMonitor {}
160
161static PLATFORM_POWER_MONITOR: ServiceRegistry<dyn PowerMonitor> = ServiceRegistry::new();
162
163pub fn set_platform_power_monitor(monitor: PowerMonitorRef) {
165 PLATFORM_POWER_MONITOR.set(monitor);
166 publish_power_state(power_state());
167}
168
169pub fn clear_platform_power_monitor() {
171 PLATFORM_POWER_MONITOR.clear();
172 if let Ok(mut observers) = power_observers().lock() {
173 observers.clear();
174 }
175}
176
177pub fn power_monitor() -> PowerMonitorRef {
179 PLATFORM_POWER_MONITOR
180 .get()
181 .unwrap_or_else(|| Arc::new(DefaultPowerMonitor))
182}
183
184pub fn power_state() -> PowerState {
186 let monitor = power_monitor();
187 PowerState {
188 thermal: monitor.thermal_state(),
189 battery: monitor.battery_status(),
190 unrestricted_background_work: monitor.unrestricted_background_work(),
191 }
192}
193
194pub fn power_capabilities() -> PowerCapabilities {
196 power_monitor().capabilities()
197}
198
199type PowerObserver = Arc<dyn Fn(PowerState) + Send + Sync>;
200
201fn power_observers() -> &'static Mutex<Vec<(u64, PowerObserver)>> {
202 static SLOT: OnceLock<Mutex<Vec<(u64, PowerObserver)>>> = OnceLock::new();
203 SLOT.get_or_init(|| Mutex::new(Vec::new()))
204}
205
206static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
207
208pub struct PowerObserverRegistration {
210 id: u64,
211}
212
213impl Drop for PowerObserverRegistration {
214 fn drop(&mut self) {
215 if let Ok(mut observers) = power_observers().lock() {
216 observers.retain(|(id, _)| *id != self.id);
217 }
218 }
219}
220
221pub fn observe_power_state(
224 observer: impl Fn(PowerState) + Send + Sync + 'static,
225) -> PowerObserverRegistration {
226 let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
227 if let Ok(mut observers) = power_observers().lock() {
228 observers.push((id, Arc::new(observer)));
229 }
230 PowerObserverRegistration { id }
231}
232
233pub fn publish_power_state(state: PowerState) {
236 let observers = power_observers()
237 .lock()
238 .map(|observers| {
239 observers
240 .iter()
241 .map(|(_, observer)| Arc::clone(observer))
242 .collect::<Vec<_>>()
243 })
244 .unwrap_or_default();
245 for observer in observers {
246 observer(state);
247 }
248}
249
250#[allow(non_snake_case)]
253pub fn rememberPowerState() -> State<PowerState> {
254 let updates = rememberEventStream((), |sender| {
255 observe_power_state(move |state| sender.send(state))
256 });
257 cranpose_core::collectAsState(updates, (), power_state())
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 struct DesktopMonitor;
265
266 impl PowerMonitor for DesktopMonitor {
267 fn capabilities(&self) -> PowerCapabilities {
268 PowerCapabilities {
269 thermal: false,
270 battery: false,
271 background_restriction: false,
272 }
273 }
274 }
275
276 struct PhoneMonitor;
277
278 impl PowerMonitor for PhoneMonitor {
279 fn capabilities(&self) -> PowerCapabilities {
280 PowerCapabilities {
281 thermal: true,
282 battery: true,
283 background_restriction: true,
284 }
285 }
286 fn thermal_state(&self) -> PowerReading<ThermalState> {
287 PowerReading::Known(ThermalState::Severe)
288 }
289 fn battery_status(&self) -> PowerReading<BatteryStatus> {
290 PowerReading::Known(BatteryStatus {
291 percent: 12,
292 charging: false,
293 })
294 }
295 fn unrestricted_background_work(&self) -> PowerReading<bool> {
296 PowerReading::Known(false)
297 }
298 }
299
300 #[test]
301 fn a_platform_without_power_apis_says_unsupported_rather_than_full() {
302 let _guard = crate::registry::test_service_guard();
303 clear_platform_power_monitor();
304 let state = power_state();
305 assert_eq!(state, PowerState::unsupported());
306 assert!(!state.thermal.is_supported());
307 assert!(!state.should_pause_work());
308 assert_eq!(power_capabilities(), PowerCapabilities::default());
309 }
310
311 #[test]
312 fn a_backend_that_measures_nothing_still_reports_its_capabilities() {
313 let _guard = crate::registry::test_service_guard();
314 set_platform_power_monitor(Arc::new(DesktopMonitor));
315 assert!(!power_capabilities().thermal);
316 assert_eq!(power_state().battery, PowerReading::Unsupported);
317 clear_platform_power_monitor();
318 }
319
320 #[test]
321 fn severe_thermal_pressure_pauses_sustained_work() {
322 let _guard = crate::registry::test_service_guard();
323 set_platform_power_monitor(Arc::new(PhoneMonitor));
324 let state = power_state();
325 assert!(state.should_pause_work());
326 assert_eq!(
327 state.battery.known().map(|battery| battery.percent),
328 Some(12)
329 );
330 assert!(!state.unrestricted_background_work.unwrap_or(true));
331 clear_platform_power_monitor();
332 }
333
334 #[test]
335 fn observers_see_published_changes_and_stop_when_dropped() {
336 let _guard = crate::registry::test_service_guard();
337 clear_platform_power_monitor();
338 let seen = Arc::new(Mutex::new(Vec::new()));
339 let recorder = Arc::clone(&seen);
340 let registration = observe_power_state(move |state| {
341 recorder
342 .lock()
343 .unwrap_or_else(|error| error.into_inner())
344 .push(state.thermal)
345 });
346 publish_power_state(PowerState {
347 thermal: PowerReading::Known(ThermalState::Moderate),
348 ..PowerState::unsupported()
349 });
350 assert_eq!(
351 seen.lock().unwrap_or_else(|e| e.into_inner()).as_slice(),
352 [PowerReading::Known(ThermalState::Moderate)]
353 );
354 drop(registration);
355 publish_power_state(PowerState::unsupported());
356 assert_eq!(seen.lock().unwrap_or_else(|e| e.into_inner()).len(), 1);
357 }
358}