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::{AtomicUsize, Ordering},
26};
27
28use cranpose_core::{State, rememberEventStream};
29use portable_atomic::AtomicU64;
30
31use crate::registry::ServiceRegistry;
32
33/// Whether the application may read the sensor.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum HeartRatePermission {
36    /// The person has not been asked, or the answer is not known yet.
37    #[default]
38    NotAsked,
39    /// The person said yes.
40    Granted,
41    /// The person said no.
42    Denied,
43}
44
45/// What the heart-rate sensor is doing.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
47pub enum HeartRateStatus {
48    /// This platform has no heart-rate sensor, or the build did not declare
49    /// the service.
50    #[default]
51    Unavailable,
52    /// The sensor is there, and the application has not been allowed to
53    /// read it yet.
54    NeedsPermission,
55    /// The person said no.
56    Denied,
57    /// Allowed, and not running.
58    Idle,
59    /// Running, without a reading yet.
60    Acquiring,
61    /// Running, and [`HeartRate::bpm`] is the latest reading.
62    Live,
63    /// Running, and the sensor is not on the skin.
64    OffBody,
65}
66
67/// The latest the sensor said.
68#[derive(Clone, Copy, Debug, Default, PartialEq)]
69pub struct HeartRate {
70    /// What the sensor is doing. `bpm` is meaningful only when this is [`HeartRateStatus::Live`].
71    pub status: HeartRateStatus,
72    /// Beats per minute, while [`status`](Self::status) is
73    /// [`HeartRateStatus::Live`].
74    pub bpm: Option<f32>,
75}
76
77impl HeartRate {
78    /// The reading, when there is a live one.
79    pub fn live_bpm(&self) -> Option<f32> {
80        match self.status {
81            HeartRateStatus::Live => self.bpm,
82            _ => None,
83        }
84    }
85}
86
87/// Why the sensor could not start.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub enum HeartRateError {
90    /// The device has no sensor, or the build did not declare the service.
91    Unavailable,
92    /// The application may not read the sensor yet.
93    NotPermitted,
94    /// The platform failed to start the sensor. The text describes the cause.
95    Failed(String),
96}
97
98/// A platform's heart-rate sensor.
99pub trait HeartRateMonitor: Send + Sync {
100    /// Whether the device has the sensor and the build declared the service.
101    fn available(&self) -> bool;
102    /// Whether the application may read it.
103    fn permission(&self) -> HeartRatePermission;
104    /// Asks the person, once per call; the answer arrives through
105    /// [`publish_heart_rate_permission`].
106    fn request_permission(&self);
107    /// Starts reading. Readings arrive through [`publish_heart_rate`].
108    fn start(&self) -> Result<(), HeartRateError>;
109    /// Stops reading. Readings stop arriving through [`publish_heart_rate`].
110    fn stop(&self);
111}
112
113/// Shared handle to a [`HeartRateMonitor`].
114pub type HeartRateMonitorRef = Arc<dyn HeartRateMonitor>;
115
116static PLATFORM_HEART_RATE: ServiceRegistry<dyn HeartRateMonitor> = ServiceRegistry::new();
117
118/// Installs the platform sensor, replacing any previous one.
119pub fn set_platform_heart_rate(monitor: HeartRateMonitorRef) {
120    PLATFORM_HEART_RATE.set(monitor);
121    publish_heart_rate(resting_reading());
122}
123
124/// Removes the platform sensor (tests and teardown).
125pub fn clear_platform_heart_rate() {
126    PLATFORM_HEART_RATE.clear();
127    READERS.store(0, Ordering::Release);
128    publish_heart_rate(HeartRate::default());
129}
130
131/// Whether this device can read a heart rate for this application at all.
132pub fn heart_rate_available() -> bool {
133    PLATFORM_HEART_RATE
134        .get()
135        .is_some_and(|monitor| monitor.available())
136}
137
138/// Whether the application may read the sensor.
139pub 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
146/// Asks the person for the sensor. The only call in the framework that does;
147/// a no-op where there is no sensor or the build did not declare it.
148pub 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
158/// What the sensor is doing when nothing is reading it.
159fn 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);
177/// How many active readers want the sensor running.
178static 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
190/// The latest reading.
191pub fn heart_rate() -> HeartRate {
192    slot().lock().map(|reading| *reading).unwrap_or_default()
193}
194
195/// Publishes a reading. Backends call this from whichever thread the
196/// platform delivers on.
197pub 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
216/// Publishes the person's answer. Backends call this when the permission
217/// prompt closes; a reader waiting on it starts the sensor.
218pub 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
248/// A registration that keeps an observer, and when it reads, the sensor.
249pub 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
268/// Registers `observer` for readings, delivering the latest at once. With
269/// `read` the sensor runs while this registration lives -- if the person
270/// has allowed it; this never asks.
271pub 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/// The wearer's heart rate, observed for as long as this call stays in the
288/// composition. While `active` the sensor runs, if the person has allowed
289/// it; this never asks -- call [`request_heart_rate_permission`] for that.
290#[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;