forked from FOSS/BangleApps
commit
e517262237
|
@ -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
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON("bthrm.json", true) || {}
|
||||
);
|
||||
|
||||
|
||||
var log = function(text, param){
|
||||
if (settings.debuglog){
|
||||
var logline = new Date().toISOString() + " - " + text;
|
||||
|
@ -13,39 +13,38 @@
|
|||
print(logline);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
log("Settings: ", settings);
|
||||
|
||||
|
||||
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.btname && settings.btname == cache.name) return cache;
|
||||
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', supportedCharacteristics[characteristic.uuid].handler);
|
||||
}
|
||||
|
||||
function writeCache(cache){
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
|
||||
var writeCache = function(cache) {
|
||||
var oldCache = getCache();
|
||||
if (oldCache != cache) {
|
||||
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 = {};
|
||||
|
@ -60,9 +59,9 @@
|
|||
};
|
||||
}
|
||||
writeCache(cache);
|
||||
}
|
||||
};
|
||||
|
||||
function characteristicsFromCache(){
|
||||
var characteristicsFromCache = function() {
|
||||
log("Read cached characteristics");
|
||||
var cache = getCache();
|
||||
if (!cache.characteristics) return [];
|
||||
|
@ -81,38 +80,34 @@
|
|||
restored.push(r);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
};
|
||||
|
||||
log("Start");
|
||||
|
||||
var lastReceivedData={
|
||||
};
|
||||
|
||||
var serviceFilters = [{
|
||||
services: [ "180d" ]
|
||||
}];
|
||||
|
||||
supportedServices = [
|
||||
"0x180d", "0x180f"
|
||||
var supportedServices = [
|
||||
"0x180d", // Heart Rate
|
||||
"0x180f", // Battery
|
||||
];
|
||||
|
||||
var supportedCharacteristics = {
|
||||
"0x2a37": {
|
||||
//Heart rate measurement
|
||||
handler: function (event){
|
||||
var dv = event.target.value;
|
||||
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) ? true : false;
|
||||
sensorContact = !!(flags & 4);
|
||||
}
|
||||
|
||||
|
||||
var idx = 2 + (flags&1);
|
||||
|
||||
|
||||
var energyExpended;
|
||||
if (flags & 8){
|
||||
energyExpended = dv.getUint16(idx,1);
|
||||
|
@ -121,11 +116,11 @@
|
|||
var interval;
|
||||
if (flags & 16) {
|
||||
interval = [];
|
||||
maxIntervalBytes = (dv.byteLength - idx);
|
||||
var maxIntervalBytes = (dv.byteLength - idx);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,45 +135,44 @@
|
|||
}
|
||||
|
||||
if (settings.replace){
|
||||
var newEvent = {
|
||||
var repEvent = {
|
||||
bpm: bpm,
|
||||
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
|
||||
src: "bthrm"
|
||||
};
|
||||
|
||||
log("Emitting HRM: ", newEvent);
|
||||
Bangle.emit("HRM", newEvent);
|
||||
|
||||
log("Emitting HRM: ", repEvent);
|
||||
Bangle.emit("HRM", repEvent);
|
||||
}
|
||||
|
||||
var newEvent = {
|
||||
bpm: bpm
|
||||
};
|
||||
|
||||
|
||||
if (location) newEvent.location = location;
|
||||
if (interval) newEvent.rr = interval;
|
||||
if (energyExpended) newEvent.energy = energyExpended;
|
||||
if (battery) newEvent.battery = battery;
|
||||
if (sensorContact) newEvent.contact = sensorContact;
|
||||
|
||||
|
||||
log("Emitting BTHRM: ", newEvent);
|
||||
Bangle.emit("BTHRM", newEvent);
|
||||
}
|
||||
},
|
||||
"0x2a38": {
|
||||
//Body sensor location
|
||||
handler: function(data){
|
||||
handler: function(dv){
|
||||
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
|
||||
if (!lastReceivedData["0x180d"]["0x2a38"]) lastReceivedData["0x180d"]["0x2a38"] = data.target.value;
|
||||
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
|
||||
}
|
||||
},
|
||||
"0x2a19": {
|
||||
//Battery
|
||||
handler: function (event){
|
||||
handler: function (dv){
|
||||
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
|
||||
if (!lastReceivedData["0x180f"]["0x2a19"]) lastReceivedData["0x180f"]["0x2a19"] = event.target.value.getUint8(0);
|
||||
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var device;
|
||||
|
@ -195,7 +189,7 @@
|
|||
maxInterval: 1500
|
||||
};
|
||||
|
||||
function waitingPromise(timeout) {
|
||||
var waitingPromise = function(timeout) {
|
||||
return new Promise(function(resolve){
|
||||
log("Start waiting for " + timeout);
|
||||
setTimeout(()=>{
|
||||
|
@ -203,7 +197,7 @@
|
|||
resolve();
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.enabled){
|
||||
Bangle.isBTHRMOn = function(){
|
||||
|
@ -215,7 +209,6 @@
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
if (settings.replace){
|
||||
var origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
|
@ -229,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){
|
||||
|
@ -252,17 +245,17 @@
|
|||
initBt();
|
||||
}, clampedTime);
|
||||
|
||||
retryTime = Math.pow(retryTime, 1.1);
|
||||
retryTime = Math.pow(clampedTime, 1.1);
|
||||
if (retryTime > maxRetryTime){
|
||||
retryTime = maxRetryTime;
|
||||
}
|
||||
} else {
|
||||
log("Already in retry...");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var buzzing = false;
|
||||
function onDisconnect(reason) {
|
||||
var onDisconnect = function(reason) {
|
||||
log("Disconnect: " + reason);
|
||||
log("GATT: ", gatt);
|
||||
log("Characteristics: ", characteristics);
|
||||
|
@ -277,11 +270,23 @@
|
|||
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
|
||||
// Allows for getting initial state of infrequently updating characteristics, like battery
|
||||
if (newCharacteristic.readValue){
|
||||
result = result.then(()=>{
|
||||
log("Reading data for " + JSON.stringify(newCharacteristic));
|
||||
return newCharacteristic.readValue().then((data)=>{
|
||||
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
|
||||
supportedCharacteristics[newCharacteristic.uuid].handler(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (newCharacteristic.properties.notify){
|
||||
result = result.then(()=>{
|
||||
log("Starting notifications for: ", newCharacteristic);
|
||||
|
@ -290,31 +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;
|
||||
});
|
||||
} else if (newCharacteristic.read){
|
||||
result = result.then(()=>{
|
||||
readData(newCharacteristic);
|
||||
log("Reading data for " + newCharacteristic);
|
||||
return newCharacteristic.read().then((data)=>{
|
||||
supportedCharacteristics[newCharacteristic.uuid].handler(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
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){
|
||||
|
@ -324,13 +321,13 @@
|
|||
if (c.properties.notify){
|
||||
addNotificationHandler(c);
|
||||
}
|
||||
|
||||
|
||||
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){
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
}
|
||||
|
||||
var reUseCounter = 0;
|
||||
};
|
||||
|
||||
function initBt() {
|
||||
var attachServicePromise = function(promise, service) {
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
};
|
||||
|
||||
var initBt = function () {
|
||||
log("initBt with blockInit: " + blockInit);
|
||||
if (blockInit){
|
||||
retry();
|
||||
|
@ -355,63 +350,58 @@
|
|||
|
||||
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.btname){
|
||||
log("Configured device name", settings.btname);
|
||||
filters = [{name: settings.btname}];
|
||||
if (settings.btid){
|
||||
log("Configured device id", settings.btid);
|
||||
filters = [{ id: settings.btid }];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
log("Requesting device with filters", filters);
|
||||
promise = NRF.requestDevice({ filters: filters });
|
||||
|
||||
promise = NRF.requestDevice({ filters: filters, active: true });
|
||||
|
||||
if (settings.gracePeriodRequest){
|
||||
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
||||
}
|
||||
|
||||
|
||||
promise = promise.then((d)=>{
|
||||
log("Got device: ", d);
|
||||
d.on('gattserverdisconnected', onDisconnect);
|
||||
device = d;
|
||||
});
|
||||
|
||||
|
||||
promise = promise.then(()=>{
|
||||
log("Wait after request");
|
||||
return waitingPromise(settings.gracePeriodRequest);
|
||||
});
|
||||
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
log("Reuse device: ", device);
|
||||
}
|
||||
|
||||
|
||||
promise = promise.then(()=>{
|
||||
if (gatt){
|
||||
log("Reuse GATT: ", gatt);
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
|
||||
promise = promise.then((gatt)=>{
|
||||
if (!gatt.connected){
|
||||
var connectPromise = gatt.connect(connectSettings);
|
||||
|
@ -427,16 +417,28 @@
|
|||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* promise = promise.then(() => {
|
||||
log(JSON.stringify(gatt.getSecurityStatus()));
|
||||
if (gatt.getSecurityStatus()['bonded']) {
|
||||
log("Already bonded");
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return gatt.startBonding()
|
||||
.then(() => console.log(gatt.getSecurityStatus()));
|
||||
}
|
||||
});*/
|
||||
|
||||
promise = promise.then(()=>{
|
||||
if (!characteristics || characteristics.length == 0){
|
||||
if (!characteristics || characteristics.length === 0){
|
||||
characteristics = characteristicsFromCache();
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
var characteristicsPromise = Promise.resolve();
|
||||
if (characteristics.length == 0){
|
||||
if (characteristics.length === 0){
|
||||
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
|
@ -454,24 +456,22 @@
|
|||
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;
|
||||
});
|
||||
|
||||
} else {
|
||||
for (var characteristic of characteristics){
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
|
||||
return promise.then(()=>{
|
||||
log("Connection established, waiting for notifications");
|
||||
reUseCounter = 0;
|
||||
characteristicsToCache(characteristics);
|
||||
clearRetryTimeout();
|
||||
}).catch((e) => {
|
||||
|
@ -479,7 +479,7 @@
|
|||
log("Error:", e);
|
||||
onDisconnect(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.setBTHRMPower = function(isOn, app) {
|
||||
// Do app power handling
|
||||
|
@ -487,7 +487,7 @@
|
|||
if (Bangle._PWR===undefined) Bangle._PWR={};
|
||||
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);
|
||||
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) {
|
||||
|
@ -510,7 +510,7 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
@ -525,11 +525,10 @@
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
var fallbackInterval;
|
||||
|
||||
function switchInternalHrm(){
|
||||
|
||||
var switchInternalHrm = function() {
|
||||
if (settings.allowFallback && !fallbackInterval){
|
||||
log("Fallback to HRM enabled");
|
||||
origSetHRMPower(1, "bthrm_fallback");
|
||||
|
@ -542,7 +541,7 @@
|
|||
}
|
||||
}, settings.fallbackTimeout);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
log("Replace HRM event");
|
||||
|
@ -557,11 +556,11 @@
|
|||
}
|
||||
switchInternalHrm();
|
||||
}
|
||||
|
||||
|
||||
E.on("kill", ()=>{
|
||||
if (gatt && gatt.connected){
|
||||
log("Got killed, trying to disconnect");
|
||||
var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
var btm = g.getHeight()-1;
|
||||
var intervalInt;
|
||||
var intervalBt;
|
||||
|
||||
var BODY_LOCS = {
|
||||
0: 'Other',
|
||||
1: 'Chest',
|
||||
2: 'Wrist',
|
||||
3: 'Finger',
|
||||
4: 'Hand',
|
||||
5: 'Ear Lobe',
|
||||
6: 'Foot',
|
||||
}
|
||||
|
||||
function clear(y){
|
||||
g.reset();
|
||||
g.clearRect(0,y,g.getWidth(),y+75);
|
||||
|
@ -15,17 +24,17 @@ function draw(y, type, event) {
|
|||
g.setFontAlign(0,0);
|
||||
g.setFontVector(40).drawString(str,px,y+20);
|
||||
str = "Event: " + type;
|
||||
if (type == "HRM") {
|
||||
if (type === "HRM") {
|
||||
str += " Confidence: " + event.confidence;
|
||||
g.setFontVector(12).drawString(str,px,y+40);
|
||||
str = " Source: " + (event.src ? event.src : "internal");
|
||||
g.setFontVector(12).drawString(str,px,y+50);
|
||||
}
|
||||
if (type == "BTHRM"){
|
||||
if (type === "BTHRM"){
|
||||
if (event.battery) str += " Bat: " + (event.battery ? event.battery : "");
|
||||
g.setFontVector(12).drawString(str,px,y+40);
|
||||
str= "";
|
||||
if (event.location) str += "Loc: " + event.location.toFixed(0) + "ms";
|
||||
if (event.location) str += "Loc: " + BODY_LOCS[event.location];
|
||||
if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
|
||||
g.setFontVector(12).drawString(str,px,y+50);
|
||||
str= "";
|
||||
|
@ -45,7 +54,7 @@ function onBtHrm(e) {
|
|||
firstEventBt = false;
|
||||
}
|
||||
draw(100, "BTHRM", e);
|
||||
if (e.bpm == 0){
|
||||
if (e.bpm === 0){
|
||||
Bangle.buzz(100,0.2);
|
||||
}
|
||||
if (intervalBt){
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
"allowFallback": true,
|
||||
"warnDisconnect": false,
|
||||
"fallbackTimeout": 10,
|
||||
"custom_replace": false,
|
||||
"custom_replace": true,
|
||||
"custom_debuglog": false,
|
||||
"custom_startWithHrm": false,
|
||||
"custom_allowFallback": false,
|
||||
"custom_startWithHrm": true,
|
||||
"custom_allowFallback": true,
|
||||
"custom_warnDisconnect": false,
|
||||
"custom_fallbackTimeout": 10,
|
||||
"gracePeriodNotification": 0,
|
||||
|
|
|
@ -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,bthrm",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
require('Storage').writeJSON(FILE, s);
|
||||
readSettings();
|
||||
}
|
||||
|
||||
|
||||
function readSettings(){
|
||||
settings = Object.assign(
|
||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON(FILE, true) || {}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
var FILE="bthrm.json";
|
||||
var settings;
|
||||
readSettings();
|
||||
|
@ -61,12 +61,13 @@
|
|||
}
|
||||
};
|
||||
|
||||
if (settings.btname){
|
||||
var name = "Clear " + settings.btname;
|
||||
if (settings.btname || settings.btid){
|
||||
var name = "Clear " + (settings.btname || settings.btid);
|
||||
mainmenu[name] = function() {
|
||||
E.showPrompt("Clear current device name?").then((r)=>{
|
||||
E.showPrompt("Clear current device?").then((r)=>{
|
||||
if (r) {
|
||||
writeSettings("btname",undefined);
|
||||
writeSettings("btid",undefined);
|
||||
}
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
|
@ -78,9 +79,7 @@
|
|||
mainmenu.Debug = function() { E.showMenu(submenu_debug); };
|
||||
return mainmenu;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var submenu_debug = {
|
||||
'' : { title: "Debug"},
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||
|
@ -103,35 +102,39 @@
|
|||
|
||||
function createMenuFromScan(){
|
||||
E.showMenu();
|
||||
E.showMessage("Scanning");
|
||||
E.showMessage("Scanning for 4 seconds");
|
||||
|
||||
var submenu_scan = {
|
||||
'' : { title: "Scan"},
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); }
|
||||
};
|
||||
var packets=10;
|
||||
var scanStart=Date.now();
|
||||
NRF.setScan(function(d) {
|
||||
packets--;
|
||||
if (packets<=0 || Date.now() - scanStart > 5000){
|
||||
NRF.setScan();
|
||||
E.showMenu(submenu_scan);
|
||||
} else if (d.name){
|
||||
print("Found device", d);
|
||||
submenu_scan[d.name] = function(){
|
||||
E.showPrompt("Set "+d.name+"?").then((r)=>{
|
||||
if (r) {
|
||||
writeSettings("btname",d.name);
|
||||
}
|
||||
E.showMenu(buildMainMenu());
|
||||
NRF.findDevices(function(devices) {
|
||||
submenu_scan[''] = { title: `Scan (${devices.length} found)`};
|
||||
if (devices.length === 0) {
|
||||
E.showAlert("No devices found")
|
||||
.then(() => E.showMenu(buildMainMenu()));
|
||||
return;
|
||||
} else {
|
||||
devices.forEach((d) => {
|
||||
print("Found device", d);
|
||||
var shown = (d.name || d.id.substr(0, 17));
|
||||
submenu_scan[shown] = function () {
|
||||
E.showPrompt("Set " + shown + "?").then((r) => {
|
||||
if (r) {
|
||||
writeSettings("btid", d.id);
|
||||
// Store the name for displaying later. Will connect by ID
|
||||
if (d.name) {
|
||||
writeSettings("btname", d.name);
|
||||
}
|
||||
}
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
}, { filters: [{services: [ "180d" ]}]});
|
||||
E.showMenu(submenu_scan);
|
||||
}, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]});
|
||||
}
|
||||
|
||||
|
||||
|
||||
var submenu_custom = {
|
||||
'' : { title: "Custom mode"},
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||
|
@ -167,7 +170,7 @@
|
|||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
var submenu_grace = {
|
||||
'' : { title: "Grace periods"},
|
||||
'< Back': function() { E.showMenu(submenu_debug); },
|
||||
|
@ -212,51 +215,6 @@
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
var submenu = {
|
||||
'' : { title: "Grace periods"},
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||
'Request': {
|
||||
value: settings.gracePeriodRequest,
|
||||
min: 0,
|
||||
max: 3000,
|
||||
step: 100,
|
||||
format: v=>v+"ms",
|
||||
onchange: v => {
|
||||
writeSettings("gracePeriodRequest",v);
|
||||
}
|
||||
},
|
||||
'Connect': {
|
||||
value: settings.gracePeriodConnect,
|
||||
min: 0,
|
||||
max: 3000,
|
||||
step: 100,
|
||||
format: v=>v+"ms",
|
||||
onchange: v => {
|
||||
writeSettings("gracePeriodConnect",v);
|
||||
}
|
||||
},
|
||||
'Notification': {
|
||||
value: settings.gracePeriodNotification,
|
||||
min: 0,
|
||||
max: 3000,
|
||||
step: 100,
|
||||
format: v=>v+"ms",
|
||||
onchange: v => {
|
||||
writeSettings("gracePeriodNotification",v);
|
||||
}
|
||||
},
|
||||
'Service': {
|
||||
value: settings.gracePeriodService,
|
||||
min: 0,
|
||||
max: 3000,
|
||||
step: 100,
|
||||
format: v=>v+"ms",
|
||||
onchange: v => {
|
||||
writeSettings("gracePeriodService",v);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
E.showMenu(buildMainMenu());
|
||||
})
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Logs health data to a file every 10 minutes, and provides an app to view it
|
||||
|
||||
**BETA - requires firmware 2v11**
|
||||
**BETA - requires firmware 2v11 or later**
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
@ -140,7 +140,7 @@ declare const require: ((module: 'heatshrink') => {
|
|||
|
||||
declare const Bangle: {
|
||||
// functions
|
||||
buzz: () => void;
|
||||
buzz: (duration?: number, intensity?: number) => Promise<void>;
|
||||
drawWidgets: () => void;
|
||||
isCharging: () => boolean;
|
||||
// events
|
||||
|
@ -158,9 +158,9 @@ declare type Image = {
|
|||
};
|
||||
|
||||
declare type GraphicsApi = {
|
||||
reset: () => void;
|
||||
reset: () => GraphicsApi;
|
||||
flip: () => void;
|
||||
setColor: (color: string) => void; // TODO we can most likely type color more usefully than this
|
||||
setColor: (color: string) => GraphicsApi; // TODO we can most likely type color more usefully than this
|
||||
drawImage: (
|
||||
image: string | Image | ArrayBuffer,
|
||||
xOffset: number,
|
||||
|
@ -169,7 +169,7 @@ declare type GraphicsApi = {
|
|||
rotate?: number;
|
||||
scale?: number;
|
||||
}
|
||||
) => void;
|
||||
) => GraphicsApi;
|
||||
// TODO add more
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue