Skip to main content

cranpose_services/
heart_rate.rs

1//! The wearer's heart rate, from the device's own sensor.
2//!
3//! Two things about a heart-rate sensor are the framework's to get right,
4//! because an application that gets them wrong is one a store can refuse.
5//!
6//! **Nothing happens unless the application asks.** The permission is in the
7//! build only when the build script declares
8//! `cranpose_capabilities::Use::heart_rate`, and the person is asked only
9//! when the application calls [`request_heart_rate_permission`]. Observing
10//! the reading never prompts: an application that has not asked, or was told
11//! no, sees [`HeartRateStatus::NeedsPermission`] or
12//! [`HeartRateStatus::Denied`] and decides for itself what to say.
13//!
14//! **The sensor runs only while something reads it.** [`rememberHeartRate`]
15//! starts it when it enters the composition active and stops it when it
16//! leaves or turns inactive, so a screen that is not showing the pulse is
17//! not paying the battery for it.
18//!
19//! A reading is beats per minute as the sensor reports it. A sensor that is
20//! not touching skin reports nothing usable; that is
21//! [`HeartRateStatus::OffBody`], not a number.
22
23use 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/// Whether the application may read the sensor.
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34pub enum HeartRatePermission {
35    /// The person has not been asked, or the answer is not known yet.
36    #[default]
37    NotAsked,
38    Granted,
39    Denied,
40}
41
42/// What the heart-rate sensor is doing.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
44pub enum HeartRateStatus {
45    /// This platform has no heart-rate sensor, or the build did not declare
46    /// the service.
47    #[default]
48    Unavailable,
49    /// The sensor is there, and the application has not been allowed to
50    /// read it yet.
51    NeedsPermission,
52    /// The person said no.
53    Denied,
54    /// Allowed, and not running.
55    Idle,
56    /// Running, without a reading yet.
57    Acquiring,
58    /// Running, and [`HeartRate::bpm`] is the latest reading.
59    Live,
60    /// Running, and the sensor is not on the skin.
61    OffBody,
62}
63
64/// The latest the sensor said.
65#[derive(Clone, Copy, Debug, Default, PartialEq)]
66pub struct HeartRate {
67    pub status: HeartRateStatus,
68    /// Beats per minute, while [`status`](Self::status) is
69    /// [`HeartRateStatus::Live`].
70    pub bpm: Option<f32>,
71}
72
73impl HeartRate {
74    /// The reading, when there is a live one.
75    pub fn live_bpm(&self) -> Option<f32> {
76        match self.status {
77            HeartRateStatus::Live => self.bpm,
78            _ => None,
79        }
80    }
81}
82
83/// Why the sensor could not start.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub enum HeartRateError {
86    Unavailable,
87    NotPermitted,
88    Failed(String),
89}
90
91/// A platform's heart-rate sensor.
92pub trait HeartRateMonitor: Send + Sync {
93    /// Whether the device has the sensor and the build declared the service.
94    fn available(&self) -> bool;
95    /// Whether the application may read it.
96    fn permission(&self) -> HeartRatePermission;
97    /// Asks the person, once per call; the answer arrives through
98    /// [`publish_heart_rate_permission`].
99    fn request_permission(&self);
100    /// Starts reading. Readings arrive through [`publish_heart_rate`].
101    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
109/// Installs the platform sensor, replacing any previous one.
110pub fn set_platform_heart_rate(monitor: HeartRateMonitorRef) {
111    PLATFORM_HEART_RATE.set(monitor);
112    publish_heart_rate(resting_reading());
113}
114
115/// Removes the platform sensor (tests and teardown).
116pub fn clear_platform_heart_rate() {
117    PLATFORM_HEART_RATE.clear();
118    READERS.store(0, Ordering::Release);
119    publish_heart_rate(HeartRate::default());
120}
121
122/// Whether this device can read a heart rate for this application at all.
123pub fn heart_rate_available() -> bool {
124    PLATFORM_HEART_RATE
125        .get()
126        .is_some_and(|monitor| monitor.available())
127}
128
129/// Whether the application may read the sensor.
130pub 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
137/// Asks the person for the sensor. The only call in the framework that does;
138/// a no-op where there is no sensor or the build did not declare it.
139pub 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
149/// What the sensor is doing when nothing is reading it.
150fn 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);
168/// How many active readers want the sensor running.
169static 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
181/// The latest reading.
182pub fn heart_rate() -> HeartRate {
183    slot().lock().map(|reading| *reading).unwrap_or_default()
184}
185
186/// Publishes a reading. Backends call this from whichever thread the
187/// platform delivers on.
188pub 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
207/// Publishes the person's answer. Backends call this when the permission
208/// prompt closes; a reader waiting on it starts the sensor.
209pub 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
239/// A registration that keeps an observer, and when it reads, the sensor.
240pub 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
259/// Registers `observer` for readings, delivering the latest at once. With
260/// `read` the sensor runs while this registration lives -- if the person
261/// has allowed it; this never asks.
262pub 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/// The wearer's heart rate, observed for as long as this call stays in the
279/// composition. While `active` the sensor runs, if the person has allowed
280/// it; this never asks -- call [`request_heart_rate_permission`] for that.
281#[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;