-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor BiometricCollector to enhance heart rate data handling #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,13 @@ | ||||||||||||
| using Toybox.Sensor; | ||||||||||||
| using Toybox.System; | ||||||||||||
| using Toybox.Math; | ||||||||||||
| using Toybox.Lang; | ||||||||||||
|
|
||||||||||||
| module Affect { | ||||||||||||
|
|
||||||||||||
| // Elegant Biometric Collector | ||||||||||||
| // Leonardo's principle: Simplicity is the ultimate sophistication | ||||||||||||
| // | ||||||||||||
| // One simple approach: Poll Sensor.getInfo() every second | ||||||||||||
| // If no real data after timeout, generate synthetic data for testing | ||||||||||||
| // Biometric Collector using real heart beat intervals | ||||||||||||
| // Uses Sensor.registerSensorDataListener() for accurate HRV measurement | ||||||||||||
| // Falls back to synthetic data in simulator only | ||||||||||||
| class BiometricCollector { | ||||||||||||
|
|
||||||||||||
| private var rmssdCalculator; | ||||||||||||
|
|
@@ -22,9 +21,11 @@ module Affect { | |||||||||||
| // State tracking | ||||||||||||
| private var tickCount; | ||||||||||||
| private var usingSynthetic; | ||||||||||||
| private var listenerRegistered; | ||||||||||||
| private var lastRealDataTick; | ||||||||||||
|
|
||||||||||||
| // Configuration | ||||||||||||
| private const SYNTHETIC_TIMEOUT = 8; // Start synthetic after 8 seconds | ||||||||||||
| private const SYNTHETIC_TIMEOUT = 10; // Start synthetic after 10 seconds with no real data | ||||||||||||
|
|
||||||||||||
| function initialize(rmssdCalc, stabilityAnalyz) { | ||||||||||||
| rmssdCalculator = rmssdCalc; | ||||||||||||
|
|
@@ -35,24 +36,76 @@ module Affect { | |||||||||||
| cv = null; | ||||||||||||
| tickCount = 0; | ||||||||||||
| usingSynthetic = false; | ||||||||||||
| listenerRegistered = false; | ||||||||||||
| lastRealDataTick = 0; | ||||||||||||
|
|
||||||||||||
| // Enable heart rate sensor | ||||||||||||
| // Register for real heart beat interval data | ||||||||||||
| registerHeartBeatListener(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Register sensor data listener for real RR intervals | ||||||||||||
| private function registerHeartBeatListener() { | ||||||||||||
| try { | ||||||||||||
| // Enable heart rate sensor first | ||||||||||||
| Sensor.setEnabledSensors([Sensor.SENSOR_HEARTRATE]); | ||||||||||||
|
|
||||||||||||
| // Register for heart beat interval data (real RR intervals!) | ||||||||||||
| var options = { | ||||||||||||
| :period => 1, // 1 second batches | ||||||||||||
| :heartBeatIntervals => { | ||||||||||||
| :enabled => true | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
| Sensor.registerSensorDataListener(method(:onSensorData) as Lang.Method, options); | ||||||||||||
| listenerRegistered = true; | ||||||||||||
| } catch (e) { | ||||||||||||
| // Sensor might not be available | ||||||||||||
| // Sensor listener not available (older device or simulator) | ||||||||||||
| listenerRegistered = false; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Callback for real heart beat interval data | ||||||||||||
| // This receives actual beat-to-beat timing from the optical HR sensor | ||||||||||||
| function onSensorData(sensorData as Sensor.SensorData) { | ||||||||||||
| // Get heart rate from sensor info | ||||||||||||
| var info = Sensor.getInfo(); | ||||||||||||
| if (info != null && info has :heartRate && info.heartRate != null && info.heartRate > 0) { | ||||||||||||
| heartRate = info.heartRate; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Process real heart beat intervals | ||||||||||||
| if (sensorData has :heartRateData && sensorData.heartRateData != null) { | ||||||||||||
| var hrData = sensorData.heartRateData; | ||||||||||||
| if (hrData has :heartBeatIntervals && hrData.heartBeatIntervals != null) { | ||||||||||||
| var intervals = hrData.heartBeatIntervals; | ||||||||||||
| if (intervals.size() > 0) { | ||||||||||||
| usingSynthetic = false; | ||||||||||||
| lastRealDataTick = tickCount; | ||||||||||||
|
|
||||||||||||
| // Feed each real RR interval to the calculator | ||||||||||||
| for (var i = 0; i < intervals.size(); i++) { | ||||||||||||
| var rrInterval = intervals[i]; | ||||||||||||
| if (rrInterval != null && rrInterval > 0) { | ||||||||||||
| rmssdCalculator.addInterval(rrInterval); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| rmssd = rmssdCalculator.getRMSSD(); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Called every second by the view timer | ||||||||||||
| function update() { | ||||||||||||
| tickCount++; | ||||||||||||
|
|
||||||||||||
| // Try to get real sensor data | ||||||||||||
| var gotRealData = pollSensor(); | ||||||||||||
| // If listener not registered, try polling approach | ||||||||||||
| if (!listenerRegistered) { | ||||||||||||
| pollSensorFallback(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Fallback to synthetic if no real data | ||||||||||||
| if (!gotRealData && tickCount > SYNTHETIC_TIMEOUT) { | ||||||||||||
| // Fallback to synthetic if no real data for a while (simulator) | ||||||||||||
| if ((tickCount - lastRealDataTick) > SYNTHETIC_TIMEOUT) { | ||||||||||||
|
||||||||||||
| // 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) { |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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; |
Copilot
AI
Feb 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment says
update()is called every second, butAffectView.onTick()callsbiometricCollector.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 likeSYNTHETIC_TIMEOUTremains interpretable.