cranpose_services/
power.rs1use std::sync::{
11 Arc, Mutex, OnceLock,
12 atomic::{AtomicU64, Ordering},
13};
14
15use cranpose_core::{State, rememberEventStream};
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)]
253#[track_caller]
254pub fn rememberPowerState() -> State<PowerState> {
255 let updates = rememberEventStream((), |sender| {
256 observe_power_state(move |state| sender.send(state))
257 });
258 cranpose_core::collectAsState(updates, (), power_state())
259}
260
261#[cfg(test)]
262mod tests {
263 use std::sync::PoisonError;
264
265 use super::*;
266
267 struct DesktopMonitor;
268
269 impl PowerMonitor for DesktopMonitor {
270 fn capabilities(&self) -> PowerCapabilities {
271 PowerCapabilities {
272 thermal: false,
273 battery: false,
274 background_restriction: false,
275 }
276 }
277 }
278
279 struct PhoneMonitor;
280
281 impl PowerMonitor for PhoneMonitor {
282 fn capabilities(&self) -> PowerCapabilities {
283 PowerCapabilities {
284 thermal: true,
285 battery: true,
286 background_restriction: true,
287 }
288 }
289 fn thermal_state(&self) -> PowerReading<ThermalState> {
290 PowerReading::Known(ThermalState::Severe)
291 }
292 fn battery_status(&self) -> PowerReading<BatteryStatus> {
293 PowerReading::Known(BatteryStatus {
294 percent: 12,
295 charging: false,
296 })
297 }
298 fn unrestricted_background_work(&self) -> PowerReading<bool> {
299 PowerReading::Known(false)
300 }
301 }
302
303 #[test]
304 fn a_platform_without_power_apis_says_unsupported_rather_than_full() {
305 let _guard = crate::registry::test_service_guard();
306 clear_platform_power_monitor();
307 let state = power_state();
308 assert_eq!(state, PowerState::unsupported());
309 assert!(!state.thermal.is_supported());
310 assert!(!state.should_pause_work());
311 assert_eq!(power_capabilities(), PowerCapabilities::default());
312 }
313
314 #[test]
315 fn a_backend_that_measures_nothing_still_reports_its_capabilities() {
316 let _guard = crate::registry::test_service_guard();
317 set_platform_power_monitor(Arc::new(DesktopMonitor));
318 assert!(!power_capabilities().thermal);
319 assert_eq!(power_state().battery, PowerReading::Unsupported);
320 clear_platform_power_monitor();
321 }
322
323 #[test]
324 fn severe_thermal_pressure_pauses_sustained_work() {
325 let _guard = crate::registry::test_service_guard();
326 set_platform_power_monitor(Arc::new(PhoneMonitor));
327 let state = power_state();
328 assert!(state.should_pause_work());
329 assert_eq!(
330 state.battery.known().map(|battery| battery.percent),
331 Some(12)
332 );
333 assert!(!state.unrestricted_background_work.unwrap_or(true));
334 clear_platform_power_monitor();
335 }
336
337 #[test]
338 fn observers_see_published_changes_and_stop_when_dropped() {
339 let _guard = crate::registry::test_service_guard();
340 clear_platform_power_monitor();
341 let seen = Arc::new(Mutex::new(Vec::new()));
342 let recorder = Arc::clone(&seen);
343 let registration = observe_power_state(move |state| {
344 recorder
345 .lock()
346 .unwrap_or_else(PoisonError::into_inner)
347 .push(state.thermal);
348 });
349 publish_power_state(PowerState {
350 thermal: PowerReading::Known(ThermalState::Moderate),
351 ..PowerState::unsupported()
352 });
353 assert_eq!(
354 seen.lock()
355 .unwrap_or_else(PoisonError::into_inner)
356 .as_slice(),
357 [PowerReading::Known(ThermalState::Moderate)]
358 );
359 drop(registration);
360 publish_power_state(PowerState::unsupported());
361 assert_eq!(seen.lock().unwrap_or_else(PoisonError::into_inner).len(), 1);
362 }
363}