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