use std::sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicU64, AtomicUsize, Ordering},
};
use cranpose_core::{State, rememberEventStream};
use crate::registry::ServiceRegistry;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum HeartRatePermission {
#[default]
NotAsked,
Granted,
Denied,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum HeartRateStatus {
#[default]
Unavailable,
NeedsPermission,
Denied,
Idle,
Acquiring,
Live,
OffBody,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct HeartRate {
pub status: HeartRateStatus,
pub bpm: Option<f32>,
}
impl HeartRate {
pub fn live_bpm(&self) -> Option<f32> {
match self.status {
HeartRateStatus::Live => self.bpm,
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HeartRateError {
Unavailable,
NotPermitted,
Failed(String),
}
pub trait HeartRateMonitor: Send + Sync {
fn available(&self) -> bool;
fn permission(&self) -> HeartRatePermission;
fn request_permission(&self);
fn start(&self) -> Result<(), HeartRateError>;
fn stop(&self);
}
pub type HeartRateMonitorRef = Arc<dyn HeartRateMonitor>;
static PLATFORM_HEART_RATE: ServiceRegistry<dyn HeartRateMonitor> = ServiceRegistry::new();
pub fn set_platform_heart_rate(monitor: HeartRateMonitorRef) {
PLATFORM_HEART_RATE.set(monitor);
publish_heart_rate(resting_reading());
}
pub fn clear_platform_heart_rate() {
PLATFORM_HEART_RATE.clear();
READERS.store(0, Ordering::Release);
publish_heart_rate(HeartRate::default());
}
pub fn heart_rate_available() -> bool {
PLATFORM_HEART_RATE
.get()
.is_some_and(|monitor| monitor.available())
}
pub fn heart_rate_permission() -> HeartRatePermission {
PLATFORM_HEART_RATE
.get()
.filter(|monitor| monitor.available())
.map_or(HeartRatePermission::Denied, |monitor| monitor.permission())
}
pub fn request_heart_rate_permission() {
if let Some(monitor) = PLATFORM_HEART_RATE
.get()
.filter(|monitor| monitor.available())
&& monitor.permission() != HeartRatePermission::Granted
{
monitor.request_permission();
}
}
fn resting_reading() -> HeartRate {
let status = match PLATFORM_HEART_RATE
.get()
.filter(|monitor| monitor.available())
{
None => HeartRateStatus::Unavailable,
Some(monitor) => match monitor.permission() {
HeartRatePermission::Granted => HeartRateStatus::Idle,
HeartRatePermission::Denied => HeartRateStatus::Denied,
HeartRatePermission::NotAsked => HeartRateStatus::NeedsPermission,
},
};
HeartRate { status, bpm: None }
}
type Observer = Arc<dyn Fn(HeartRate) + Send + Sync>;
static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
static READERS: AtomicUsize = AtomicUsize::new(0);
fn slot() -> &'static Mutex<HeartRate> {
static SLOT: OnceLock<Mutex<HeartRate>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(HeartRate::default()))
}
fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
static OBSERVERS: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
OBSERVERS.get_or_init(|| Mutex::new(Vec::new()))
}
pub fn heart_rate() -> HeartRate {
slot().lock().map(|reading| *reading).unwrap_or_default()
}
pub fn publish_heart_rate(reading: HeartRate) {
{
let Ok(mut current) = slot().lock() else {
return;
};
if *current == reading {
return;
}
*current = reading;
}
let listeners: Vec<Observer> = observers()
.lock()
.map(|observers| observers.iter().map(|(_, o)| Arc::clone(o)).collect())
.unwrap_or_default();
for observer in listeners {
observer(reading);
}
}
pub fn publish_heart_rate_permission(granted: bool) {
if granted && READERS.load(Ordering::Acquire) > 0 {
start_for_readers();
} else {
publish_heart_rate(resting_reading());
}
}
fn start_for_readers() {
let Some(monitor) = PLATFORM_HEART_RATE
.get()
.filter(|monitor| monitor.available())
else {
publish_heart_rate(HeartRate::default());
return;
};
if monitor.permission() != HeartRatePermission::Granted {
publish_heart_rate(resting_reading());
return;
}
publish_heart_rate(HeartRate {
status: HeartRateStatus::Acquiring,
bpm: None,
});
if let Err(error) = monitor.start() {
log::warn!("cranpose: the heart-rate sensor did not start: {error:?}");
publish_heart_rate(resting_reading());
}
}
pub struct HeartRateReader {
id: u64,
reading: bool,
}
impl Drop for HeartRateReader {
fn drop(&mut self) {
if let Ok(mut observers) = observers().lock() {
observers.retain(|(id, _)| *id != self.id);
}
if self.reading && READERS.fetch_sub(1, Ordering::AcqRel) == 1 {
if let Some(monitor) = PLATFORM_HEART_RATE.get() {
monitor.stop();
}
publish_heart_rate(resting_reading());
}
}
}
pub fn observe_heart_rate(
read: bool,
observer: impl Fn(HeartRate) + Send + Sync + 'static,
) -> HeartRateReader {
let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
let observer: Observer = Arc::new(observer);
if let Ok(mut observers) = observers().lock() {
observers.push((id, Arc::clone(&observer)));
}
if read && READERS.fetch_add(1, Ordering::AcqRel) == 0 {
start_for_readers();
}
observer(heart_rate());
HeartRateReader { id, reading: read }
}
#[expect(non_snake_case)]
#[track_caller]
pub fn rememberHeartRate(active: bool) -> State<HeartRate> {
let updates = rememberEventStream(active, move |sender| {
observe_heart_rate(active, move |reading| sender.send(reading))
});
cranpose_core::collectAsState(updates, (), heart_rate())
}
#[cfg(test)]
#[path = "tests/heart_rate_tests.rs"]
mod tests;