diff --git a/apps/hrmtest/ChangeLog b/apps/hrmtest/ChangeLog new file mode 100644 index 0000000000..9dda489a9c --- /dev/null +++ b/apps/hrmtest/ChangeLog @@ -0,0 +1,6 @@ +0.01: New App! +0.02: Fixes +0.03: updated to work with new API and additional features added such as longer recording time and additional filtered data +0.04: added support for bangle.js 2 +0.05: Added time to output file, reallocated BTN1 to start timer. +0.06: Created hrmtestapp based on hrrawexp diff --git a/apps/hrmtest/README.md b/apps/hrmtest/README.md new file mode 100644 index 0000000000..0d9faf604e --- /dev/null +++ b/apps/hrmtest/README.md @@ -0,0 +1,20 @@ +Extract hrm raw signal data to CSV file +======================================= + +Simple app that will run the heart rate monitor for a defined period of time you set at the start and record data to a csv file. + +Updated to work with new API and includes support for Bangle.js 2. Additional capability includes: + +1. Now also records upto 2 hours - if you cancel at any time the CSV file will still be there, the timer you set at the start is more so that you get an alert when it's complete. +2. Along with raw PPG readings, it also records bandpassed filtered data in a second column, available in the new API. +3. Rather than overwriting 1 data file, the app will record upto 5 files before recording to a generic data file as a fallback if all 5 allocated files remain on the watch storage. The limit is in place to avoid going over storage limits as these files can get large over time. + +-The hrm sensor is sampled @50Hz on the Bangle.JS 1 and 25Hz on the Bangle 2 by default. At least on the Bangle 2 you can change the sample rate by using the 'custom boot code' app and running this line: +Bangle.setOptions({hrmPollInterval:20});­ + +the 20 in the boot code means the hrm will poll every 20ms (50Hz) instead of the default 40. + +4. On the bangle.JS 2 you can swipe up to begin recording, and on the Bangle.JS 1 you just use the top button. + +For Bangle 1, there is an example Python script that can process this signal, smooth it and also extract a myriad of heart rate variability metrics using the hrvanalysis library. I will be working on a method for Bangle 2 because the data seems more noisy so will need a different processing method: +https://github.com/jabituyaben/BangleJS-HRM-Signal-Processing diff --git a/apps/hrmtest/app-icon.js b/apps/hrmtest/app-icon.js new file mode 100644 index 0000000000..01718675ed --- /dev/null +++ b/apps/hrmtest/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDCBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAP4AAD/8A//AAH//D//gAP//n//gAf/////AA/////+AA/////8MA/////4cB/////w+B/////B+B////+D+B////8H+B////4P+B////w/+B/+P/h/+B/+H/D/+A/+D+H/8A//h8P/8A//w4f/8Af/4A//4Af/8B//4AP/+D//wAP//H//wAH/////gAD/////AAB////+AAA////+AAAf///8AAAP///wAAAH///gAAAD///AAAAA//+AAAAAf/4AAAAAH/gAAAAAB+AAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")) diff --git a/apps/hrmtest/app-icon.png b/apps/hrmtest/app-icon.png new file mode 100644 index 0000000000..b120cdb425 Binary files /dev/null and b/apps/hrmtest/app-icon.png differ diff --git a/apps/hrmtest/app.js b/apps/hrmtest/app.js new file mode 100644 index 0000000000..466cb5e054 --- /dev/null +++ b/apps/hrmtest/app.js @@ -0,0 +1,274 @@ +/** + * HRM Test - An app to test various HRM sensor configuration options, and save HRM data + * to a file for offline analysis. + * It initially starts the hrm using default settings and updates the display whenever + * raw HRM data is received. + * Swiping the screen changes the LED power. + * Pressing the button stops the HRM. + * The top third of the screen shows the HRM parameters, including calculaed bpm. + * The lower two thirds is a graph of raw HRM readings. + * Graham Jones, 2023, based on official BangleJS hrrawexp app + */ + +var counter = 15; +var logging_started; +var interval; +var value; +var filt; + +var fileClosed = 0; +var Storage = require("Storage"); +var file; + +var screenSize = g.getHeight(); +var screenH = g.getHeight(); +var screenW = g.getWidth(); +var textOriginY = 0; +var textOriginX = 0; +var textW = screenW; +var textH = screenH/3; +var graphOriginX = 0; +var graphOriginY = textOriginY + textH + 1; +var graphH = screenH - textH; +var graphW = screenW; + +var HRVal = 0; // latest HRM readings +var HRConfidence = 0; +var rawVals = []; // Raw values read by i2c +var algVals = []; // Raw values read from analogue pin. +var rawBufSize = screenW; + +var ledCurrentVals = [0x30, 0x50, 0x5A, 0xE0]; +var ledCurrentIdx = 0; + +var slot0LedCurrentVal = 15; //64 + + + +function fileExists(fName){ + /** + * Returns true if a file named by the string parameter fName exists in storage, or else + * returns false + */ + s = require('Storage'); + var fileList = s.list(); + var fileExists = false; + for (let i = 0; i < fileList.length; i++) { + fileExists = fileList[i].includes(fName); + if(fileExists){ + break; + } + } + return fileExists; +} + +function drawText() { + //g.clear(); + g.clearRect(textOriginX,textOriginY,textW,textH); + g.setColor("#CC00CC"); + g.setFontAlign(-1, -1, 0); // top left origin + + y = textOriginY; + g.setFont("6x8", 3); + g.drawString(HRVal, textOriginX, y); + g.setFont("6x8", 2); + g.drawString(HRConfidence+"%", textOriginX+70, y); + g.setFont("6x8", 2); + if (logging_started) { + g.drawString("RUN", textOriginX+115, y); + } else { + g.drawString("STOP", textOriginX+115, y); + } + + y = y + 28; + g.setFont("6x8", 3); + g.drawString(slot0LedCurrentVal, textOriginX + 0, y); + g.drawString(ledCurrentIdx, textOriginX + 70, y); + g.drawString(rawVals.length, textOriginX + 130, y); + + g.setFont("6x8", 2); + //g.setFontAlign(-1, -1); + g.drawString("+", screenSize-10, screenSize/2); + g.drawString("-", 10, screenSize/2); + g.drawString("GO",screenSize/2 , (screenSize/2)+(screenSize/5)); + //g.setColor("#ffffff"); + //g.setFontAlign(0, 0); // center font + g.setFont("6x8", 4); + g.drawString("^",screenSize/2 , 150); + + drawGraph(); + g.flip(); +} + + +function drawGraph() { + //g.clear(); + g.clearRect(graphOriginX,graphOriginY,graphOriginX + graphW, graphOriginY + graphH); + var minVal = rawVals[0]; + var maxVal = minVal; + for (var i=0;imaxVal) maxVal = rawVals[i]; + } + var yMin = screenH; + var yMax = graphOriginY; + console.log("drawGraph() - minVal="+minVal+", maxVal="+maxVal); + for (var i=0;i ledCurrentVals.length -1 ) { + ledCurrentIdx = 0; + } + if (ledCurrentIdx < 0) { + ledCurrentIdx = ledCurrentVals.length -1; + } + setLedCurrent(); + drawText(); +} + +function changeSlot0Current(changeVal) { + // Update the requested slot0Current by changing it + // Wraps around to the upper or lower limit if it is out of range. + slot0LedCurrentVal += changeVal; + if (slot0LedCurrentVal > 0xef) { + slot0LedCurrentVal = 0; + } + if (slot0LedCurrentVal < 0) { + slot0LedCurrentVal = 0xef; + } + setLedCurrent(); + drawText(); +} + +function initialiseHrm() { + Bangle.setHRMPower(1); + Bangle.setOptions({ + hrmGreenAdjust: false + }); + setLedCurrent(); + +} + +function startStopHrm() { + if (!logging_started) { + console.log("startStopHrm - starting"); + var filename = ""; + var fileset = false; + + for (let i = 0; i < 5; i++) { + filename = "HRM_data" + i.toString() + ".csv"; + if(fileExists(filename) == 0){ + file = require("Storage").open(filename,"w"); + console.log("creating new file " + filename); + fileset = true; + } + if(fileset){ + break; + } + } + + if (!fileset){ + console.log("overwiting file"); + file = require("Storage").open("HRM_data.csv","w"); + } + + file.write(""); + file = require("Storage").open(filename,"a"); + + //launchtime = 0 | getTime(); + //file.write(launchtime + "," + "\n"); + logging_started = true; + counter = counter * 60; + interval = setInterval(countDownTimerCallback, 1000); + + initialiseHrm(); + + } else { + console.log("startStopHrm - stopping"); + Bangle.setHRMPower(0); + clearInterval(interval); + g.drawString("Done", g.getWidth() / 2, g.getHeight() / 2); + Bangle.buzz(500, 1); + fileClosed = 1; + logging_started = false; + + } +} + +function fmtMSS(e) { + h = Math.floor(e / 3600); + e %= 3600; + m = Math.floor(e / 60); + s = e % 60; + return h + ":" + m + ':' + s; +} + +function countDownTimerCallback() { + /** + * Called once per second by timer 'interval' + */ + drawText(); +} + +/////////////////////////////////////// +// Main Program +console.log("Registering button callback"); +setWatch(startStopHrm, BTN1, { repeat: true }); +//setWatch(btn2Pressed, BTN2, { repeat: true }); +//setWatch(btn3Pressed, BTN3, { repeat: true }); + +console.log("Registering swipe callback"); +Bangle.on("swipe",function(directionLR, directionUD){ + if (1==directionLR){ + changeLedCurrent(1); + } + else if(directionLR == -1){ + changeLedCurrent(-1); + } + else if (directionUD ==1){ + changeSlot0Current(5); + } + else if (directionUD == -1) { + changeSlot0Current(-5); + } + }); + +console.log("Registering raw hrm data callback"); +Bangle.on('HRM-raw', function (hrm) { + value = hrm.raw; + filt = hrm.filt; + let alg = Math.round(analogRead(29)* 16383); + rawVals.push(alg); // FIXME - pushing analogue value for testing + //algVals.push(alg) + if (rawVals.length > rawBufSize) { + rawVals.shift(); + //algVals.shift(); + } + //var dataArray = [value,filt,HRVal,HRConfidence]; + file.write(getTime() + "," + value + "," + filt + + "," + HRVal + "," + HRConfidence + "\n"); +}); + +console.log("Registering hrm values callback"); +Bangle.on('HRM', function (hrmB) { + HRVal = hrmB.bpm; + HRConfidence = hrmB.confidence; +}); + +drawText(); + + + diff --git a/apps/hrmtest/interface.html b/apps/hrmtest/interface.html new file mode 100644 index 0000000000..7033414151 --- /dev/null +++ b/apps/hrmtest/interface.html @@ -0,0 +1,54 @@ + + + + + +
+ + + + + + + diff --git a/apps/hrmtest/metadata.json b/apps/hrmtest/metadata.json new file mode 100644 index 0000000000..b43ec9ae0e --- /dev/null +++ b/apps/hrmtest/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "hrmtest", + "name": "HRM Sensor Test", + "shortName": "HRM_Test", + "version": "0.06", + "description": "Test various HRM configuration options", + "icon": "app-icon.png", + "tags": "", + "readme": "README.md", + "interface": "interface.html", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"hrmtest.app.js","url":"app.js"}, + {"name":"hrmtest.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/testing/hrm/Pinetime_Banglejs_hrm_leds.jpg b/testing/hrm/Pinetime_Banglejs_hrm_leds.jpg new file mode 100644 index 0000000000..9fdcc844fb Binary files /dev/null and b/testing/hrm/Pinetime_Banglejs_hrm_leds.jpg differ diff --git a/testing/hrm/Pinetime_HRM.jpg b/testing/hrm/Pinetime_HRM.jpg new file mode 100644 index 0000000000..c072ce70e6 Binary files /dev/null and b/testing/hrm/Pinetime_HRM.jpg differ