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