Skip to content

Commit 34cf6c9

Browse files
committed
Added hrmtest app to try out different LED intensities etc.
1 parent 1e6b0b4 commit 34cf6c9

File tree

9 files changed

+371
-0
lines changed

9 files changed

+371
-0
lines changed

apps/hrmtest/ChangeLog

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
0.01: New App!
2+
0.02: Fixes
3+
0.03: updated to work with new API and additional features added such as longer recording time and additional filtered data
4+
0.04: added support for bangle.js 2
5+
0.05: Added time to output file, reallocated BTN1 to start timer.
6+
0.06: Created hrmtestapp based on hrrawexp

apps/hrmtest/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Extract hrm raw signal data to CSV file
2+
=======================================
3+
4+
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.
5+
6+
Updated to work with new API and includes support for Bangle.js 2. Additional capability includes:
7+
8+
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.
9+
2. Along with raw PPG readings, it also records bandpassed filtered data in a second column, available in the new API.
10+
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.
11+
12+
-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:
13+
Bangle.setOptions({hrmPollInterval:20});­
14+
15+
the 20 in the boot code means the hrm will poll every 20ms (50Hz) instead of the default 40.
16+
17+
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.
18+
19+
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:
20+
https://github.com/jabituyaben/BangleJS-HRM-Signal-Processing

apps/hrmtest/app-icon.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/hrmtest/app-icon.png

826 Bytes
Loading

