diff --git a/apps/stressless/ChangeLog b/apps/stressless/ChangeLog new file mode 100644 index 000000000..55ebb3e4b --- /dev/null +++ b/apps/stressless/ChangeLog @@ -0,0 +1 @@ +0.01: New App diff --git a/apps/stressless/app-icon.js b/apps/stressless/app-icon.js new file mode 100644 index 000000000..0d9739661 --- /dev/null +++ b/apps/stressless/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AFW7AAgThDBQeNCaYaSDg4TTFygcFCaYuWDgYTTFyHB5AmRGCARK4XJF7bXR5PQF9vQ6/CF8COL6/XYDgwERxYvBYEKOM67AhLQwEDFwTAGYywvIXIIhCAgPQSILAE4RmWF5AnBMAQECA4gJDMCovDGAfBQ4PR4K+C4RiCZAr3UFwgvDJ4IAB5IrEYAgJBHwaoF6HQNRIvFEQItDXYaTEYAQ+EAogADZZIvFGAYTBAgIBBFQg0CRwQREF5wuGeQpNDQYSUDAYYyFR5guIF4jyCdQ3CMYY+DNwTtRMBSJCAwgyBNAI+HFygwEQoJ4EQwQpEHwwuVGAhPFGwIABFIY+GFywwDdoTAFe4ZgCFzjDFSAy4NFyowIF4PIF0gwHXAKYGFz4wHXBgubGAxeOFzT1KFsowQF0AwNF0QxKFsoxIEj/XGBgQDCJYrODQICHEgQDGAQoSECwhaNCoYJDAwgoHBxBfVJgpfJGIgOHL5haGPgoBDTxIBHAH4AkA==")) diff --git a/apps/stressless/app.js b/apps/stressless/app.js new file mode 100644 index 000000000..b27c79a0d --- /dev/null +++ b/apps/stressless/app.js @@ -0,0 +1,287 @@ +var option = null; + +//debugging or analysis files +//var logfile = require("Storage").open("HRV_log.csv", "w"); + +var logfile = require("Storage").open("HRV_logs.csv", "a"); + +var csv = [ + "time", + "sample count", + "HR", + "SDNN", + "RMSSD", + "Temp", + "movement" + ]; +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 BPM_array = []; +var raw_HR_array = new Float32Array(1536); +var alternate_array = new Float32Array(3072); +var pulse_array = []; +var cutoff_threshold = 0.5; +var sample_frequency = 51.6; +var gap_threshold = 0.15; +var movement = 0; + +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*/)); + // add ad final element + log[log.length - 1] = data; +} + +function average(samples) { + 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;*/ +} + +function StandardDeviation (array) { + const n = array.length; + 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); + + + g.clear(); + g.drawString("processing 1/5", px, py); + + rolling_average(raw_HR_array,5); + g.clear(); + g.drawString("processing 2/5", px, py); + + upscale(); + g.clear(); + g.drawString("processing 3/5", px, py); + + rolling_average(alternate_array,5); + g.clear(); + g.drawString("processing 4/5", px, py); + + apply_cutoff(); + find_peaks(); + + g.clear(); + g.drawString("processing 5/5", px, py); + + calculate_HRV(); +} + +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() { "ram" + var index = 0; + for (let i = raw_HR_array.length - 1; i > 5; i -= 5) { + p0 = raw_HR_array[i]; + p1 = raw_HR_array[i - 1]; + p2 = raw_HR_array[i - 2]; + p3 = raw_HR_array[i - 3]; + p4 = raw_HR_array[i - 4]; + for (let T = 0; T < 100; T += 10) { + x = T / 100; + D = bernstein(p0, p1, p2, p3, p4, x); + alternate_array[index] = D; + index++; + } + } +} + +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() { "ram" + var x; + for (let i = 0; i < alternate_array.length; i++) { + x = alternate_array[i]; + if (x < cutoff_threshold) + x = cutoff_threshold; + alternate_array[i] = x; + } +} + +function find_peaks() { "ram" + var previous; + var previous_slope = 0; + var slope; + var gap_size = 0; + var temp_array = []; + + for (let i = 0; i < alternate_array.length; i++) { + if (previous == null) + previous = alternate_array[i]; + slope = alternate_array[i] - previous; + if (slope * previous_slope < 0) { + if (gap_size > 30) { + pulse_array.push(gap_size); + gap_size = 0; + } + } + else { + gap_size++; + } + previous_slope = slope; + previous = alternate_array[i]; + } +} + +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); + sum += square; + } + + var meansquare = sum/data.length; + var RMS = Math.sqrt(meansquare); + RMS = parseInt(RMS); + return RMS; +} + +function calculate_HRV() { + var gap_average = average(pulse_array); + var temp_array = []; + var gap_max = (1 + gap_threshold) * gap_average; + var gap_min = (1 - gap_threshold) * gap_average; + for (let i = 0; i < pulse_array.length; i++) { + if (pulse_array[i] > gap_min && pulse_array[i] < gap_max) + temp_array.push(pulse_array[i]); + } + gap_average = average(temp_array); + var calculatedHR = (sample_frequency*60)/(gap_average/2); + if(option == 0) + 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); + var RMS_SD = RMSSD(temp_array); + g.drawString("SDNN:" + SDNN + +"\nRMSSD:" + RMS_SD + + "\nHR:" + calculatedHR.toFixed(0) + +"\nSample Count:" + temp_array.length, px, py); + Bangle.setLCDPower(1); + if(option == 0) { // single run + Bangle.buzz(500,1); + option = null; + drawButtons(); + } else { + var csv = [ + 0|getTime(), + temp_array.length, + calculatedHR.toFixed(0), + SDNN, + RMS_SD, + E.getTemperature(), + movement.toFixed(5) + ]; + logfile.write(csv.join(",")+"\n"); + + + turn_on(); + } +} + + +function btn3Pressed() { + if(option === null){ + logfile.write(""); //reset HRV log + g.clear(); + g.drawString("continuous mode", px, py); + option = 1; + + turn_on(); + } +} + +function turn_on() { + BPM_array = []; + pulse_array = []; + samples = 0; + if (accel) clearInterval(accel); + movement = 0; + accel = setInterval(function () { + movement = movement + Bangle.getAccel().diff; + }, 1000); + Bangle.setHRMPower(1); + collectData = true; +} + +function drawButtons() { + g.setColor("#00ff7f"); + g.setFont("6x8", 2); + g.setFontAlign(-1,1); + g.drawString("start recording HRV", 120, 210); + g.setColor("#ffffff"); + g.setFontAlign(0, 0); +} + +g.clear(); + +drawButtons(); + +g.setFont("6x8", 2); +g.setColor("#ffffff"); +g.setFontAlign(0, 0); // center font + +setWatch(btn3Pressed, BTN3, {repeat:true}); + + + +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++; +}); diff --git a/apps/stressless/metadata.json b/apps/stressless/metadata.json new file mode 100644 index 000000000..8c27620d3 --- /dev/null +++ b/apps/stressless/metadata.json @@ -0,0 +1,13 @@ +{ "id": "stressless", + "name": "Stressless", + "shortName":"Stressless", + "icon": "stressless.png", + "version":"0.01", + "description": "This is a heart activity tracker for PIIS stressless project", + "tags": "health", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"stressless.app.js","url":"app.js"}, + {"name":"stressless.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/stressless/stressless.png b/apps/stressless/stressless.png new file mode 100644 index 000000000..6f79d7c83 Binary files /dev/null and b/apps/stressless/stressless.png differ