diff --git a/apps/bthrv/ChangeLog b/apps/bthrv/ChangeLog new file mode 100644 index 000000000..0e51186a4 --- /dev/null +++ b/apps/bthrv/ChangeLog @@ -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 diff --git a/apps/bthrv/README.md b/apps/bthrv/README.md new file mode 100644 index 000000000..8a80b0fd4 --- /dev/null +++ b/apps/bthrv/README.md @@ -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) diff --git a/apps/bthrv/app-icon.js b/apps/bthrv/app-icon.js new file mode 100644 index 000000000..4d4cf6354 --- /dev/null +++ b/apps/bthrv/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/ABUMAokcAq0eAok+Aok2AgcCm0EAoUHmw2DAoMOAgMDh9jEgPAg/98cfn/gg/58cbv/ggcB8cz8HADIPjmIECgHB8OAAoVB8AFDgPgIQcBCwYFMAH4ARA")) diff --git a/apps/bthrv/app.js b/apps/bthrv/app.js new file mode 100644 index 000000000..7f6ec2d35 --- /dev/null +++ b/apps/bthrv/app.js @@ -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')); diff --git a/apps/bthrv/app.png b/apps/bthrv/app.png new file mode 100644 index 000000000..7a45b9c42 Binary files /dev/null and b/apps/bthrv/app.png differ diff --git a/apps/bthrv/metadata.json b/apps/bthrv/metadata.json new file mode 100644 index 000000000..6a8e7e940 --- /dev/null +++ b/apps/bthrv/metadata.json @@ -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} + ] +} diff --git a/apps/bthrv/recorder.js b/apps/bthrv/recorder.js new file mode 100644 index 000000000..0fce6971e --- /dev/null +++ b/apps/bthrv/recorder.js @@ -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) + }; + } +}) + diff --git a/apps/bthrv/screenshot.png b/apps/bthrv/screenshot.png new file mode 100644 index 000000000..1cd153160 Binary files /dev/null and b/apps/bthrv/screenshot.png differ