apps/hrmtest/app.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/**
2+
* HRM Test - An app to test various HRM sensor configuration options, and save HRM data
3+
* to a file for offline analysis.
4+
* It initially starts the hrm using default settings and updates the display whenever
5+
* raw HRM data is received.
6+
* Swiping the screen changes the LED power.
7+
* Pressing the button stops the HRM.
8+
* The top third of the screen shows the HRM parameters, including calculaed bpm.
9+
* The lower two thirds is a graph of raw HRM readings.
10+
* Graham Jones, 2023, based on official BangleJS hrrawexp app
11+
*/
12+
13+
var counter = 15;
14+
var logging_started;
15+
var interval;
16+
var value;
17+
var filt;
18+
19+
var fileClosed = 0;
20+
var Storage = require("Storage");
21+
var file;
22+
23+
var screenSize = g.getHeight();
24+
var screenH = g.getHeight();
25+
var screenW = g.getWidth();
26+
var textOriginY = 0;
27+
var textOriginX = 0;
28+
var textW = screenW;
29+
var textH = screenH/3;
30+
var graphOriginX = 0;
31+
var graphOriginY = textOriginY + textH + 1;
32+
var graphH = screenH - textH;
33+
var graphW = screenW;
34+
35+
var HRVal = 0; // latest HRM readings
36+
var HRConfidence = 0;
37+
var rawVals = []; // Raw values read by i2c
38+
var algVals = []; // Raw values read from analogue pin.
39+
var rawBufSize = screenW;
40+
41+
var ledCurrentVals = [0x30, 0x50, 0x5A, 0xE0];
42+
var ledCurrentIdx = 0;
43+
44+
var slot0LedCurrentVal = 15; //64
45+
46+
47+
48+
function fileExists(fName){
49+
/**
50+
* Returns true if a file named by the string parameter fName exists in storage, or else
51+
* returns false
52+
*/
53+
s = require('Storage');
54+
var fileList = s.list();
55+
var fileExists = false;
56+
for (let i = 0; i < fileList.length; i++) {
57+
fileExists = fileList[i].includes(fName);
58+
if(fileExists){
59+
break;
60+
}
61+
}
62+
return fileExists;
63+
}
64+
65+
function drawText() {
66+
//g.clear();
67+
g.clearRect(textOriginX,textOriginY,textW,textH);
68+
g.setColor("#CC00CC");
69+
g.setFontAlign(-1, -1, 0); // top left origin
70+
71+
y = textOriginY;
72+
g.setFont("6x8", 3);
73+
g.drawString(HRVal, textOriginX, y);
74+
g.setFont("6x8", 2);
75+
g.drawString(HRConfidence+"%", textOriginX+70, y);
76+
g.setFont("6x8", 2);
77+
if (logging_started) {
78+
g.drawString("RUN", textOriginX+115, y);
79+
} else {
80+
g.drawString("STOP", textOriginX+115, y);
81+
}
82+
83+
y = y + 28;
84+
g.setFont("6x8", 3);
85+
g.drawString(slot0LedCurrentVal, textOriginX + 0, y);
86+
g.drawString(ledCurrentIdx, textOriginX + 70, y);
87+
g.drawString(rawVals.length, textOriginX + 130, y);
88+
89+
g.setFont("6x8", 2);
90+
//g.setFontAlign(-1, -1);
91+
g.drawString("+", screenSize-10, screenSize/2);
92+
g.drawString("-", 10, screenSize/2);
93+
g.drawString("GO",screenSize/2 , (screenSize/2)+(screenSize/5));
94+
//g.setColor("#ffffff");
95+
//g.setFontAlign(0, 0); // center font
96+
g.setFont("6x8", 4);
97+
g.drawString("^",screenSize/2 , 150);
98+
99+
drawGraph();
100+
g.flip();
101+
}
102+
103+
104+
function drawGraph() {
105+
//g.clear();
106+
g.clearRect(graphOriginX,graphOriginY,graphOriginX + graphW, graphOriginY + graphH);
107+
var minVal = rawVals[0];
108+
var maxVal = minVal;
109+
for (var i=0;i<rawVals.length; i++) {
110+
if (rawVals[i]<minVal) minVal = rawVals[i];
111+
if (rawVals[i]>maxVal) maxVal = rawVals[i];
112+
}
113+
var yMin = screenH;
114+
var yMax = graphOriginY;
115+
console.log("drawGraph() - minVal="+minVal+", maxVal="+maxVal);
116+
for (var i=0;i<rawVals.length-1; i++) {
117+
var y = yMin + (rawVals[i]-minVal)*(yMax-yMin)/(maxVal-minVal);
118+
g.drawRect(i,yMin,i+1,y);
119+
}
120+
}
121+
122+
function setLedCurrent() {
123+
console.log("setLedCurrent()");
124+
Bangle.hrmWr(0x17,slot0LedCurrentVal);
125+
//Bangle.hrmWr(0x19, ledCurrentVals[ledCurrentIdx]);
126+
}
127+
128+
function changeLedCurrent(changeVal) {
129+
// Update the requested ledCurrent by changing its index by changeVal
130+
// Wraps around to the ends of ledCurrentVals if index is out of range.
131+
ledCurrentIdx += changeVal;
132+
if (ledCurrentIdx > ledCurrentVals.length -1 ) {
133+
ledCurrentIdx = 0;
134+
}
135+
if (ledCurrentIdx < 0) {
136+
ledCurrentIdx = ledCurrentVals.length -1;
137+
}
138+
setLedCurrent();
139+
drawText();
140+
}
141+
142+
function changeSlot0Current(changeVal) {
143+
// Update the requested slot0Current by changing it
144+
// Wraps around to the upper or lower limit if it is out of range.
145+
slot0LedCurrentVal += changeVal;
146+
if (slot0LedCurrentVal > 0xef) {
147+
slot0LedCurrentVal = 0;
148+
}
149+
if (slot0LedCurrentVal < 0) {
150+
slot0LedCurrentVal = 0xef;
151+
}
152+
setLedCurrent();
153+
drawText();
154+
}
155+
156+
function initialiseHrm() {
157+
Bangle.setHRMPower(1);
158+
Bangle.setOptions({
159+
hrmGreenAdjust: false
160+
});
161+
setLedCurrent();
162+
163+
}
164+
165+
function startStopHrm() {
166+
if (!logging_started) {
167+
console.log("startStopHrm - starting");
168+
var filename = "";
169+
var fileset = false;
170+
171+
for (let i = 0; i < 5; i++) {
172+
filename = "HRM_data" + i.toString() + ".csv";
173+
if(fileExists(filename) == 0){
174+
file = require("Storage").open(filename,"w");
175+
console.log("creating new file " + filename);
176+
fileset = true;
177+
}
178+
if(fileset){
179+
break;
180+
}
181+
}
182+
183+
if (!fileset){
184+
console.log("overwiting file");
185+
file = require("Storage").open("HRM_data.csv","w");
186+
}
187+
188+
file.write("");
189+
file = require("Storage").open(filename,"a");
190+
191+
//launchtime = 0 | getTime();
192+
//file.write(launchtime + "," + "\n");
193+
logging_started = true;
194+
counter = counter * 60;
195+
interval = setInterval(countDownTimerCallback, 1000);
196+
197+
initialiseHrm();
198+
199+
} else {
200+
console.log("startStopHrm - stopping");
201+
Bangle.setHRMPower(0);
202+
clearInterval(interval);
203+
g.drawString("Done", g.getWidth() / 2, g.getHeight() / 2);
204+
Bangle.buzz(500, 1);
205+
fileClosed = 1;
206+
logging_started = false;
207+
208+
}
209+
}
210+
211+
function fmtMSS(e) {
212+
h = Math.floor(e / 3600);
213+
e %= 3600;
214+
m = Math.floor(e / 60);
215+
s = e % 60;
216+
return h + ":" + m + ':' + s;
217+
}
218+
219+
function countDownTimerCallback() {
220+
/**
221+
* Called once per second by timer 'interval'
222+
*/
223+
drawText();
224+
}
225+
226+
///////////////////////////////////////
227+
// Main Program
228+
console.log("Registering button callback");
229+
setWatch(startStopHrm, BTN1, { repeat: true });
230+
//setWatch(btn2Pressed, BTN2, { repeat: true });
231+
//setWatch(btn3Pressed, BTN3, { repeat: true });
232+
233+
console.log("Registering swipe callback");
234+
Bangle.on("swipe",function(directionLR, directionUD){
235+
if (1==directionLR){
236+
changeLedCurrent(1);
237+
}
238+
else if(directionLR == -1){
239+
changeLedCurrent(-1);
240+
}
241+
else if (directionUD ==1){
242+
changeSlot0Current(5);
243+
}
244+
else if (directionUD == -1) {
245+
changeSlot0Current(-5);
246+
}
247+
});
248+
249+
console.log("Registering raw hrm data callback");
250+
Bangle.on('HRM-raw', function (hrm) {
251+
value = hrm.raw;
252+
filt = hrm.filt;
253+
let alg = Math.round(analogRead(29)* 16383);
254+
rawVals.push(alg); // FIXME - pushing analogue value for testing
255+
//algVals.push(alg)
256+
if (rawVals.length > rawBufSize) {
257+
rawVals.shift();
258+
//algVals.shift();
259+
}
260+
//var dataArray = [value,filt,HRVal,HRConfidence];
261+
file.write(getTime() + "," + value + "," + filt
262+
+ "," + HRVal + "," + HRConfidence + "\n");
263+
});
264+
265+
console.log("Registering hrm values callback");
266+
Bangle.on('HRM', function (hrmB) {
267+
HRVal = hrmB.bpm;
268+
HRConfidence = hrmB.confidence;
269+
});
270+
271+
drawText();
272+
273+
274+

