forked from espruino/BangleApps
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added hrmtest app to try out different LED intensities etc.
- Loading branch information
Showing
9 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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;i<rawVals.length; i++) { | ||
if (rawVals[i]<minVal) minVal = rawVals[i]; | ||
if (rawVals[i]>maxVal) maxVal = rawVals[i]; | ||
} | ||
var yMin = screenH; | ||
var yMax = graphOriginY; | ||
console.log("drawGraph() - minVal="+minVal+", maxVal="+maxVal); | ||
for (var i=0;i<rawVals.length-1; i++) { | ||
var y = yMin + (rawVals[i]-minVal)*(yMax-yMin)/(maxVal-minVal); | ||
g.drawRect(i,yMin,i+1,y); | ||
} | ||
} | ||
|
||
function setLedCurrent() { | ||
console.log("setLedCurrent()"); | ||
Bangle.hrmWr(0x17,slot0LedCurrentVal); | ||
//Bangle.hrmWr(0x19, ledCurrentVals[ledCurrentIdx]); | ||
} | ||
|
||
function changeLedCurrent(changeVal) { | ||
// Update the requested ledCurrent by changing its index by changeVal | ||
// Wraps around to the ends of ledCurrentVals if index is out of range. | ||
ledCurrentIdx += changeVal; | ||
if (ledCurrentIdx > 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(); | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
<html> | ||
<head> | ||
<link rel="stylesheet" href="../../css/spectre.min.css"> | ||
</head> | ||
<body> | ||
<div id="data"></div> | ||
<button class="btn btn-default" id="btnSave">Save</button> | ||
<button class="btn btn-default" id="btnDelete">Delete</button> | ||
|
||
<script src="../../core/lib/interface.js"></script> | ||
<script> | ||
var dataElement = document.getElementById("data"); | ||
var csvData = ""; | ||
|
||
function getData() { | ||
// show loading window | ||
Util.showModal("Loading..."); | ||
// get the data | ||
dataElement.innerHTML = ""; | ||
Util.readStorageFile(`hrm_log.csv`,data=>{ | ||
csvData = data.trim(); | ||
// remove window | ||
Util.hideModal(); | ||
// If no data, report it and exit | ||
if (data.length==0) { | ||
dataElement.innerHTML = "<b>No data found</b>"; | ||
return; | ||
} | ||
else{ | ||
dataElement.innerHTML = "<b>data file found</b>"; | ||
return; | ||
} | ||
}); | ||
} | ||
|
||
// You can call a utility function to save the data | ||
document.getElementById("btnSave").addEventListener("click", function() { | ||
Util.saveCSV("HRM_data", csvData); | ||
}); | ||
// Or you can also delete the file | ||
document.getElementById("btnDelete").addEventListener("click", function() { | ||
Util.showModal("Deleting..."); | ||
Util.eraseStorageFile("hrm_log.csv", function() { | ||
Util.hideModal(); | ||
getData(); | ||
}); | ||
}); | ||
// Called when app starts | ||
function onInit() { | ||
getData(); | ||
} | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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} | ||
] | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.