1
0
Fork 0

Merge pull request #2013 from halemmerich/bthrm

BTHRM - Various improvements
master
Gordon Williams 2022-07-04 14:15:06 +01:00 committed by GitHub
commit 6c0ad7a394
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 42 deletions

View File

@ -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

View File

@ -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.

View File

@ -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", ()=>{

View File

@ -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",

View File

@ -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)
};
};
}) })

View File

@ -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();
} }
}, },
}; };