Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BLE Heart Rate Monitor improvements #1229

Merged
merged 10 commits into from
Jan 7, 2022
11 changes: 7 additions & 4 deletions apps.json
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@
"id": "recorder",
"name": "Recorder (BETA)",
"shortName": "Recorder",
"version": "0.05",
"version": "0.06",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",
Expand Down Expand Up @@ -1040,16 +1040,19 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.01",
"version": "0.02",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "boot",
"type": "app",
"tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true}
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"}
]
},
{
Expand Down
3 changes: 3 additions & 0 deletions apps/bthrm/ChangeLog
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
0.01: New App!
0.02: Make overriding the HRM event optional
Emit BTHRM event for external sensor
Add recorder app plugin
65 changes: 50 additions & 15 deletions apps/bthrm/boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,43 @@
var log = function() {};//print
var gatt;
var status;

Bangle.isHRMOn = function() {

var origIsHRMOn = Bangle.isHRMOn;

Bangle.isBTHRMOn = function(){
return (status=="searching" || status=="connecting") || (gatt!==undefined);
}
Bangle.setHRMPower = function(isOn, app) {

Bangle.isHRMOn = function() {
var settings = require('Storage').readJSON("bthrm.json", true) || {};

print(settings);
if (settings.enabled && !settings.replace){
return origIsHRMOn();
} else if (settings.enabled && settings.replace){
return Bangle.isBTHRMOn();
}
return origIsHRMOn() || Bangle.isBTHRMOn();
}

Bangle.setBTHRMPower = function(isOn, app) {


var settings = require('Storage').readJSON("bthrm.json", true) || {};

// Do app power handling
if (!app) app="?";
log("setHRMPower ->", isOn, app);
log("setBTHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[];
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app);
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app);
isOn = Bangle._PWR.HRM.length;
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
log("setHRMPower on", app);
if (!Bangle.isHRMOn()) {
log("HRM not already on");
log("setBTHRMPower on", app);
if (!Bangle.isBTHRMOn()) {
log("BTHRM not already on");
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
Expand Down Expand Up @@ -49,7 +68,11 @@
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit('HRM',{


var eventName = settings.replace ? "HRM" : "BTHRM";

Bangle.emit(eventName, {
bpm:bpm,
confidence:100
});
Expand All @@ -65,15 +88,27 @@
});
}
} else { // not on
log("setHRMPower off", app);
log("setBTHRMPower off", app);
if (gatt) {
log("HRM connected - disconnecting");
log("BTHRM connected - disconnecting");
status = undefined;
try {gatt.disconnect();}catch(e) {
log("HRM disconnect error", e);
log("BTHRM disconnect error", e);
}
gatt = undefined;
}
}
};

var origSetHRMPower = Bangle.setHRMPower;

Bangle.setHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled || !isOn){
Bangle.setBTHRMPower(isOn, app);
}
if (settings.enabled && !settings.replace || !isOn){
origSetHRMPower(isOn, app);
}
}
})();
61 changes: 61 additions & 0 deletions apps/bthrm/bthrm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
var btm = g.getHeight()-1;
var eventInt = null;
var eventBt = null;
var counterInt = 0;
var counterBt = 0;


function draw(y, event, type, counter) {
var px = g.getWidth()/2;
g.reset();
g.setFontAlign(0,0);
g.clearRect(0,y,g.getWidth(),y+80);
if (type == null || event == null || counter == 0) return;
var str = event.bpm + "";
g.setFontVector(40).drawString(str,px,y+20);
str = "Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+50);
str = "Event: " + type;
g.setFontVector(12).drawString(str,px,y+60);
}

function onBtHrm(e) {
print("Event for BT " + JSON.stringify(e));
counterBt += 5;
eventBt = e;
}

function onHrm(e) {
print("Event for Int " + JSON.stringify(e));
counterInt += 5;
eventInt = e;
}

Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm);

Bangle.setHRMPower(1,'bthrm')
Bangle.setBTHRMPower(1,'bthrm')

g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();

g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);

function drawInt(){
counterInt--;
if (counterInt < 0) counterInt = 0;
if (counterInt > 5) counterInt = 5;
draw(24, eventInt, "HRM", counterInt);
}
function drawBt(){
counterBt--;
if (counterBt < 0) counterBt = 0;
if (counterBt > 5) counterBt = 5;
draw(100, eventBt, "BTHRM", counterBt);
}

