Skip to content

Commit

Permalink
Updated README to show current status
Browse files Browse the repository at this point in the history
  • Loading branch information
jones139 committed Dec 29, 2023
1 parent 34cf6c9 commit 980f5c7
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 63 deletions.
31 changes: 18 additions & 13 deletions apps/hrmtest/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
Extract hrm raw signal data to CSV file
=======================================
Display and Log HRM Raw Data
============================

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.
When started, this app starts the HRM and displays a number of values on a 3x3 grid,
with a graph below.

Updated to work with new API and includes support for Bangle.js 2. Additional capability includes:
The values displayed are:
| HR (bpm) | Confidence (%) | Log Filename |
| Latest Raw Reading | Latest Environment Reading | Latest Analogue Reading |
| Register 0x17 LED Intensity | Register 0x19 LED Intensity | Period (seconds) covered by the graph |

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.
Pressing the physical watch button stopps the logging and saves the file - it can be retrieved using the espruino [web IDE](https://www.espruino.com/ide/)

-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});­
It is pretty rough as an application at the moment, but if you run it for a few seconds you can see things changing which is useful. The issues I know about with it are listed below - I will fix them soon, unless I abandon BanlgleJS in favour of PineTime for a while given the amount of trouble this HRM is causing!

the 20 in the boot code means the hrm will poll every 20ms (50Hz) instead of the default 40.
Notes / issues:
- This version has the hrmGreenAdjust option set to true, so you can see the HRM device changing the LED intensity.
- If you change hrmGreenAdjust to false in the source code you should be able to change the LED intensity with vertical swipes of the screen.
- The environment intensity callback is never triggered for some reason, so this is always zero.
- The register 0x19 value does not seem to change.
- It is supposed to only maintain 170 hrm readings, so the graph should cover just under 7
seconds of data - but something is not right about how I am managing the data arrays because the data period keeps going up.
- The Analoge value is read from pin 29 as recommended here: https://forum.espruino.com/comments/17236199/, but I do not think it is reading the heart rate value.

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
168 changes: 118 additions & 50 deletions apps/hrmtest/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,19 @@
var counter = 15;
var logging_started;
var interval;
var value;
var filt;

var fileClosed = 0;
var Storage = require("Storage");
//var Storage = require("Storage");
var file;
var fileName;

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 textH = screenH/2;
var graphOriginX = 0;
var graphOriginY = textOriginY + textH + 1;
var graphH = screenH - textH;
Expand All @@ -35,7 +34,13 @@ 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.
rawVals.push(0);
var anlgVals = []; // Raw values read from analogue pin.
anlgVals.push(0);
var envVals = []; // Environment values by i2c
envVals.push(0);
var timeVals = [];
timeVals.push(getTime());
var rawBufSize = screenW;

var ledCurrentVals = [0x30, 0x50, 0x5A, 0xE0];
Expand Down Expand Up @@ -75,48 +80,80 @@ function drawText() {
g.drawString(HRConfidence+"%", textOriginX+70, y);
g.setFont("6x8", 2);
if (logging_started) {
g.drawString("RUN", textOriginX+115, y);
g.drawString(fileName, textOriginX+115, y);
} else {
g.drawString("STOP", textOriginX+115, y);
}

y = y + 28;
g.setFont("6x8", 3);
g.setFont("6x8", 2);
g.drawString(rawVals[rawVals.length -1], textOriginX + 0, y);
g.drawString(envVals[envVals.length -1], textOriginX + 70, y);
g.drawString(anlgVals[anlgVals.length -1], textOriginX + 130, y);

y = y + 20;
g.setFont("6x8", 2);
g.drawString(slot0LedCurrentVal, textOriginX + 0, y);
g.drawString(ledCurrentIdx, textOriginX + 70, y);
g.drawString(rawVals.length, textOriginX + 130, y);
g.drawString((timeVals[timeVals.length-1]-timeVals[0])+"s", textOriginX + 130, y);

g.setFont("6x8", 2);
//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.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);
//g.setFont("6x8", 4);
//g.drawString("^",screenSize/2 , 150);

drawGraph();
g.flip();
}


