diff --git a/apps/phystrax/app.js b/apps/phystrax/app.js index 82cab3900..0f403a067 100644 --- a/apps/phystrax/app.js +++ b/apps/phystrax/app.js @@ -1,15 +1,10 @@ -var isMeasuring = false; -var currentHeartRate = null; -var lcdTimeout; -var bpmValues = []; -var logData = []; -var lastHeartRateLogTimestamp = 0; +let isMeasuring = false; +let currentHR = null; +let lcdTimeout; +let logData = []; +let bpmValues = []; +let lastLogTime = 0; -// HRV calculation variables -var hrvSlots = [10, 20, 30, 60, 120, 300]; -var hrvValues = {}; - -// Heart rate monitor functions function startMeasure() { isMeasuring = true; Bangle.setLCDTimeout(0); @@ -18,9 +13,9 @@ function startMeasure() { }, 50000); setTimeout(() => { - Bangle.setHRMPower(1); + Bangle.setHRMPower(1); // starts HRM Bangle.on('HRM', handleHeartRate); - Bangle.buzz(500, 20); + Bangle.buzz(200, 10); // Buzz to indicate measurement start drawScreen(); }, 500); } @@ -30,23 +25,76 @@ function stopMeasure() { clearTimeout(lcdTimeout); Bangle.setLCDTimeout(10); Bangle.setHRMPower(0); - Bangle.removeAllListeners('HRM'); - Bangle.buzz(500, 20); + Bangle.removeAllListeners('HRM'); //stop HRM + saveDataToCSV(); // Save data to CSV when measurement stops + Bangle.buzz(200, 10); // Buzz to indicate measurement stop drawScreen(); } +function handleHeartRate(hrm) { + if (hrm.confidence > 90) { + currentHR = hrm.bpm; + let currentTime = Date.now(); + let elaspedTime = currentTime - lastLogTime; + let nextLog = 5 * 1000 - (elaspedTime % (5 * 1000)); // Calculate time to next log + if (elaspedTime >= 5 * 1000) { // Check if it's time for the next log + lastLogTime = currentTime - (elaspedTime % (5 * 1000)); // Set last log time to the previous 5-second boundary + let date = new Date(lastLogTime); + let dateStr = require("locale").date(date); + let timeStr = require("locale").time(date, 1); + let seconds = date.getSeconds(); + let timestamp = `${dateStr} ${timeStr}:${seconds}`; // Concatenate date, time, and seconds + logData.push({ timestamp: timestamp, heartRate: currentHR }); + bpmValues.push(currentHR); // Store heart rate for HRV calculation + if (bpmValues.length > 30) bpmValues.shift(); // Keep last 30 heart rate values + // Calculate and add SDNN (standard deviation of NN intervals) to the last log entry + logData[logData.length - 1].hrv = calcSDNN(); + drawScreen(); + } + // Schedule next measurement + setTimeout(() => { + handleHeartRate(hrm); + }, nextLog); + } +} + + +function calcSDNN() { + if (bpmValues.length < 5) return 0; // No calculation if insufficient data + + // Calculate differences between adjacent heart rate values + const differences = []; + for (let i = 1; i < bpmValues.length; i++) { + differences.push(Math.abs(bpmValues[i] - bpmValues[i - 1])); + } + + // Calculate mean difference + const meanDifference = differences.reduce((acc, val) => acc + val, 0) / differences.length; + + // Calculate squared differences from mean difference + const squaredDifferences = differences.map(diff => Math.pow(diff - meanDifference, 2)); + + // Calculate mean squared difference + const meanSquaredDifference = squaredDifferences.reduce((acc, val) => acc + val, 0) / squaredDifferences.length; + + // Calculate SDNN (standard deviation of NN intervals) + const sdnn = Math.sqrt(meanSquaredDifference); + + return sdnn; +} + function drawScreen(message) { g.clear(); // Clear the display // Set the background color - g.setColor('#95E7FF'); + g.setColor('#95E7FF'); // Fill the entire display with the background color g.fillRect(0, 0, g.getWidth(), g.getHeight()); // Set font and alignment for drawing text g.setFontAlign(0, 0); - g.setFont('Vector', 15); + g.setFont('Vector', 15); // Draw the title g.setColor('#000000'); // Set text color to black @@ -57,12 +105,13 @@ function drawScreen(message) { g.setFont('6x8', 2); g.drawString('Measuring...', g.getWidth() / 2, g.getHeight() / 2 - 10); - // Draw current heart rate if available and not zero - if (currentHeartRate !== null && currentHeartRate > 0) { - g.setFont('6x8', 4); - g.drawString(currentHeartRate.toString(), g.getWidth() / 2, g.getHeight() / 2 + 20); + // Draw current heart rate if available + g.setFont('6x8', 4); + if (currentHR !== null) { + g.drawString(currentHR.toString(), g.getWidth() / 2, g.getHeight() / 2 + 20); g.setFont('6x8', 1.6); g.drawString(' BPM', g.getWidth() / 2 + 42, g.getHeight() / 2 + 20); + } // Draw instructions @@ -72,16 +121,17 @@ function drawScreen(message) { // Draw last heart rate g.setFont('Vector', 12); g.drawString('Last Heart Rate:', g.getWidth() / 2, g.getHeight() / 2 - 20); - if (currentHeartRate !== null && currentHeartRate > 0) { + if (currentHR !== null && currentHR > 0) { g.setFont('6x8', 4); - g.drawString(currentHeartRate.toString(), g.getWidth() / 2, g.getHeight() / 2 + 10); + g.drawString(currentHR.toString(), g.getWidth() / 2, g.getHeight() / 2 + 10); g.setFont('6x8', 1.6); g.drawString(' BPM', g.getWidth() / 2 + 42, g.getHeight() / 2 + 12); + } else { g.setFont('6x8', 2); g.drawString('No data', g.getWidth() / 2, g.getHeight() / 2 + 10); g.setFont('6x8', 1); - g.drawString(message || 'Press button to start!', g.getWidth() / 2, g.getHeight() / 2 + 30); + g.drawString(message || 'Press button to start', g.getWidth() / 2, g.getHeight() / 2 + 30); } } @@ -90,78 +140,14 @@ function drawScreen(message) { } function saveDataToCSV() { - let date = new Date(); - let dateStr = require("locale").date(date); - let timeStr = require("locale").time(date, 1); - let seconds = date.getSeconds(); - let timestamp = `${dateStr} ${timeStr}:${seconds}`; - - // Check if 10 seconds have passed since the last heart rate log - if (date.getTime() - lastHeartRateLogTimestamp >= 10 * 1000) { - logData.push({ timestamp: timestamp, heartRate: currentHeartRate }); - lastHeartRateLogTimestamp = date.getTime(); // Update the last heart rate log timestamp - - // Log data to CSV - let csvContent = "Timestamp,Heart Rate,HRV (ms)\n"; - logData.forEach(entry => { - if (entry.heartRate > 0) { - let hrv = hrvValues[entry.timestamp] || ""; // Get HRV value for the timestamp - csvContent += `${entry.timestamp},${entry.heartRate},${hrv}\n`; - } - }); - require("Storage").write("heart_rate_data.csv", csvContent); - } -} - -function handleHeartRate(hrm) { - if (isMeasuring) { - currentHeartRate = hrm.bpm; - drawScreen(); - - // Log data to CSV - saveDataToCSV(); - - // Estimate RR intervals - let rrIntervals = estimateRRIntervals(bpmValues); - - // Calculate HRV - calculateHRV(rrIntervals); - } -} - -function estimateRRIntervals(rawData) { - let rrIntervals = []; - for (let i = 1; i < rawData.length; i++) { - let rrInterval = rawData[i] - rawData[i - 1]; - rrIntervals.push(rrInterval); - } - return rrIntervals; -} - -function calculateHRV(rawData) { - // Calculate HRV with SDNN method for each slot in hrvSlots - let sdnn = calcSDNN(rawData); - hrvValues[Date.now()] = sdnn; // Store HRV value with current timestamp - - // Log HRV data if 5 minutes have elapsed - if (Date.now() - lastHeartRateLogTimestamp >= 5 * 60 * 1000) { - console.log("Recalculating HRV..."); - saveDataToCSV(); // Save both heart rate and HRV data to the CSV file - lastHeartRateLogTimestamp = Date.now(); // Update the timestamp - } -} - -function calcSDNN(rrIntervals) { - let sumOfDifferencesSquared = 0; - let meanRRInterval = rrIntervals.reduce((acc, val) => acc + val, 0) / rrIntervals.length; - rrIntervals.forEach(rr => { - sumOfDifferencesSquared += Math.pow(rr - meanRRInterval, 2); + let csvContent = "Timestamp,Heart Rate(bpm),HRV(ms)\n"; + logData.forEach(entry => { + csvContent += `${entry.timestamp},${entry.heartRate},${entry.hrv}\n`; }); - let meanOfDifferencesSquared = sumOfDifferencesSquared / (rrIntervals.length - 1); - return Math.sqrt(meanOfDifferencesSquared); + require("Storage").write("heart_rate_data.csv", csvContent); } -setWatch(function () { +setWatch(function() { if (!isMeasuring) { startMeasure(); } else {