forked from FOSS/BangleApps
commit
6c0ad7a394
|
@ -25,3 +25,7 @@
|
||||||
0.10: Use default Bangle formatter for booleans
|
0.10: Use default Bangle formatter for booleans
|
||||||
0.11: App now shows status info while connecting
|
0.11: App now shows status info while connecting
|
||||||
Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central)
|
Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central)
|
||||||
|
0.12: Fix HRM fallback handling
|
||||||
|
Use default boolean formatter in custom menu and directly apply config if useful
|
||||||
|
Allow recording unmodified internal HR
|
||||||
|
Better connection retry handling
|
||||||
|
|
|
@ -19,7 +19,14 @@ Just install the app, then install an app that uses the heart rate monitor.
|
||||||
Once installed you will have to go into this app's settings while your heart rate monitor
|
Once installed you will have to go into this app's settings while your heart rate monitor
|
||||||
is available for bluetooth pairing and scan for devices.
|
is available for bluetooth pairing and scan for devices.
|
||||||
|
|
||||||
**To disable this and return to normal HRM, uninstall the app**
|
**To disable this and return to normal HRM, uninstall the app or change the settings**
|
||||||
|
|
||||||
|
### Modes
|
||||||
|
|
||||||
|
* Off - Internal HRM is used, no attempt on connecting to BT HRM.
|
||||||
|
* Default - Replaces internal HRM with BT HRM and falls back to internal HRM if no valid measurements received.
|
||||||
|
* Both - The BT HRM needs to be started explicitly by an app that wants to use it. BT HRM has its own event and is completely separated from the internal HRM. Apps not supporting the BT HRM will not see the BT HRM measurements.
|
||||||
|
* Custom - Combine low level settings as you see fit.
|
||||||
|
|
||||||
## Compatible Heart Rate Monitors
|
## Compatible Heart Rate Monitors
|
||||||
|
|
||||||
|
@ -35,6 +42,10 @@ So far it has been tested on:
|
||||||
* Polar OH1
|
* Polar OH1
|
||||||
* Wahoo TICKR X 2
|
* Wahoo TICKR X 2
|
||||||
|
|
||||||
|
## Recorder plugin
|
||||||
|
|
||||||
|
The recorder plugin can record the BT HRM event (blue) and the original unchanged HRM event (green). This is mainly useful for debugging purposes or comparing the BT with the internal HRM, as the resulting "merged" HRM can be recordet using the default HRM recorder.
|
||||||
|
|
||||||
## Internals
|
## Internals
|
||||||
|
|
||||||
This replaces `Bangle.setHRMPower` with its own implementation.
|
This replaces `Bangle.setHRMPower` with its own implementation.
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
var log = function(text, param){
|
var log = function(text, param){
|
||||||
if (global.showStatusInfo)
|
if (global.showStatusInfo)
|
||||||
showStatusInfo(text)
|
showStatusInfo(text);
|
||||||
if (settings.debuglog){
|
if (settings.debuglog){
|
||||||
var logline = new Date().toISOString() + " - " + text;
|
var logline = new Date().toISOString() + " - " + text;
|
||||||
if (param) logline += ": " + JSON.stringify(param);
|
if (param) logline += ": " + JSON.stringify(param);
|
||||||
|
@ -94,13 +94,24 @@
|
||||||
"0x180f", // Battery
|
"0x180f", // Battery
|
||||||
];
|
];
|
||||||
|
|
||||||
|
var bpmTimeout;
|
||||||
|
|
||||||
var supportedCharacteristics = {
|
var supportedCharacteristics = {
|
||||||
"0x2a37": {
|
"0x2a37": {
|
||||||
//Heart rate measurement
|
//Heart rate measurement
|
||||||
|
active: false,
|
||||||
handler: function (dv){
|
handler: function (dv){
|
||||||
var flags = dv.getUint8(0);
|
var flags = dv.getUint8(0);
|
||||||
|
|
||||||
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
|
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
|
||||||
|
supportedCharacteristics["0x2a37"].active = bpm > 0;
|
||||||
|
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
|
||||||
|
if (supportedCharacteristics["0x2a37"].active) stopFallback();
|
||||||
|
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||||
|
bpmTimeout = setTimeout(()=>{
|
||||||
|
supportedCharacteristics["0x2a37"].active = false;
|
||||||
|
startFallback();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
var sensorContact;
|
var sensorContact;
|
||||||
|
|
||||||
|
@ -144,7 +155,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
log("Emitting HRM", repEvent);
|
log("Emitting HRM", repEvent);
|
||||||
Bangle.emit("HRM", repEvent);
|
Bangle.emit("HRM_int", repEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
var newEvent = {
|
var newEvent = {
|
||||||
|
@ -202,6 +213,10 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
if (settings.enabled){
|
if (settings.enabled){
|
||||||
|
Bangle.isBTHRMActive = function (){
|
||||||
|
return supportedCharacteristics["0x2a37"].active;
|
||||||
|
};
|
||||||
|
|
||||||
Bangle.isBTHRMOn = function(){
|
Bangle.isBTHRMOn = function(){
|
||||||
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
||||||
};
|
};
|
||||||
|
@ -212,24 +227,28 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.replace){
|
if (settings.replace){
|
||||||
var origIsHRMOn = Bangle.isHRMOn;
|
Bangle.origIsHRMOn = Bangle.isHRMOn;
|
||||||
|
|
||||||
Bangle.isHRMOn = function() {
|
Bangle.isHRMOn = function() {
|
||||||
if (settings.enabled && !settings.replace){
|
if (settings.enabled && !settings.replace){
|
||||||
return origIsHRMOn();
|
return Bangle.origIsHRMOn();
|
||||||
} else if (settings.enabled && settings.replace){
|
} else if (settings.enabled && settings.replace){
|
||||||
return Bangle.isBTHRMOn();
|
return Bangle.isBTHRMOn();
|
||||||
}
|
}
|
||||||
return origIsHRMOn() || Bangle.isBTHRMOn();
|
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var clearRetryTimeout = function() {
|
var clearRetryTimeout = function(resetTime) {
|
||||||
if (currentRetryTimeout){
|
if (currentRetryTimeout){
|
||||||
log("Clearing timeout " + currentRetryTimeout);
|
log("Clearing timeout " + currentRetryTimeout);
|
||||||
clearTimeout(currentRetryTimeout);
|
clearTimeout(currentRetryTimeout);
|
||||||
currentRetryTimeout = undefined;
|
currentRetryTimeout = undefined;
|
||||||
}
|
}
|
||||||
|
if (resetTime) {
|
||||||
|
log("Resetting retry time");
|
||||||
|
retryTime = initialRetryTime;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var retry = function() {
|
var retry = function() {
|
||||||
|
@ -261,9 +280,9 @@
|
||||||
log("Disconnect: " + reason);
|
log("Disconnect: " + reason);
|
||||||
log("GATT", gatt);
|
log("GATT", gatt);
|
||||||
log("Characteristics", characteristics);
|
log("Characteristics", characteristics);
|
||||||
retryTime = initialRetryTime;
|
clearRetryTimeout(reason != "Connection Timeout");
|
||||||
clearRetryTimeout();
|
supportedCharacteristics["0x2a37"].active = false;
|
||||||
switchInternalHrm();
|
startFallback();
|
||||||
blockInit = false;
|
blockInit = false;
|
||||||
if (settings.warnDisconnect && !buzzing){
|
if (settings.warnDisconnect && !buzzing){
|
||||||
buzzing = true;
|
buzzing = true;
|
||||||
|
@ -478,7 +497,7 @@
|
||||||
return promise.then(()=>{
|
return promise.then(()=>{
|
||||||
log("Connection established, waiting for notifications");
|
log("Connection established, waiting for notifications");
|
||||||
characteristicsToCache(characteristics);
|
characteristicsToCache(characteristics);
|
||||||
clearRetryTimeout();
|
clearRetryTimeout(true);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
characteristics = [];
|
characteristics = [];
|
||||||
log("Error:", e);
|
log("Error:", e);
|
||||||
|
@ -496,9 +515,11 @@
|
||||||
isOn = Bangle._PWR.BTHRM.length;
|
isOn = Bangle._PWR.BTHRM.length;
|
||||||
// so now we know if we're really on
|
// so now we know if we're really on
|
||||||
if (isOn) {
|
if (isOn) {
|
||||||
|
switchFallback();
|
||||||
if (!Bangle.isBTHRMConnected()) initBt();
|
if (!Bangle.isBTHRMConnected()) initBt();
|
||||||
} else { // not on
|
} else { // not on
|
||||||
log("Power off for " + app);
|
log("Power off for " + app);
|
||||||
|
clearRetryTimeout(true);
|
||||||
if (gatt) {
|
if (gatt) {
|
||||||
if (gatt.connected){
|
if (gatt.connected){
|
||||||
log("Disconnect with gatt", gatt);
|
log("Disconnect with gatt", gatt);
|
||||||
|
@ -516,7 +537,33 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var origSetHRMPower = Bangle.setHRMPower;
|
if (settings.replace){
|
||||||
|
Bangle.on("HRM", (e) => {
|
||||||
|
e.modified = true;
|
||||||
|
Bangle.emit("HRM_int", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.origOn = Bangle.on;
|
||||||
|
Bangle.on = function(name, callback) {
|
||||||
|
if (name == "HRM") {
|
||||||
|
Bangle.origOn("HRM_int", callback);
|
||||||
|
} else {
|
||||||
|
Bangle.origOn(name, callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.origRemoveListener = Bangle.removeListener;
|
||||||
|
Bangle.removeListener = function(name, callback) {
|
||||||
|
if (name == "HRM") {
|
||||||
|
Bangle.origRemoveListener("HRM_int", callback);
|
||||||
|
} else {
|
||||||
|
Bangle.origRemoveListener(name, callback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.origSetHRMPower = Bangle.setHRMPower;
|
||||||
|
|
||||||
if (settings.startWithHrm){
|
if (settings.startWithHrm){
|
||||||
|
|
||||||
|
@ -526,40 +573,54 @@
|
||||||
Bangle.setBTHRMPower(isOn, app);
|
Bangle.setBTHRMPower(isOn, app);
|
||||||
}
|
}
|
||||||
if ((settings.enabled && !settings.replace) || !settings.enabled){
|
if ((settings.enabled && !settings.replace) || !settings.enabled){
|
||||||
origSetHRMPower(isOn, app);
|
Bangle.origSetHRMPower(isOn, app);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var fallbackInterval;
|
var fallbackActive = false;
|
||||||
|
var inSwitch = false;
|
||||||
|
|
||||||
var switchInternalHrm = function() {
|
var stopFallback = function(){
|
||||||
if (settings.allowFallback && !fallbackInterval){
|
if (fallbackActive){
|
||||||
log("Fallback to HRM enabled");
|
Bangle.origSetHRMPower(0, "bthrm_fallback");
|
||||||
origSetHRMPower(1, "bthrm_fallback");
|
fallbackActive = false;
|
||||||
fallbackInterval = setInterval(()=>{
|
|
||||||
if (Bangle.isBTHRMConnected()){
|
|
||||||
origSetHRMPower(0, "bthrm_fallback");
|
|
||||||
clearInterval(fallbackInterval);
|
|
||||||
fallbackInterval = undefined;
|
|
||||||
log("Fallback to HRM disabled");
|
log("Fallback to HRM disabled");
|
||||||
}
|
}
|
||||||
}, settings.fallbackTimeout);
|
};
|
||||||
|
|
||||||
|
var startFallback = function(){
|
||||||
|
if (!fallbackActive && settings.allowFallback) {
|
||||||
|
fallbackActive = true;
|
||||||
|
Bangle.origSetHRMPower(1, "bthrm_fallback");
|
||||||
|
log("Fallback to HRM enabled");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var switchFallback = function() {
|
||||||
|
log("Check falling back to HRM");
|
||||||
|
if (!inSwitch){
|
||||||
|
inSwitch = true;
|
||||||
|
if (Bangle.isBTHRMActive()){
|
||||||
|
stopFallback();
|
||||||
|
} else {
|
||||||
|
startFallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inSwitch = false;
|
||||||
|
};
|
||||||
|
|
||||||
if (settings.replace){
|
if (settings.replace){
|
||||||
log("Replace HRM event");
|
log("Replace HRM event");
|
||||||
if (Bangle._PWR && Bangle._PWR.HRM){
|
if (Bangle._PWR && Bangle._PWR.HRM){
|
||||||
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
|
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
|
||||||
var app = Bangle._PWR.HRM[i];
|
var app = Bangle._PWR.HRM[i];
|
||||||
log("Moving app " + app);
|
log("Moving app " + app);
|
||||||
origSetHRMPower(0, app);
|
Bangle.origSetHRMPower(0, app);
|
||||||
Bangle.setBTHRMPower(1, app);
|
Bangle.setBTHRMPower(1, app);
|
||||||
if (Bangle._PWR.HRM===undefined) break;
|
if (Bangle._PWR.HRM===undefined) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switchInternalHrm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
E.on("kill", ()=>{
|
E.on("kill", ()=>{
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "bthrm",
|
"id": "bthrm",
|
||||||
"name": "Bluetooth Heart Rate Monitor",
|
"name": "Bluetooth Heart Rate Monitor",
|
||||||
"shortName": "BT HRM",
|
"shortName": "BT HRM",
|
||||||
"version": "0.11",
|
"version": "0.12",
|
||||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
|
|
@ -32,8 +32,45 @@
|
||||||
Bangle.removeListener('BTHRM', onHRM);
|
Bangle.removeListener('BTHRM', onHRM);
|
||||||
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
|
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
|
||||||
},
|
},
|
||||||
draw : (x,y) => g.setColor((bpm != "")?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
|
draw : (x,y) => g.setColor((Bangle.isBTHRMActive && Bangle.isBTHRMActive())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
recorders.hrmint = function() {
|
||||||
|
var active = false;
|
||||||
|
var bpmTimeout;
|
||||||
|
var bpm = "", bpmConfidence = "", src="";
|
||||||
|
function onHRM(h) {
|
||||||
|
bpmConfidence = h.confidence;
|
||||||
|
bpm = h.bpm;
|
||||||
|
srv = h.src;
|
||||||
|
if (h.bpm > 0){
|
||||||
|
active = true;
|
||||||
|
print("active" + h.bpm);
|
||||||
|
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||||
|
bpmTimeout = setTimeout(()=>{
|
||||||
|
print("inactive");
|
||||||
|
active = false;
|
||||||
|
},3000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name : "HR int",
|
||||||
|
fields : ["Heartrate", "Confidence"],
|
||||||
|
getValues : () => {
|
||||||
|
var r = [bpm,bpmConfidence,src];
|
||||||
|
bpm = ""; bpmConfidence = ""; src="";
|
||||||
|
return r;
|
||||||
|
},
|
||||||
|
start : () => {
|
||||||
|
Bangle.origOn('HRM', onHRM);
|
||||||
|
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(1,"recorder");
|
||||||
|
},
|
||||||
|
stop : () => {
|
||||||
|
Bangle.removeListener('HRM', onHRM);
|
||||||
|
if (Bangle.origSetHRMPower) Bangle.origSetHRMPower(0,"recorder");
|
||||||
|
},
|
||||||
|
draw : (x,y) => g.setColor(( Bangle.origIsHRMOn && Bangle.origIsHRMOn() && active)?"#0f0":"#8f8").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
|
||||||
|
};
|
||||||
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,14 @@
|
||||||
var settings;
|
var settings;
|
||||||
readSettings();
|
readSettings();
|
||||||
|
|
||||||
|
function applyCustomSettings(){
|
||||||
|
writeSettings("enabled",true);
|
||||||
|
writeSettings("replace",settings.custom_replace);
|
||||||
|
writeSettings("startWithHrm",settings.custom_startWithHrm);
|
||||||
|
writeSettings("allowFallback",settings.custom_allowFallback);
|
||||||
|
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
function buildMainMenu(){
|
function buildMainMenu(){
|
||||||
var mainmenu = {
|
var mainmenu = {
|
||||||
'': { 'title': 'Bluetooth HRM' },
|
'': { 'title': 'Bluetooth HRM' },
|
||||||
|
@ -35,7 +43,6 @@
|
||||||
case 1:
|
case 1:
|
||||||
writeSettings("enabled",true);
|
writeSettings("enabled",true);
|
||||||
writeSettings("replace",true);
|
writeSettings("replace",true);
|
||||||
writeSettings("debuglog",false);
|
|
||||||
writeSettings("startWithHrm",true);
|
writeSettings("startWithHrm",true);
|
||||||
writeSettings("allowFallback",true);
|
writeSettings("allowFallback",true);
|
||||||
writeSettings("fallbackTimeout",10);
|
writeSettings("fallbackTimeout",10);
|
||||||
|
@ -43,17 +50,11 @@
|
||||||
case 2:
|
case 2:
|
||||||
writeSettings("enabled",true);
|
writeSettings("enabled",true);
|
||||||
writeSettings("replace",false);
|
writeSettings("replace",false);
|
||||||
writeSettings("debuglog",false);
|
|
||||||
writeSettings("startWithHrm",false);
|
writeSettings("startWithHrm",false);
|
||||||
writeSettings("allowFallback",false);
|
writeSettings("allowFallback",false);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
writeSettings("enabled",true);
|
applyCustomSettings();
|
||||||
writeSettings("replace",settings.custom_replace);
|
|
||||||
writeSettings("debuglog",settings.custom_debuglog);
|
|
||||||
writeSettings("startWithHrm",settings.custom_startWithHrm);
|
|
||||||
writeSettings("allowFallback",settings.custom_allowFallback);
|
|
||||||
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
writeSettings("mode",v);
|
writeSettings("mode",v);
|
||||||
|
@ -138,23 +139,23 @@
|
||||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||||
'Replace HRM': {
|
'Replace HRM': {
|
||||||
value: !!settings.custom_replace,
|
value: !!settings.custom_replace,
|
||||||
format: v => settings.custom_replace ? "On" : "Off",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("custom_replace",v);
|
writeSettings("custom_replace",v);
|
||||||
|
if (settings.mode == 3) applyCustomSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Start w. HRM': {
|
'Start w. HRM': {
|
||||||
value: !!settings.custom_startWithHrm,
|
value: !!settings.custom_startWithHrm,
|
||||||
format: v => settings.custom_startWithHrm ? "On" : "Off",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("custom_startWithHrm",v);
|
writeSettings("custom_startWithHrm",v);
|
||||||
|
if (settings.mode == 3) applyCustomSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'HRM Fallback': {
|
'HRM Fallback': {
|
||||||
value: !!settings.custom_allowFallback,
|
value: !!settings.custom_allowFallback,
|
||||||
format: v => settings.custom_allowFallback ? "On" : "Off",
|
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("custom_allowFallback",v);
|
writeSettings("custom_allowFallback",v);
|
||||||
|
if (settings.mode == 3) applyCustomSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Fallback Timeout': {
|
'Fallback Timeout': {
|
||||||
|
@ -165,6 +166,7 @@
|
||||||
format: v=>v+"s",
|
format: v=>v+"s",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
writeSettings("custom_fallbackTimout",v*1000);
|
writeSettings("custom_fallbackTimout",v*1000);
|
||||||
|
if (settings.mode == 3) applyCustomSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue