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