Refactor BiometricCollector to enhance heart rate data handling#2
Refactor BiometricCollector to enhance heart rate data handling#2
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors BiometricCollector to use beat-to-beat interval data (RR intervals) via a sensor data listener to improve HRV/RMSSD accuracy, with a polling fallback and synthetic-data fallback behavior.
Changes:
- Registers
Sensor.registerSensorDataListener()to ingest real heartbeat intervals and compute RMSSD from true RR samples. - Adds listener state tracking (
listenerRegistered,lastRealDataTick) and acleanup()method intended to unregister the listener. - Simplifies the polling fallback to only capture heart rate (removing HR-derived RR estimation).
| // Called every second by the view timer | ||
| function update() { |
There was a problem hiding this comment.
Comment says update() is called every second, but AffectView.onTick() calls biometricCollector.update() on a modulo of the animation phase (~every ~1.25s with current constants). Consider updating this comment (or the cadence) so the time-based logic like SYNTHETIC_TIMEOUT remains interpretable.
source/BiometricCollector.mc
Outdated
| // Fallback to synthetic if no real data for a while (simulator) | ||
| if ((tickCount - lastRealDataTick) > SYNTHETIC_TIMEOUT) { |
There was a problem hiding this comment.
update() always falls back to generateSyntheticData() after SYNTHETIC_TIMEOUT based solely on lastRealDataTick. On devices where the heartbeat-interval listener is unsupported (or intervals aren’t provided), lastRealDataTick never advances, so the widget will start injecting synthetic RR/HR on real hardware and corrupt RMSSD/HR. Consider gating synthetic generation strictly to the simulator (or only when no real HR is available) and avoid mixing synthetic intervals into real sessions.
| // Fallback to synthetic if no real data for a while (simulator) | |
| if ((tickCount - lastRealDataTick) > SYNTHETIC_TIMEOUT) { | |
| // Fallback to synthetic only when no real HR is available | |
| // and we haven't seen real interval data for a while (simulator, or no sensor) | |
| if ((tickCount - lastRealDataTick) > SYNTHETIC_TIMEOUT && heartRate == null) { |
source/BiometricCollector.mc
Outdated
| // Get heart rate | ||
| // Get heart rate at minimum | ||
| if (info has :heartRate && info.heartRate != null && info.heartRate > 0) { | ||
| heartRate = info.heartRate; |
There was a problem hiding this comment.
pollSensorFallback() updates heartRate but never updates lastRealDataTick (or clears usingSynthetic). This guarantees the synthetic timeout will trigger even while real HR is coming in, and once synthetic starts it will overwrite real HR in generateSyntheticData(). Update lastRealDataTick when real HR is observed (or introduce a separate “lastHeartRateTick”) and ensure the synthetic path can’t override real HR readings.
| heartRate = info.heartRate; | |
| heartRate = info.heartRate; | |
| // Treat polled heart rate as real data to prevent synthetic override | |
| usingSynthetic = false; | |
| lastRealDataTick = tickCount; |
| // Clean up sensor listener | ||
| function cleanup() { | ||
| if (listenerRegistered) { | ||
| try { | ||
| Sensor.unregisterSensorDataListener(); | ||
| } catch (e) { | ||
| // Ignore cleanup errors | ||
| } | ||
| listenerRegistered = false; | ||
| } |
There was a problem hiding this comment.
A cleanup() API was added, but there are currently no call sites in the codebase (e.g., AffectView.onHide() / AffectApp.onStop()), so the sensor listener may remain registered after the widget is hidden and keep the HR sensor active unnecessarily. Wire cleanup() into the view/app lifecycle (and consider unregistering during reset() if applicable).
…improve real heart rate measurement
No description provided.