cranpose_services/
heart_rate.rs1use std::sync::{
24 Arc, Mutex, OnceLock,
25 atomic::{AtomicUsize, Ordering},
26};
27
28use cranpose_core::{State, rememberEventStream};
29use portable_atomic::AtomicU64;
30
31use crate::registry::ServiceRegistry;
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum HeartRatePermission {
36 #[default]
38 NotAsked,
39 Granted,
41 Denied,
43}
44
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
47pub enum HeartRateStatus {
48 #[default]
51 Unavailable,
52 NeedsPermission,
55 Denied,
57 Idle,
59 Acquiring,
61 Live,
63 OffBody,
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq)]
69pub struct HeartRate {
70 pub status: HeartRateStatus,
72 pub bpm: Option<f32>,
75}
76
77impl HeartRate {
78 pub fn live_bpm(&self) -> Option<f32> {
80 match self.status {
81 HeartRateStatus::Live => self.bpm,
82 _ => None,
83 }
84 }
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum HeartRateError {
90 Unavailable,
92 NotPermitted,
94 Failed(String),
96}
97
98pub trait HeartRateMonitor: Send + Sync {
100 fn available(&self) -> bool;
102 fn permission(&self) -> HeartRatePermission;
104 fn request_permission(&self);
107 fn start(&self) -> Result<(), HeartRateError>;
109 fn stop(&self);
111}
112
113pub type HeartRateMonitorRef = Arc<dyn HeartRateMonitor>;
115
116static PLATFORM_HEART_RATE: ServiceRegistry<dyn HeartRateMonitor> = ServiceRegistry::new();
117
118pub fn set_platform_heart_rate(monitor: HeartRateMonitorRef) {
120 PLATFORM_HEART_RATE.set(monitor);
121 publish_heart_rate(resting_reading());
122}
123
124pub fn clear_platform_heart_rate() {
126 PLATFORM_HEART_RATE.clear();
127 READERS.store(0, Ordering::Release);
128 publish_heart_rate(HeartRate::default());
129}
130
131pub fn heart_rate_available() -> bool {
133 PLATFORM_HEART_RATE
134 .get()
135 .is_some_and(|monitor| monitor.available())
136}
137
138pub fn heart_rate_permission() -> HeartRatePermission {
140 PLATFORM_HEART_RATE
141 .get()
142 .filter(|monitor| monitor.available())
143 .map_or(HeartRatePermission::Denied, |monitor| monitor.permission())
144}
145
146pub fn request_heart_rate_permission() {
149 if let Some(monitor) = PLATFORM_HEART_RATE
150 .get()
151 .filter(|monitor| monitor.available())
152 && monitor.permission() != HeartRatePermission::Granted
153 {
154 monitor.request_permission();
155 }
156}
157
158fn resting_reading() -> HeartRate {
160 let status = match PLATFORM_HEART_RATE
161 .get()
162 .filter(|monitor| monitor.available())
163 {
164 None => HeartRateStatus::Unavailable,
165 Some(monitor) => match monitor.permission() {
166 HeartRatePermission::Granted => HeartRateStatus::Idle,
167 HeartRatePermission::Denied => HeartRateStatus::Denied,
168 HeartRatePermission::NotAsked => HeartRateStatus::NeedsPermission,
169 },
170 };
171 HeartRate { status, bpm: None }
172}
173
174type Observer = Arc<dyn Fn(HeartRate) + Send + Sync>;
175
176static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
177static READERS: AtomicUsize = AtomicUsize::new(0);
179
180fn slot() -> &'static Mutex<HeartRate> {
181 static SLOT: OnceLock<Mutex<HeartRate>> = OnceLock::new();
182 SLOT.get_or_init(|| Mutex::new(HeartRate::default()))
183}
184
185fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
186 static OBSERVERS: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
187 OBSERVERS.get_or_init(|| Mutex::new(Vec::new()))
188}
189
190pub fn heart_rate() -> HeartRate {
192 slot().lock().map(|reading| *reading).unwrap_or_default()
193}
194
195pub fn publish_heart_rate(reading: HeartRate) {
198 {
199 let Ok(mut current) = slot().lock() else {
200 return;
201 };
202 if *current == reading {
203 return;
204 }
205 *current = reading;
206 }
207 let listeners: Vec<Observer> = observers()
208 .lock()
209 .map(|observers| observers.iter().map(|(_, o)| Arc::clone(o)).collect())
210 .unwrap_or_default();
211 for observer in listeners {
212 observer(reading);
213 }
214}
215
216pub fn publish_heart_rate_permission(granted: bool) {
219 if granted && READERS.load(Ordering::Acquire) > 0 {
220 start_for_readers();
221 } else {
222 publish_heart_rate(resting_reading());
223 }
224}
225
226fn start_for_readers() {
227 let Some(monitor) = PLATFORM_HEART_RATE
228 .get()
229 .filter(|monitor| monitor.available())
230 else {
231 publish_heart_rate(HeartRate::default());
232 return;
233 };
234 if monitor.permission() != HeartRatePermission::Granted {
235 publish_heart_rate(resting_reading());
236 return;
237 }
238 publish_heart_rate(HeartRate {
239 status: HeartRateStatus::Acquiring,
240 bpm: None,
241 });
242 if let Err(error) = monitor.start() {
243 log::warn!("cranpose: the heart-rate sensor did not start: {error:?}");
244 publish_heart_rate(resting_reading());
245 }
246}
247
248pub struct HeartRateReader {
250 id: u64,
251 reading: bool,
252}
253
254impl Drop for HeartRateReader {
255 fn drop(&mut self) {
256 if let Ok(mut observers) = observers().lock() {
257 observers.retain(|(id, _)| *id != self.id);
258 }
259 if self.reading && READERS.fetch_sub(1, Ordering::AcqRel) == 1 {
260 if let Some(monitor) = PLATFORM_HEART_RATE.get() {
261 monitor.stop();
262 }
263 publish_heart_rate(resting_reading());
264 }
265 }
266}
267
268pub fn observe_heart_rate(
272 read: bool,
273 observer: impl Fn(HeartRate) + Send + Sync + 'static,
274) -> HeartRateReader {
275 let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
276 let observer: Observer = Arc::new(observer);
277 if let Ok(mut observers) = observers().lock() {
278 observers.push((id, Arc::clone(&observer)));
279 }
280 if read && READERS.fetch_add(1, Ordering::AcqRel) == 0 {
281 start_for_readers();
282 }
283 observer(heart_rate());
284 HeartRateReader { id, reading: read }
285}
286
287#[expect(non_snake_case)]
291#[track_caller]
292pub fn rememberHeartRate(active: bool) -> State<HeartRate> {
293 let updates = rememberEventStream(active, move |sender| {
294 observe_heart_rate(active, move |reading| sender.send(reading))
295 });
296 cranpose_core::collectAsState(updates, (), heart_rate())
297}
298
299#[cfg(test)]
300#[path = "tests/heart_rate_tests.rs"]
301mod tests;