From 0bdc4ff10aefe461a43bde20e6af4d8a78fefa24 Mon Sep 17 00:00:00 2001 From: Youssef Date: Mon, 2 Feb 2026 20:10:53 -0500 Subject: [PATCH 1/2] Refactor BiometricCollector to enhance heart rate data handling --- source/BiometricCollector.mc | 130 ++++++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 40 deletions(-) diff --git a/source/BiometricCollector.mc b/source/BiometricCollector.mc index 867a700..5056cb0 100644 --- a/source/BiometricCollector.mc +++ b/source/BiometricCollector.mc @@ -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,12 +36,62 @@ 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(); + } + } } } @@ -48,11 +99,13 @@ module Affect { 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) { generateSyntheticData(); } @@ -63,38 +116,21 @@ module Affect { } } - // Poll Sensor.getInfo() - the simplest reliable method - private function pollSensor() { + // Fallback polling for devices where listener doesn't work + private function pollSensorFallback() { var info = Sensor.getInfo(); if (info == null) { - return false; + return; } - // Get heart rate + // Get heart rate at minimum if (info has :heartRate && info.heartRate != null && info.heartRate > 0) { heartRate = info.heartRate; - usingSynthetic = false; - - // Generate RR interval from HR (approximate but works) - // RR = 60000 / HR (in milliseconds) - var estimatedRR = (60000.0 / heartRate).toNumber(); - - // Add small natural variation - var variation = (Math.rand() % 40) - 20; // ±20ms - var rrInterval = estimatedRR + variation; - - // Feed to RMSSD calculator - rmssdCalculator.addInterval(rrInterval); - rmssd = rmssdCalculator.getRMSSD(); - - return true; } - - return false; } - // Generate realistic synthetic data for simulator/testing + // Generate realistic synthetic data for simulator/testing only private function generateSyntheticData() { usingSynthetic = true; @@ -102,9 +138,12 @@ module Affect { var phase = tickCount * 0.05; heartRate = 70 + (Math.sin(phase) * 10).toNumber(); - // RR interval with realistic HRV variation + // Simulate realistic HRV with larger natural variation + // Real HRV has RMSSD typically 20-80ms in healthy adults var baseRR = (60000.0 / heartRate).toNumber(); - var variation = (Math.rand() % 60) - 30; // ±30ms natural variation + + // Use larger variation to simulate real HRV (±50ms gives RMSSD ~40-50ms) + var variation = (Math.rand() % 100) - 50; var rrInterval = baseRR + variation; // Feed to calculators @@ -120,21 +159,19 @@ module Affect { function getTickCount() { return tickCount; } // Check if we have enough data for meaningful display - // Requires both HR and valid RMSSD (enough RR intervals collected) function hasData() { return heartRate != null && rmssd != null; } - // Check if HR is available (for progress indication) + // Check if HR is available function hasHeartRate() { return heartRate != null; } // Get RMSSD readiness as percentage (0-100) - // Based on how many RR intervals collected vs minimum needed function getReadinessPercent() { var intervalCount = rmssdCalculator.getIntervalCount(); - var minNeeded = 10; // MIN_INTERVALS from RMSSDCalculator + var minNeeded = 10; if (intervalCount >= minNeeded) { return 100; } @@ -148,8 +185,21 @@ module Affect { cv = null; tickCount = 0; usingSynthetic = false; + lastRealDataTick = 0; rmssdCalculator.reset(); stabilityAnalyzer.reset(); } + + // Clean up sensor listener + function cleanup() { + if (listenerRegistered) { + try { + Sensor.unregisterSensorDataListener(); + } catch (e) { + // Ignore cleanup errors + } + listenerRegistered = false; + } + } } } From 16e4bfda48f3fa4771234f1d52304b76969c3ef4 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 3 Feb 2026 13:29:43 -0500 Subject: [PATCH 2/2] Refactor BiometricCollector to eliminate synthetic data handling and improve real heart rate measurement --- source/BiometricCollector.mc | 91 ++++++++++++++---------------------- 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/source/BiometricCollector.mc b/source/BiometricCollector.mc index 5056cb0..e8ff6b6 100644 --- a/source/BiometricCollector.mc +++ b/source/BiometricCollector.mc @@ -7,37 +7,32 @@ module Affect { // Biometric Collector using real heart beat intervals // Uses Sensor.registerSensorDataListener() for accurate HRV measurement - // Falls back to synthetic data in simulator only + // NO synthetic data on real devices - only shows real measurements class BiometricCollector { private var rmssdCalculator; private var stabilityAnalyzer; // Current readings - private var heartRate; + private var currentHR; private var rmssd; private var cv; // State tracking private var tickCount; - private var usingSynthetic; private var listenerRegistered; - private var lastRealDataTick; - - // Configuration - private const SYNTHETIC_TIMEOUT = 10; // Start synthetic after 10 seconds with no real data + private var hasReceivedRealData; function initialize(rmssdCalc, stabilityAnalyz) { rmssdCalculator = rmssdCalc; stabilityAnalyzer = stabilityAnalyz; - heartRate = null; + currentHR = null; rmssd = null; cv = null; tickCount = 0; - usingSynthetic = false; listenerRegistered = false; - lastRealDataTick = 0; + hasReceivedRealData = false; // Register for real heart beat interval data registerHeartBeatListener(); @@ -69,8 +64,11 @@ module Affect { 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; + if (info != null) { + var hr = info.heartRate; + if (hr != null && hr > 0) { + currentHR = hr; + } } // Process real heart beat intervals @@ -79,8 +77,7 @@ module Affect { if (hrData has :heartBeatIntervals && hrData.heartBeatIntervals != null) { var intervals = hrData.heartBeatIntervals; if (intervals.size() > 0) { - usingSynthetic = false; - lastRealDataTick = tickCount; + hasReceivedRealData = true; // Feed each real RR interval to the calculator for (var i = 0; i < intervals.size(); i++) { @@ -99,16 +96,11 @@ module Affect { function update() { tickCount++; - // If listener not registered, try polling approach + // If listener not registered, try polling for HR at minimum if (!listenerRegistered) { pollSensorFallback(); } - // Fallback to synthetic if no real data for a while (simulator) - if ((tickCount - lastRealDataTick) > SYNTHETIC_TIMEOUT) { - generateSyntheticData(); - } - // Update stability analyzer with current RMSSD if (rmssd != null) { stabilityAnalyzer.addRMSSD(rmssd); @@ -116,56 +108,44 @@ module Affect { } } - // Fallback polling for devices where listener doesn't work + // Fallback polling for HR only (no fake RR intervals) private function pollSensorFallback() { var info = Sensor.getInfo(); - if (info == null) { - return; - } - - // Get heart rate at minimum - if (info has :heartRate && info.heartRate != null && info.heartRate > 0) { - heartRate = info.heartRate; + if (info != null) { + // Get heart rate only - we won't fake RR intervals + var hr = info.heartRate; + if (hr != null && hr > 0) { + currentHR = hr; + } } } - // Generate realistic synthetic data for simulator/testing only - private function generateSyntheticData() { - usingSynthetic = true; - - // Slowly varying HR between 60-80 bpm - var phase = tickCount * 0.05; - heartRate = 70 + (Math.sin(phase) * 10).toNumber(); - - // Simulate realistic HRV with larger natural variation - // Real HRV has RMSSD typically 20-80ms in healthy adults - var baseRR = (60000.0 / heartRate).toNumber(); - - // Use larger variation to simulate real HRV (±50ms gives RMSSD ~40-50ms) - var variation = (Math.rand() % 100) - 50; - var rrInterval = baseRR + variation; - - // Feed to calculators - rmssdCalculator.addInterval(rrInterval); - rmssd = rmssdCalculator.getRMSSD(); - } - // Getters - function getHeartRate() { return heartRate; } + function getHeartRate() { return currentHR; } function getRMSSD() { return rmssd; } function getCoefficientOfVariation() { return cv; } - function isSynthetic() { return usingSynthetic; } function getTickCount() { return tickCount; } // Check if we have enough data for meaningful display function hasData() { - return heartRate != null && rmssd != null; + return currentHR != null && rmssd != null; } // Check if HR is available function hasHeartRate() { - return heartRate != null; + return currentHR != null; + } + + // Check if we're receiving real HRV data + function hasRealHRVData() { + return hasReceivedRealData && rmssd != null; + } + + // Check if HRV is unavailable (for showing appropriate message) + function isHRVUnavailable() { + // After 15 seconds with no real RR data, HRV is unavailable + return tickCount > 15 && !hasReceivedRealData; } // Get RMSSD readiness as percentage (0-100) @@ -180,12 +160,11 @@ module Affect { // Reset state function reset() { - heartRate = null; + currentHR = null; rmssd = null; cv = null; tickCount = 0; - usingSynthetic = false; - lastRealDataTick = 0; + hasReceivedRealData = false; rmssdCalculator.reset(); stabilityAnalyzer.reset(); }