diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md index 42ad619bd..8d5872670 100644 --- a/apps/bthrm/README.md +++ b/apps/bthrm/README.md @@ -2,7 +2,7 @@ When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one. -HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor. +HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM')` event as if it came from the on board monitor. This means it's compatible with many Bangle.js apps including: @@ -16,19 +16,23 @@ as that requires live sensor data (rather than just BPM readings). Just install the app, then install an app that uses the heart rate monitor. -Once installed it'll automatically try and connect to the first bluetooth -heart rate monitor it finds. +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. **To disable this and return to normal HRM, uninstall the app** ## Compatible Heart Rate Monitors This works with any heart rate monitor providing the standard Bluetooth -Heart Rate Service (`180D`) and characteristic (`2A37`). +Heart Rate Service (`180D`) and characteristic (`2A37`). It additionally supports +the location (`2A38`) characteristic and the Battery Service (`180F`), reporting +that information in the `BTHRM` event when they are available. So far it has been tested on: * CooSpo Bluetooth Heart Rate Monitor +* Polar H10 +* Polar OH1 * Wahoo TICKR X 2 ## Internals @@ -38,7 +42,6 @@ This replaces `Bangle.setHRMPower` with its own implementation. ## TODO * A widget to show connection state? -* Specify a specific device by address? ## Creator diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index f9de2ee5d..064f65eac 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -18,33 +18,33 @@ if (settings.enabled){ - function clearCache(){ + var clearCache = function() { return require('Storage').erase("bthrm.cache.json"); - } + }; - function getCache(){ + var getCache = function() { var cache = require('Storage').readJSON("bthrm.cache.json", true) || {}; if (settings.btid && settings.btid === cache.id) return cache; clearCache(); return {}; - } + }; - function addNotificationHandler(characteristic){ + var addNotificationHandler = function(characteristic) { log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler); characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); - } + }; - function writeCache(cache){ + var writeCache = function(cache) { var oldCache = getCache(); if (oldCache !== cache) { log("Writing cache"); - require('Storage').writeJSON("bthrm.cache.json", cache) + require('Storage').writeJSON("bthrm.cache.json", cache); } else { log("No changes, don't write cache"); } - } + }; - function characteristicsToCache(characteristics){ + var characteristicsToCache = function(characteristics) { log("Cache characteristics"); var cache = getCache(); if (!cache.characteristics) cache.characteristics = {}; @@ -59,9 +59,9 @@ }; } writeCache(cache); - } + }; - function characteristicsFromCache(){ + var characteristicsFromCache = function() { log("Read cached characteristics"); var cache = getCache(); if (!cache.characteristics) return []; @@ -80,19 +80,16 @@ restored.push(r); } return restored; - } + }; log("Start"); var lastReceivedData={ }; - var serviceFilters = [{ - services: [ "180d" ] - }]; - var supportedServices = [ - "0x180d", "0x180f" + "0x180d", // Heart Rate + "0x180f", // Battery ]; var supportedCharacteristics = { @@ -100,17 +97,17 @@ //Heart rate measurement handler: function (dv){ var flags = dv.getUint8(0); - + var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit - + var sensorContact; - + if (flags & 2){ sensorContact = !!(flags & 4); } - + var idx = 2 + (flags&1); - + var energyExpended; if (flags & 8){ energyExpended = dv.getUint16(idx,1); @@ -123,7 +120,7 @@ log("Found " + (maxIntervalBytes / 2) + " rr data fields"); for(var i = 0 ; i < maxIntervalBytes / 2; i++){ interval[i] = dv.getUint16(idx,1); // in milliseconds - idx += 2 + idx += 2; } } @@ -143,7 +140,7 @@ confidence: (sensorContact || sensorContact === undefined)? 100 : 0, src: "bthrm" }; - + log("Emitting HRM: ", repEvent); Bangle.emit("HRM", repEvent); } @@ -151,7 +148,7 @@ var newEvent = { bpm: bpm }; - + if (location) newEvent.location = location; if (interval) newEvent.rr = interval; if (energyExpended) newEvent.energy = energyExpended; @@ -183,7 +180,7 @@ var characteristics = []; var blockInit = false; var currentRetryTimeout; - var initialRetryTime = 1000; + var initialRetryTime = 40; var maxRetryTime = 60000; var retryTime = initialRetryTime; @@ -192,7 +189,7 @@ maxInterval: 1500 }; - function waitingPromise(timeout) { + var waitingPromise = function(timeout) { return new Promise(function(resolve){ log("Start waiting for " + timeout); setTimeout(()=>{ @@ -200,7 +197,7 @@ resolve(); }, timeout); }); - } + }; if (settings.enabled){ Bangle.isBTHRMOn = function(){ @@ -225,15 +222,15 @@ }; } - function clearRetryTimeout(){ + var clearRetryTimeout = function() { if (currentRetryTimeout){ log("Clearing timeout " + currentRetryTimeout); clearTimeout(currentRetryTimeout); currentRetryTimeout = undefined; } - } + }; - function retry(){ + var retry = function() { log("Retry"); if (!currentRetryTimeout){ @@ -255,10 +252,10 @@ } else { log("Already in retry..."); } - } + }; var buzzing = false; - function onDisconnect(reason) { + var onDisconnect = function(reason) { log("Disconnect: " + reason); log("GATT: ", gatt); log("Characteristics: ", characteristics); @@ -273,9 +270,9 @@ if (Bangle.isBTHRMOn()){ retry(); } - } + }; - function createCharacteristicPromise(newCharacteristic){ + var createCharacteristicPromise = function(newCharacteristic) { log("Create characteristic promise: ", newCharacteristic); var result = Promise.resolve(); // For values that can be read, go ahead and read them, even if we might be notified in the future @@ -298,23 +295,23 @@ log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); startPromise = startPromise.then(()=>{ log("Wait after connect"); - waitingPromise(settings.gracePeriodNotification) + return waitingPromise(settings.gracePeriodNotification); }); } return startPromise; }); } return result.then(()=>log("Handled characteristic: ", newCharacteristic)); - } + }; - function attachCharacteristicPromise(promise, characteristic){ + var attachCharacteristicPromise = function(promise, characteristic) { return promise.then(()=>{ log("Handling characteristic:", characteristic); return createCharacteristicPromise(characteristic); }); - } + }; - function createCharacteristicsPromise(newCharacteristics){ + var createCharacteristicsPromise = function(newCharacteristics) { log("Create characteristics promise: ", newCharacteristics); var result = Promise.resolve(); for (var c of newCharacteristics){ @@ -328,9 +325,9 @@ result = attachCharacteristicPromise(result, c); } return result.then(()=>log("Handled characteristics")); - } + }; - function createServicePromise(service){ + var createServicePromise = function(service) { log("Create service promise: ", service); var result = Promise.resolve(); result = result.then(()=>{ @@ -338,15 +335,13 @@ return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); }); return result.then(()=>log("Handled service" + service.uuid)); - } + }; - function attachServicePromise(promise, service){ + var attachServicePromise = function(promise, service) { return promise.then(()=>createServicePromise(service)); - } + }; - var reUseCounter = 0; - - function initBt() { + var initBt = function () { log("initBt with blockInit: " + blockInit); if (blockInit){ retry(); @@ -355,19 +350,15 @@ blockInit = true; - if (reUseCounter > 10){ - log("Reuse counter to high"); - gatt=undefined; - reUseCounter = 0; - } - var promise; + var filters; if (!device){ - var filters = serviceFilters; if (settings.btid){ log("Configured device id", settings.btid); - filters = [{id: settings.btid }]; + filters = [{ id: settings.btid }]; + } else { + return; } log("Requesting device with filters", filters); promise = NRF.requestDevice({ filters: filters, active: true }); @@ -397,13 +388,13 @@ } else { log("GATT is new: ", gatt); characteristics = []; - var cachedName = getCache().name; - if (device.name !== cachedName){ - log("Device name changed from " + cachedName + " to " + device.name + ", clearing cache"); + var cachedId = getCache().id; + if (device.id !== cachedId){ + log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache"); clearCache(); } var newCache = getCache(); - newCache.name = device.name; + newCache.id = device.id; writeCache(newCache); gatt = device.gatt; } @@ -465,7 +456,7 @@ log("Add " + settings.gracePeriodService + "ms grace period after services"); result = result.then(()=>{ log("Wait after services"); - return waitingPromise(settings.gracePeriodService) + return waitingPromise(settings.gracePeriodService); }); } return result; @@ -478,10 +469,9 @@ return characteristicsPromise; }); - + return promise.then(()=>{ log("Connection established, waiting for notifications"); - reUseCounter = 0; characteristicsToCache(characteristics); clearRetryTimeout(); }).catch((e) => { @@ -489,7 +479,7 @@ log("Error:", e); onDisconnect(e); }); - } + }; Bangle.setBTHRMPower = function(isOn, app) { // Do app power handling @@ -538,7 +528,7 @@ var fallbackInterval; - function switchInternalHrm(){ + var switchInternalHrm = function() { if (settings.allowFallback && !fallbackInterval){ log("Fallback to HRM enabled"); origSetHRMPower(1, "bthrm_fallback"); @@ -551,7 +541,7 @@ } }, settings.fallbackTimeout); } - } + }; if (settings.replace){ log("Replace HRM event"); diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index b35ebd6af..faff72d0f 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -6,7 +6,7 @@ "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "type": "app", - "tags": "health,bluetooth", + "tags": "health,bluetooth,hrm", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [