BT HRV - Adds app for taking HRV measurements

pull/1389/head
Martin Boonk 2022-02-01 19:34:41 +01:00
parent 0b4d2b5b5d
commit dc435c3733
8 changed files with 234 additions and 0 deletions

11
apps/bthrv/ChangeLog Normal file
View File

@ -0,0 +1,11 @@
0.01: New App!
0.02: Make overriding the HRM event optional
Emit BTHRM event for external sensor
Add recorder app plugin
0.03: Prevent readings from internal sensor mixing into BT values
Mark events with src property
Show actual source of event in app
0.04: Allow reading additional data if available: HRM battery and position
Better caching of scanned BT device properties
New setting for not starting the BTHRM together with HRM
Save some RAM by not definining functions if disabled in settings

11
apps/bthrv/README.md Normal file
View File

@ -0,0 +1,11 @@
# Bluetooth Heart Rate Variance
This app uses [BTHRM](https://banglejs.com/apps/#bthrm) and can calculate the HRV if the used bluetooth heart rate monitor delivers interval data.
## Usage
Just install and start the app. Select button resets the already measured values.
## Creator
[halemmerich](https://github.com/halemmerich)

1
apps/bthrv/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwJC/ABUMAokcAq0eAok+Aok2AgcCm0EAoUHmw2DAoMOAgMDh9jEgPAg/98cfn/gg/58cbv/ggcB8cz8HADIPjmIECgHB8OAAoVB8AFDgPgIQcBCwYFMAH4ARA"))

143
apps/bthrv/app.js Normal file
View File

@ -0,0 +1,143 @@
var btm = g.getHeight()-1;
var ui = false;
function clear(y){
g.reset();
g.clearRect(0,y,g.getWidth(),g.getHeight());
}
var startingTime;
var currentSlot = 0;
var hrvSlots = [10,20,30,60,120,300];
var hrvValues = {};
var rrRmsProgress;
var saved = false;
var rrNumberOfValues = 0;
var rrSquared = 0;
var rrLastValue
var rrMax;
var rrMin;
function calcHrv(rr){
//Calculate HRV with RMSSD method: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5624990/
for (currentRr of rr){
if (!rrMax) rrMax = currentRr;
if (!rrMin) rrMin = currentRr;
rrMax = Math.max(rrMax, currentRr);
rrMin = Math.min(rrMin, currentRr);
//print("Calc for: " + currentRr);
rrNumberOfValues++;
if (!rrLastValue){
rrLastValue = currentRr;
continue;
}
rrSquared += (rrLastValue - currentRr)*(rrLastValue - currentRr);
//print("rr²: " + rrSquared);
rrLastValue = currentRr;
}
var rms = Math.sqrt(rrSquared / rrNumberOfValues);
//print("rms: " + rms);
return rms;
}
function draw(y, hrv) {
clear(y);
var px = g.getWidth()/2;
var str = hrv.toFixed(1) + "ms";
g.reset();
g.setFontAlign(0,0);
g.setFontVector(40).drawString(str,px,y+20);
for (var i = 0; i < hrvSlots.length; i++){
str = hrvSlots[i] + "s: ";
if (hrvValues[hrvSlots[i]]) str += hrvValues[hrvSlots[i]].toFixed(1) + "ms";
g.setFontVector(16).drawString(str,px,y+44+(i*17));
}
g.setRotation(3);
g.setFontVector(12).drawString("Reset",g.getHeight()/2, g.getWidth()-10);
g.setRotation(0);
}
function onBtHrm(e) {
if (e.rr && !startingTime) Bangle.buzz(500);
if (e.rr && !startingTime) startingTime=Date.now();
//print("Event:" + e.rr);
var hrv = calcHrv(e.rr);
if (hrv){
if (currentSlot <= hrvSlots.length && (Date.now() - startingTime) > (hrvSlots[currentSlot] * 1000) && !hrvValues[hrvSlots[currentSlot]]){
hrvValues[hrvSlots[currentSlot]] = hrv;
currentSlot++;
}
}
if (!saved && currentSlot == hrvSlots.length){
var file = require('Storage').open("bthrv.csv", "a");
var data = new Date(startingTime).toISOString();
for (var c of hrvSlots){
data+=","+hrvValues[c];
}
data+="," + rrMax + "," + rrMin + ","+rrNumberOfValues;
data+="\n";
file.write(data);
saved = true;
Bangle.buzz(500);
}
if (hrv){
if (!ui){
Bangle.setUI("leftright", ()=>{
resetHrv();
clear(30);
});
ui = true;
}
draw(30, hrv);
}
}
function resetHrv(){
hrvValues={};
startingTime=undefined;
currentSlot=0;
saved=false;
rrNumberOfValues = 0;
rrSquared = 0;
rrLastValue = undefined;
rrMax = undefined;
rrMin = undefined;
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
if (Bangle.setBTHRMPower){
Bangle.on('BTHRM', onBtHrm);
Bangle.setBTHRMPower(1,'bthrv');
if (require('Storage').list(/bthrv.csv/).length == 0){
var file = require('Storage').open("bthrv.csv", "a");
var data = "Time";
for (var c of hrvSlots){
data+="," + c + "s";
}
data+=",RR_max,RR_min,Measurements";
data+="\n";
file.write(data);
}
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
} else {
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Missing BT HRM",g.getWidth()/2,g.getHeight()/2 - 16);
}
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrv'));

BIN
apps/bthrv/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

17
apps/bthrv/metadata.json Normal file
View File

@ -0,0 +1,17 @@
{
"id": "bthrv",
"name": "Bluetooth Heart Rate variance calculator",
"shortName": "BT HRV",
"version": "0.01",
"description": "Calculates HRV from a a BT HRM with interval data",
"icon": "app.png",
"type": "app",
"tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthrv.app.js","url":"app.js"},
{"name":"bthrv.recorder.js","url":"recorder.js"},
{"name":"bthrv.img","url":"app-icon.js","evaluate":true}
]
}

51
apps/bthrv/recorder.js Normal file
View File

@ -0,0 +1,51 @@
(function(recorders) {
recorders.bthrv = function() {
var lastGetValue = 0;
var lastUpdate = 0;
var rrHistory = [];
var hrv = "";
function onHRM(h) {
if(!h.rr) return;
if (lastUpdate + 3000 < Date.now()){
rrHistory = [];
}
rrHistory = rrHistory.concat(h.rr);
lastUpdate=Date.now();
}
return {
name : "BT HRV",
fields : ["BT HRV"],
getValues : () => {
if (lastGetValue + 10000 < Date.now()){
lastGetValue = Date.now();
if (rrHistory.length > 0){
if (rrHistory.length > 1){
var squaredSum = 0;
var last = rrHistory[0]
for (var i = 1; i < rrHistory.length; i++){
squaredSum += (last - rrHistory[i])*(last - rrHistory[i]);
last = rrHistory[i];
}
hrv = Math.sqrt(squaredSum/rrHistory.length);
}
}
}
result = [hrv];
hrv = "";
rrHistory = [];
return result;
},
start : () => {
Bangle.on('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('BTHRM', onHRM);
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor((rrHistory.length > 0)?"#00f":"#008").drawImage(atob("DAwBAAAACECECECEDGClacEEAAAA"),x,y)
};
}
})

BIN
apps/bthrv/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB