diff --git a/apps.json b/apps.json index 289889d65..39f424b02 100644 --- a/apps.json +++ b/apps.json @@ -2735,7 +2735,7 @@ "name": "Heart Rate Variability monitor", "shortName":"HRV monitor", "icon": "hrv.png", - "version":"0.03", + "version":"0.04", "description": "Heart Rate Variability monitor, see Readme for more info", "tags": "", "readme": "README.md", diff --git a/apps/HRV/ChangeLog b/apps/HRV/ChangeLog index 447062294..78446c932 100644 --- a/apps/HRV/ChangeLog +++ b/apps/HRV/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Added options to either run as a one-off reading, or a continuous mode to log data until the watch is reset 0.03: Add RMSSD recording +0.04: Modify to work with new heart rate API, but still not sure it's working correctly diff --git a/apps/HRV/app.js b/apps/HRV/app.js index 5919d1d79..b5e0ba8dd 100644 --- a/apps/HRV/app.js +++ b/apps/HRV/app.js @@ -17,22 +17,23 @@ var csv = [ logfile.write(csv.join(",")+"\n"); var debugging = true; +var samples = 0; // how many samples have we connected? +var collectData = false; // are we currently collecting data? -var first_signals = 0; // ignore the first several signals -var heartrate = []; var BPM_array = []; var raw_HR_array = new Float32Array(1536); var alternate_array = new Float32Array(3072); var pulse_array = []; -var pulsecount = 0; var cutoff_threshold = 0.5; var sample_frequency = 51.6; var gap_threshold = 0.15; -var hr_min = 40; -var hr_max = 160; var movement = 0; -function storeMyData(data, file_type) { +var px = g.getWidth()/2; +var py = g.getHeight()/2; +var accel; // interval for acceleration logging + +function storeMyData(data, file_type) { "ram" log = raw_HR_array; // shift elements backwards - note the 4, because a Float32 is 4 bytes log.set(new Float32Array(log.buffer, 4 /*bytes*/)); @@ -41,59 +42,58 @@ function storeMyData(data, file_type) { } function average(samples) { - var sum = 0; + return E.sum(samples) / samples.length; // faster builtin + /* var sum = 0; for (var i = 0; i < samples.length; i++) { sum += parseFloat(samples[i]); } var avg = sum / samples.length; - return avg; + return avg;*/ } function StandardDeviation (array) { const n = array.length; - const mean = array.reduce((a, b) => a + b) / n; - return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n); + const mean = E.sum(array) / n; //array.reduce((a, b) => a + b) / n; + //return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n); + return Math.sqrt(E.variance(array, mean)); } function turn_off() { Bangle.setHRMPower(0); - - var accel = setInterval(function () { - movement = movement + Bangle.getAccel().diff; - }, 1000); - + + g.clear(); - g.drawString("processing 1/5", 120, 120); + g.drawString("processing 1/5", px, py); rolling_average(raw_HR_array,5); g.clear(); - g.drawString("processing 2/5", 120, 120); + g.drawString("processing 2/5", px, py); upscale(); g.clear(); - g.drawString("processing 3/5", 120, 120); + g.drawString("processing 3/5", px, py); rolling_average(alternate_array,5); g.clear(); - g.drawString("processing 4/5", 120, 120); + g.drawString("processing 4/5", px, py); apply_cutoff(); find_peaks(); - + g.clear(); - g.drawString("processing 5/5", 120, 120); + g.drawString("processing 5/5", px, py); calculate_HRV(); } -function bernstein(A, B, C, D, E, t) { +function bernstein(A, B, C, D, E, t) { "ram" s = 1 - t; x = (A * Math.pow(s, 4)) + (B * 4 * Math.pow(s, 3) * t) + (C * 6 * s * s * t * t) + (D * 4 * s * Math.pow(t, 3)) + (E * Math.pow(t, 4)); return x; } -function upscale() { +function upscale() { "ram" var index = 0; for (let i = raw_HR_array.length - 1; i > 5; i -= 5) { p0 = raw_HR_array[i]; @@ -110,19 +110,18 @@ function upscale() { } } -function rolling_average(values, count) { +function rolling_average(values, count) { "ram" var temp_array = []; for (let i = 0; i < values.length; i++) { temp_array = []; for (let x = 0; x < count; x++) temp_array.push(values[i + x]); - values[i] = average(temp_array); } } -function apply_cutoff() { +function apply_cutoff() { "ram" var x; for (let i = 0; i < alternate_array.length; i++) { x = alternate_array[i]; @@ -132,7 +131,7 @@ function apply_cutoff() { } } -function find_peaks() { +function find_peaks() { "ram" var previous; var previous_slope = 0; var slope; @@ -157,17 +156,17 @@ function find_peaks() { } } -function RMSSD(samples){ +function RMSSD(samples){ "ram" var sum = 0; var square = 0; var data = []; var value = 0; - + for (let i = 0; i < samples.length-1; i++) { value = Math.abs(samples[i]-samples[i+1])*((1 / (sample_frequency * 2)) * 1000); data.push(value); } - + for (let i = 0; i < data.length; i++) { square = data[i] * data[i]; Math.round(square); @@ -192,7 +191,7 @@ function calculate_HRV() { gap_average = average(temp_array); var calculatedHR = (sample_frequency*60)/(gap_average/2); if(option == 0) - g.flip(); + Bangle.setLCDPower(1); g.clear(); //var display_stdv = StandardDeviation(pulse_array).toFixed(1); var SDNN = (StandardDeviation(temp_array) * (1 / (sample_frequency * 2) * 1000)).toFixed(0); @@ -200,14 +199,13 @@ function calculate_HRV() { g.drawString("SDNN:" + SDNN +"\nRMSSD:" + RMS_SD + "\nHR:" + calculatedHR.toFixed(0) - +"\nSample Count:" + temp_array.length, 120, 120); - - if(option == 0){ + +"\nSample Count:" + temp_array.length, px, py); + Bangle.setLCDPower(1); + if(option == 0) { // single run Bangle.buzz(500,1); - clearInterval(routine); - } - - else{ + option = null; + drawButtons(); + } else { var csv = [ 0|getTime(), temp_array.length, @@ -219,85 +217,87 @@ function calculate_HRV() { ]; logfile.write(csv.join(",")+"\n"); - movement = 0; + // for (let i = 0; i < raw_HR_array.length; i++) { // raw_HR_array[i] = null; - //} + //} + + turn_on(); } } function btn1Pressed() { if(option === null){ - clearInterval(accel); g.clear(); - g.drawString("one-off assessment", 120, 120); + g.drawString("one-off assessment", px, py); option = 0; - Bangle.setHRMPower(1); + + turn_on(); } } function btn3Pressed() { if(option === null){ logfile.write(""); //reset HRV log - clearInterval(accel); g.clear(); - g.drawString("continuous mode", 120, 120); + g.drawString("continuous mode", px, py); option = 1; - Bangle.setHRMPower(1); - } + + turn_on(); + } } -var routine = setInterval(function () { - clearInterval(accel); - first_signals = 0; // ignore the first several signals - pulsecount = 0; +function turn_on() { BPM_array = []; - heartrate = []; pulse_array = []; + samples = 0; + if (accel) clearInterval(accel); + movement = 0; + accel = setInterval(function () { + movement = movement + Bangle.getAccel().diff; + }, 1000); Bangle.setHRMPower(1); -}, 180000); + collectData = true; +} -var accel = setInterval(function () { - movement = movement + Bangle.getAccel().diff; -}, 1000); +function drawButtons() { + g.setColor("#00ff7f"); + g.setFont("6x8", 2); + g.setFontAlign(-1,1); + g.drawString("continuous", 120, 210); + g.drawString("one-time", 140, 50); + g.setColor("#ffffff"); + g.setFontAlign(0, 0); +} g.clear(); -g.setColor("#00ff7f"); -g.setFont("6x8", 2); -g.setFontAlign(-1,1); -g.drawString("continuous", 120, 210); -g.setFontAlign(-1,1); -g.drawString("one-time", 140, 50); +drawButtons(); + +g.setFont("6x8", 2); g.setColor("#ffffff"); g.setFontAlign(0, 0); // center font -g.drawString("check app README", 120, 120); -g.drawString("for more info", 120, 140); +g.drawString("check app README\nfor more info", px, py); setWatch(btn1Pressed, BTN1, {repeat:true}); setWatch(btn3Pressed, BTN3, {repeat:true}); -Bangle.on('HRM', function (hrm) { - if(option == 0) - g.flip(); - if (first_signals < 3) { - g.clear(); - g.drawString("setting up...\nremain still " + first_signals * 20 + "%", 120, 120); - first_signals++; - } - else { - BPM_array = hrm.raw; - if(hrm.bpm > hr_min && hrm.bpm < hr_max) - heartrate.push(hrm.bpm); - if (pulsecount < 7) { - for (let i = 0; i < 256; i++) { - storeMyData(BPM_array[i], 0); - } - g.clear(); - g.drawString("logging: " + ((pulsecount/6)*100).toFixed(0) + "%", 120, 120); - } - if(pulsecount == 6) - turn_off(); - pulsecount++; - } + + +Bangle.on('HRM-raw', function (e) { + if (!collectData) return; + storeMyData(e.raw, 0); + if (!(samples & 7)) { + Bangle.setLCDPower(1); + g.clearRect(0, py-10, g.getWidth(), py+22); + if (samples < 100) + g.drawString("setting up...\nremain still " + samples + "%", px, py, true); + else + g.drawString("logging: " + (samples*100/raw_HR_array.length).toFixed(0) + "%", px, py, true); + } + if (samples > raw_HR_array.length) { + collectData = false; + turn_off(); + } + samples++; });