function drawGraph() {
let i = 0;
let y = 0;
//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++) {

// Draw raw values as solid bars
let minVal = rawVals[0];
let maxVal = minVal;
for (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);
let yMin = screenH;
let yMax = graphOriginY;
for (i=0;i<rawVals.length-1; i++) {
y = yMin + (rawVals[i]-minVal)*(yMax-yMin)/(maxVal-minVal);
if (y < yMax) y = yMax; // Screen is inverted, so comparison looks wrong!
if (y > yMin) y = yMin;
g.setColor('#ff0000').drawRect(i,yMin,i+1,y);
}
console.log("drawGraph() - minVal="+minVal
+", maxVal="+maxVal
+ ", yMin="+yMin
+", yMax="+yMax
+", y="+y);

// Draw analogue values as a line
minVal = anlgVals[0];
maxVal = minVal;
for (i=0;i<anlgVals.length; i++) {
if (anlgVals[i]<minVal) minVal = anlgVals[i];
if (anlgVals[i]>maxVal) maxVal = anlgVals[i];
}
let lastPoint = [0,0]
for (i=0;i<anlgVals.length-1; i++) {
y = yMin + (anlgVals[i]-minVal)*(yMax-yMin)/(maxVal-minVal);
if (y < yMax) y = yMax; // Screen is inverted, so comparison looks wrong!
if (y > yMin) y = yMin;
g.setColor('#000000').drawLine(lastPoint[0],lastPoint[1],i+1,y);
lastPoint=[i, y];
}

}

function setLedCurrent() {
Expand All @@ -125,6 +162,14 @@ function setLedCurrent() {
//Bangle.hrmWr(0x19, ledCurrentVals[ledCurrentIdx]);
}

function getLedCurrent() {
/**
* Read the LED current registers from the HRM and populate the relevant global variables.
*/
slot0LedCurrentVal = Bangle.hrmRd(0x17,0);
ledCurentIdx = Bangle.hrmRd(0x19);
}

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.
Expand Down Expand Up @@ -154,9 +199,13 @@ function changeSlot0Current(changeVal) {
}

function initialiseHrm() {
Bangle.setLCDPower(1);
Bangle.setHRMPower(1);
Bangle.setOptions({
hrmGreenAdjust: false
backlightTimeout:0,
powerSave:false,
hrmPushEnv:true,
hrmGreenAdjust: true
});
setLedCurrent();

Expand All @@ -165,14 +214,14 @@ function initialiseHrm() {
function startStopHrm() {
if (!logging_started) {
console.log("startStopHrm - starting");
var filename = "";
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);
fileName = "HRM_" + i.toString() + ".csv";
if(fileExists(fileName) == 0){
file = require("Storage").open(fileName,"w");
console.log("creating new file " + fileName);
fileset = true;
}
if(fileset){
Expand All @@ -181,12 +230,13 @@ function startStopHrm() {
}

if (!fileset){
console.log("overwiting file");
file = require("Storage").open("HRM_data.csv","w");
fileName = "HRM_0.csv";
console.log("overwiting file "+fileName);
file = require("Storage").open(fileName,"w");
}

file.write("");
file = require("Storage").open(filename,"a");
file = require("Storage").open(fileName,"a");

//launchtime = 0 | getTime();
//file.write(launchtime + "," + "\n");
Expand All @@ -208,21 +258,26 @@ function startStopHrm() {
}
}

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'
*/
Bangle.setLocked(false); // Prevent the touch screen locking so we can use scroll inputs
getLedCurrent();
drawText();
}


function fmtMSS(e) {
h = Math.floor(e / 3600);
e %= 3600;
m = Math.floor(e / 60);
s = e % 60;
return h + ":" + m + ':' + s;
}


///////////////////////////////////////
// Main Program
console.log("Registering button callback");
Expand All @@ -248,27 +303,40 @@ Bangle.on("swipe",function(directionLR, directionUD){

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)
let value = hrm.raw;
let filt = hrm.filt;
let valueA = Math.round(analogRead(29)* 16383);
rawVals.push(value);
anlgVals.push(valueA);
timeVals.push(getTime());
if (rawVals.length > rawBufSize) {
rawVals.shift();
//algVals.shift();
anlgVals.shift();
timeVals.shift();
}
file.write(timeVals[timeVals.length-1] + "," + value + "," + filt
+ "," + valueA + "," + envVals[envVals.length-1] + "," + HRVal + "," + HRConfidence + "\n");
});

console.log("Registering hrm environment data callback");
Bangle.on('HRM-env', function (hrm) {
let value = hrm;
envVals.push(value);
if (envVals.length > rawBufSize) {
envVals.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();
// Start logging
startStopHrm();



0 comments on commit 980f5c7

Please sign in to comment.