Skip to content

Commit

Permalink
Added hrmtest app to try out different LED intensities etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
jones139 committed Dec 29, 2023
1 parent 1e6b0b4 commit 34cf6c9
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 0 deletions.
6 changes: 6 additions & 0 deletions apps/hrmtest/ChangeLog
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
20 changes: 20 additions & 0 deletions apps/hrmtest/README.md
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
1 change: 1 addition & 0 deletions apps/hrmtest/app-icon.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added apps/hrmtest/app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
274 changes: 274 additions & 0 deletions apps/hrmtest/app.js
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();



54 changes: 54 additions & 0 deletions apps/hrmtest/interface.html
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>
16 changes: 16 additions & 0 deletions apps/hrmtest/metadata.json
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}
]
}
Binary file added testing/hrm/Pinetime_Banglejs_hrm_leds.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testing/hrm/Pinetime_HRM.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 34cf6c9

Please sign in to comment.