var interval = setInterval(drawInt, 1000);
var interval = setInterval(drawBt, 1000);
27 changes: 27 additions & 0 deletions apps/bthrm/recorder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(function(recorders) {
recorders.bthrm = function() {
var bpm = 0;
function onHRM(h) {
bpm = h.bpm;
}
return {
name : "BTHR",
fields : ["BT Heartrate"],
getValues : () => {
result = [bpm];
bpm = 0;
return result;
},
start : () => {
Bangle.on('BTHRM', onHRM);
Bangle.setBTHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('BTHRM', onHRM);
Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
}
})

33 changes: 33 additions & 0 deletions apps/bthrm/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
(function(back) {
var FILE = "bthrm.json";

var settings = Object.assign({
enabled: true,
replace: true,
}, require('Storage').readJSON(FILE, true) || {});

function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}

E.showMenu({
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
onchange: v => {
settings.enabled = v;
writeSettings();
}
},
'Use HRM event': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
onchange: v => {
settings.replace = v;
writeSettings();
}
}
});
})
4 changes: 4 additions & 0 deletions apps/recorder/ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@
0.03: Fix theme and maps/graphing if no GPS
0.04: Multiple bugfixes
0.05: Add recording for coresensor
0.06: Add recording for battery stats
Fix execution of other recorders (*.recorder.js)
Modified icons and colors for better visibility
Only show plotting speed if Latitude is available
5 changes: 3 additions & 2 deletions apps/recorder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ You can record
* **Time** The current time
* **GPS** GPS Latitude, Longitude and Altitude
* **Steps** Steps counted by the step counter
* **HR** Heart rate
* **HR** Heart rate and confidence
* **BAT** Battery percentage and voltage
* **Core** CoreTemp body temperature

**Note:** It is possible for other apps to record information using this app
Expand All @@ -25,4 +26,4 @@ function in `widget.js` for more information.

## Tips

When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a red satellite symbol, which you will see turn green when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
7 changes: 4 additions & 3 deletions apps/recorder/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,10 @@ function viewTrack(filename, info) {
menu['Plot Alt.'] = function() {
plotGraph(info, "Altitude");
};
menu['Plot Speed'] = function() {
plotGraph(info, "Speed");
};
if (info.fields.includes("Latitude"))
menu['Plot Speed'] = function() {
plotGraph(info, "Speed");
};
// TODO: steps, heart rate?
menu['Erase'] = function() {
E.showPrompt("Delete Track?").then(function(v) {
Expand Down
33 changes: 21 additions & 12 deletions apps/recorder/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,41 +48,50 @@
Bangle.removeListener('GPS', onGPS);
Bangle.setGPSPower(0,"recorder");
},
draw : (x,y) => g.setColor(hasFix?"#0ff":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y)
draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y)
};
},
hrm:function() {
var bpm = 0, bpmConfidence = 0;
var hasBPM = false;
function onHRM(h) {
if (h.confidence >= bpmConfidence) {
bpmConfidence = h.confidence;
bpm = h.bpm;
if (bpmConfidence) hasBPM = true;
}
}
return {
name : "HR",
fields : ["Heartrate"],
fields : ["Heartrate", "Confidence"],
getValues : () => {
var r = [bpmConfidence?bpm:""];
var r = [bpm,bpmConfidence];
bpm = 0; bpmConfidence = 0;
return r;
},
start : () => {
hasBPM = false;
Bangle.on('HRM', onHRM);
Bangle.setHRMPower(1,"recorder");
},
stop : () => {
hasBPM = false;
Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y)
draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
},
bat:function() {
return {
name : "BAT",
fields : ["Battery Percentage", "Battery Voltage", "Charging"],
getValues : () => {
return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()];
},
start : () => {
},
stop : () => {
},
draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y)
};
},

temp:function() {
var core = 0, skin = 0;
var hasCore = false;
Expand All @@ -106,7 +115,7 @@
hasCore = false;
Bangle.removeListener('CoreTemp', onCore);
},
draw : (x,y) => g.setColor(hasCore?"#0f0":"#888").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAwBAAAOAKPOfgZgZgZgZgfgPAAA"),x,y)
};
},
steps:function() {
Expand All @@ -121,7 +130,7 @@
},
start : () => { lastSteps = Bangle.getStepCount(); },
stop : () => {},
draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y)
draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y)
};
}
// TODO: recAltitude from pressure sensor
Expand All @@ -138,7 +147,7 @@
}
})
*/
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders));
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders));
return recorders;
}

Expand Down