apps/hrmtest/interface.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<html>
2+
<head>
3+
<link rel="stylesheet" href="../../css/spectre.min.css">
4+
</head>
5+
<body>
6+
<div id="data"></div>
7+
<button class="btn btn-default" id="btnSave">Save</button>
8+
<button class="btn btn-default" id="btnDelete">Delete</button>
9+
10+
<script src="../../core/lib/interface.js"></script>
11+
<script>
12+
var dataElement = document.getElementById("data");
13+
var csvData = "";
14+
15+
function getData() {
16+
// show loading window
17+
Util.showModal("Loading...");
18+
// get the data
19+
dataElement.innerHTML = "";
20+
Util.readStorageFile(`hrm_log.csv`,data=>{
21+
csvData = data.trim();
22+
// remove window
23+
Util.hideModal();
24+
// If no data, report it and exit
25+
if (data.length==0) {
26+
dataElement.innerHTML = "<b>No data found</b>";
27+
return;
28+
}
29+
else{
30+
dataElement.innerHTML = "<b>data file found</b>";
31+
return;
32+
}
33+
});
34+
}
35+
36+
// You can call a utility function to save the data
37+
document.getElementById("btnSave").addEventListener("click", function() {
38+
Util.saveCSV("HRM_data", csvData);
39+
});
40+
// Or you can also delete the file
41+
document.getElementById("btnDelete").addEventListener("click", function() {
42+
Util.showModal("Deleting...");
43+
Util.eraseStorageFile("hrm_log.csv", function() {
44+
Util.hideModal();
45+
getData();
46+
});
47+
});
48+
// Called when app starts
49+
function onInit() {
50+
getData();
51+
}
52+
</script>
53+
</body>
54+
</html>

apps/hrmtest/metadata.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"id": "hrmtest",
3+
"name": "HRM Sensor Test",
4+
"shortName": "HRM_Test",
5+
"version": "0.06",
6+
"description": "Test various HRM configuration options",
7+
"icon": "app-icon.png",
8+
"tags": "",
9+
"readme": "README.md",
10+
"interface": "interface.html",
11+
"supports": ["BANGLEJS","BANGLEJS2"],
12+
"storage": [
13+
{"name":"hrmtest.app.js","url":"app.js"},
14+
{"name":"hrmtest.img","url":"app-icon.js","evaluate":true}
15+
]
16+
}
308 KB
Loading

testing/hrm/Pinetime_HRM.jpg

140 KB
Loading

0 commit comments

Comments
 (0)