From dc435c3733e227b0ae5f0a7145a2adefc747d2d5 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Tue, 1 Feb 2022 19:34:41 +0100 Subject: [PATCH] BT HRV - Adds app for taking HRV measurements --- apps/bthrv/ChangeLog | 11 +++ apps/bthrv/README.md | 11 +++ apps/bthrv/app-icon.js | 1 + apps/bthrv/app.js | 143 ++++++++++++++++++++++++++++++++++++++ apps/bthrv/app.png | Bin 0 -> 670 bytes apps/bthrv/metadata.json | 17 +++++ apps/bthrv/recorder.js | 51 ++++++++++++++ apps/bthrv/screenshot.png | Bin 0 -> 4039 bytes 8 files changed, 234 insertions(+) create mode 100644 apps/bthrv/ChangeLog create mode 100644 apps/bthrv/README.md create mode 100644 apps/bthrv/app-icon.js create mode 100644 apps/bthrv/app.js create mode 100644 apps/bthrv/app.png create mode 100644 apps/bthrv/metadata.json create mode 100644 apps/bthrv/recorder.js create mode 100644 apps/bthrv/screenshot.png 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 0000000000000000000000000000000000000000..7a45b9c429c2994f4aed3bf334317e8489e9953d GIT binary patch literal 670 zcmV;P0%84$P)`cK`bmR#E4@2 z1H^QKRf^b(omfQ0#zs&<@qrMrPzl6XX(K*R5W#4OBzQ^I;{4W~ySa~Ko$NI*A1r6i zot-nk!_1kT3xPl&5C{YU{|yU)ZeV4jg=FOj5CNC-ny}zG-gen7p5x}mQ)~q$u;aLFt!VL1hmY(%*qsjH<-YS`mIy`fc!i1_ol`cNRsRE zAtp-=90S_bJ`OAfw#pxr9hObk!G2(k+NUziJ&kG!^Z=iMdz3GA5jd~*7i^0hBsqcN z1YXOgkJ4`0Nniu8xsJY8;J&n+t@G|*R%wE$#`7H8%MoxJ7*czkVzkR2bmFeSzR+X9 z1@&K2-#-0bYtBh&t4kYDat4k9-<{_puoGAU`~-gM{Sjb+Vid9O!8pm4;*8?;1FI$I z8_*2AQ{PBxE;p%?w9SZ { + 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 0000000000000000000000000000000000000000..1cd153160affa2e420871c9e11225788f254587e GIT binary patch literal 4039 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 literal 0 HcmV?d00001