BT HRM - Allow reading multiple characteristics and services

pull/1388/head
Martin Boonk 2022-01-28 23:47:56 +01:00
parent 4ea4966feb
commit 5197045e39
7 changed files with 868 additions and 256 deletions

View File

@ -7,3 +7,10 @@
Show actual source of event in app
0.04: Automatically reconnect BT sensor
App buzzes if no BTHRM events for more than 3 seconds
0.05: Allow reading additional data if available: HRM battery, position and RR
Better caching of scanned BT device properties
New setting for not starting the BTHRM together with HRM
Save some RAM by not defining functions if disabled in settings
Always emit BTHRM event
Cleanup promises code and allow to configure custom additional waiting times to work around bugs
Disconnect cleanly on exit

View File

@ -1,32 +1,222 @@
(function() {
//var sf = require("Storage").open("bthrm.log","a");
var settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
/*var logline = Date.now().toFixed(3) + " - " + text;
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
if (param){
logline += " " + JSON.stringify(param);
}
sf.write(logline + "\n");
print(logline);*/
print(logline);
}
};
log("Settings: ", settings);
if (settings.enabled){
function clearCache(){
return require('Storage').erase("bthrm.cache.json");
}
function getCache(){
return require('Storage').readJSON("bthrm.cache.json", true) || {};
}
function addNotificationHandler(characteristic){
log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler);
characteristic.on('characteristicvaluechanged', supportedCharacteristics[characteristic.uuid].handler);
}
function writeCache(cache){
var oldCache = getCache();
if (oldCache != cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache)
} else {
log("No changes, don't write cache");
}
}
function characteristicsToCache(characteristics){
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
for (var c of characteristics){
//"handle_value":16,"handle_decl":15
log("Saving handle " + c.handle_value + " for characteristic: ", c);
cache.characteristics[c.uuid] = {
"handle": c.handle_value,
"uuid": c.uuid,
"notify": c.properties.notify,
"read": c.properties.read
};
}
writeCache(cache);
}
function characteristicsFromCache(){
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
var restored = [];
for (var c in cache.characteristics){
var cached = cache.characteristics[c];
var r = new BluetoothRemoteGATTCharacteristic();
log("Restoring characteristic ", cached);
r.handle_value = cached.handle;
r.uuid = cached.uuid;
r.properties = {};
r.properties.notify = cached.notify;
r.properties.read = cached.read;
addNotificationHandler(r);
log("Restored characteristic: ", r);
restored.push(r);
}
return restored;
}
log("Start");
var blockInit = false;
var lastReceivedData={
};
var serviceFilters = [{
services: [ "180d" ]
}];
supportedServices = [
"0x180d", "0x180f"
];
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
handler: function (event){
var dv = event.target.value;
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;
}
var idx = 2 + (flags&1);
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
idx += 2;
}
var interval;
if (flags & 16) {
interval = [];
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
}
}
var location;
if (lastReceivedData && lastReceivedData["0x180d"] && lastReceivedData["0x180d"]["0x2a38"]){
location = lastReceivedData["0x180d"]["0x2a38"];
}
var battery;
if (lastReceivedData && lastReceivedData["0x180f"] && lastReceivedData["0x180f"]["0x2a19"]){
battery = lastReceivedData["0x180f"]["0x2a19"];
}
if (settings.replace){
var newEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM: ", newEvent);
Bangle.emit("HRM", newEvent);
}
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){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
if (!lastReceivedData["0x180d"]["0x2a38"]) lastReceivedData["0x180d"]["0x2a38"] = data.target.value;
}
},
"0x2a19": {
//Battery
handler: function (event){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
if (!lastReceivedData["0x180f"]["0x2a19"]) lastReceivedData["0x180f"]["0x2a19"] = event.target.value.getUint8(0);
}
}
};
var device;
var gatt;
var characteristics = [];
var blockInit = false;
var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var origIsHRMOn = Bangle.isHRMOn;
Bangle.isBTHRMOn = function(){
return (gatt!==undefined && gatt.connected);
var connectSettings = {
minInterval: 7.5,
maxInterval: 1500
};
Bangle.isHRMOn = function() {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
function waitingPromise(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
log("Done waiting for " + timeout);
resolve();
}, timeout);
});
}
if (settings.enabled){
Bangle.isBTHRMOn = function(){
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
};
Bangle.isBTHRMConnected = function(){
return gatt && gatt.connected;
};
}
if (settings.replace){
var origIsHRMOn = Bangle.isHRMOn;
Bangle.isHRMOn = function() {
if (settings.enabled && !settings.replace){
return origIsHRMOn();
} else if (settings.enabled && settings.replace){
@ -34,24 +224,28 @@
}
return origIsHRMOn() || Bangle.isBTHRMOn();
};
}
var serviceFilters = [{
services: [
"180d"
]
}];
function retry(){
log("Retry with time " + retryTime);
function clearRetryTimeout(){
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
}
function retry(){
log("Retry");
if (!currentRetryTimeout){
var clampedTime = retryTime < 100 ? 100 : retryTime;
var clampedTime = retryTime < 200 ? 200 : initialRetryTime;
currentRetryTimeout = setTimeout(() => {
log("Set timeout for retry as " + clampedTime);
clearRetryTimeout();
currentRetryTimeout = setTimeout(() => {
log("Retrying");
currentRetryTimeout = undefined;
initBt();
}, clampedTime);
@ -59,35 +253,93 @@
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
}
var buzzing = false;
function onDisconnect(reason) {
log("Disconnect: " + reason);
log("Gatt: ", gatt);
log("GATT: ", gatt);
log("Characteristics: ", characteristics);
retryTime = initialRetryTime;
clearRetryTimeout();
switchInternalHrm();
blockInit = false;
if (settings.warnDisconnect && !buzzing){
buzzing = true;
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
}
if (Bangle.isBTHRMOn()){
retry();
}
}
function onCharacteristic(event) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit(settings.replace ? "HRM" : "BTHRM", {
bpm: bpm,
confidence: bpm == 0 ? 0 : 100,
src: settings.replace ? "bthrm" : undefined
function createCharacteristicPromise(newCharacteristic){
log("Create characteristic promise: ", newCharacteristic);
var result = Promise.resolve();
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications for: ", newCharacteristic);
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started for ", newCharacteristic));
if (settings.gracePeriodNotification > 0){
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
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){
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
}
function createCharacteristicsPromise(newCharacteristics){
log("Create characteristics promise: ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
if (!supportedCharacteristics[c.uuid]) continue;
log("Supporting characteristic: ", c);
characteristics.push(c);
if (c.properties.notify){
addNotificationHandler(c);
}
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
}
function createServicePromise(service){
log("Create service promise: ", service);
var result = Promise.resolve();
result = result.then(()=>{
log("Handling service: " + service.uuid);
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;
@ -100,66 +352,127 @@
blockInit = true;
var connectionPromise;
if (reUseCounter > 3){
log("Reuse counter to high")
if (gatt.connected == true){
try {
log("Force disconnect with gatt: ", gatt);
gatt.disconnect();
} catch(e) {
log("Error during force disconnect", e);
}
}
if (reUseCounter > 10){
log("Reuse counter to high");
gatt=undefined;
reUseCounter = 0;
}
if (!gatt){
var requestPromise = NRF.requestDevice({ filters: serviceFilters });
connectionPromise = requestPromise.then(function(device) {
gatt = device.gatt;
log("Gatt after request:", gatt);
gatt.device.on('gattserverdisconnected', onDisconnect);
var promise;
if (!device){
promise = NRF.requestDevice({ filters: serviceFilters });
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 {
reUseCounter++;
log("Reusing gatt:", gatt);
connectionPromise = gatt.connect();
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");
clearCache();
}
var newCache = getCache();
newCache.name = device.name;
writeCache(newCache);
gatt = device.gatt;
}
var servicePromise = connectionPromise.then(function() {
return gatt.getPrimaryService(0x180d);
return Promise.resolve(gatt);
});
var characteristicPromise = servicePromise.then(function(service) {
log("Got service:", service);
return service.getCharacteristic(0x2A37);
promise = promise.then((gatt)=>{
if (!gatt.connected){
var connectPromise = gatt.connect(connectSettings);
if (settings.gracePeriodConnect > 0){
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
connectPromise = connectPromise.then(()=>{
log("Wait after connect");
return waitingPromise(settings.gracePeriodConnect);
});
}
return connectPromise;
} else {
return Promise.resolve();
}
});
var notificationPromise = characteristicPromise.then(function(c) {
log("Got characteristic:", c);
c.on('characteristicvaluechanged', onCharacteristic);
return c.startNotifications();
promise = promise.then(()=>{
if (!characteristics || characteristics.length == 0){
characteristics = characteristicsFromCache();
}
});
notificationPromise.then(()=>{
log("Wait for notifications");
retryTime = initialRetryTime;
blockInit=false;
promise = promise.then(()=>{
var getCharacteristicsPromise = Promise.resolve();
if (characteristics.length == 0){
getCharacteristicsPromise = getCharacteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
notificationPromise.catch((e) => {
getCharacteristicsPromise = getCharacteristicsPromise().then((services)=>{
log("Got services:", services);
var result = Promise.resolve();
for (var service of services){
if (!(supportedServices.includes(service.uuid))) continue;
log("Supporting service: ", service.uuid);
result = attachServicePromise(result, service);
}
if (settings.gracePeriodService > 0) {
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService)
});
}
return result;
});
} else {
for (var characteristic of characteristics){
getCharacteristicsPromise = attachCharacteristicPromise(getCharacteristicsPromise, characteristic, true);
}
}
return getCharacteristicsPromise;
});
promise = promise.then(()=>{
log("Connection established, waiting for notifications");
reUseCounter = 0;
characteristicsToCache(characteristics);
clearRetryTimeout();
}).catch((e) => {
characteristics = [];
log("Error:", e);
blockInit = false;
retry();
onDisconnect(e);
});
}
Bangle.setBTHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
// Do app power handling
if (!app) app="?";
if (Bangle._PWR===undefined) Bangle._PWR={};
@ -169,43 +482,58 @@
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
if (!Bangle.isBTHRMOn()) {
initBt();
}
if (!Bangle.isBTHRMConnected()) initBt();
} else { // not on
log("Power off for " + app);
if (gatt) {
try {
if (gatt.connected){
log("Disconnect with gatt: ", gatt);
gatt.disconnect();
} catch(e) {
gatt.disconnect().then(()=>{
log("Successful disconnect", e);
}).catch(()=>{
log("Error during disconnect", e);
});
}
blockInit = false;
gatt = undefined;
}
}
};
var origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled || !isOn){
log("Enable BTHRM power");
if (settings.enabled){
Bangle.setBTHRMPower(isOn, app);
}
if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){
log("Enable HRM power");
if ((settings.enabled && !settings.replace) || !settings.enabled){
origSetHRMPower(isOn, app);
}
};
}
var fallbackInterval;
function switchInternalHrm(){
if (settings.allowFallback && !fallbackInterval){
log("Fallback to HRM enabled");
origSetHRMPower(1, "bthrm_fallback");
fallbackInterval = setInterval(()=>{
if (Bangle.isBTHRMConnected()){
origSetHRMPower(0, "bthrm_fallback");
clearInterval(fallbackInterval);
fallbackInterval = undefined;
log("Fallback to HRM disabled");
}
}, settings.fallbackTimeout);
}
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled && settings.replace){
if (settings.replace){
log("Replace HRM event");
if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){
if (Bangle._PWR && Bangle._PWR.HRM){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
@ -214,5 +542,16 @@
if (Bangle._PWR.HRM===undefined) break;
}
}
switchInternalHrm();
}
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
var promise = gatt.disconnect();
promise.then(()=>log("Disconnected on kill"));
promise.catch((e)=>log("Error during disconnnect on kill", e));
}
});
}
})();

View File

@ -1,69 +1,95 @@
var btm = g.getHeight()-1;
var eventInt = null;
var eventBt = null;
var counterInt = 0;
var counterBt = 0;
var intervalInt;
var intervalBt;
function clear(y){
g.reset();
g.clearRect(0,y,g.getWidth(),y+75);
}
function draw(y, event, type, counter) {
function draw(y, type, event) {
clear(y);
var px = g.getWidth()/2;
var str = event.bpm + "";
g.reset();
g.setFontAlign(0,0);
g.clearRect(0,y,g.getWidth(),y+75);
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;
if (type == "HRM") str += " Source: " + (event.src ? event.src : "internal");
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 (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.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
g.setFontVector(12).drawString(str,px,y+50);
str= "";
if (event.contact) str += " Contact: " + event.contact;
if (event.energy) str += " kJoule: " + event.energy.toFixed(0);
g.setFontVector(12).drawString(str,px,y+60);
}
}
var firstEventBt = true;
var firstEventInt = true;
function onBtHrm(e) {
//print("Event for BT " + JSON.stringify(e));
if (firstEventBt){
clear(24);
firstEventBt = false;
}
draw(100, "BTHRM", e);
if (e.bpm == 0){
Bangle.buzz(100,0.2);
}
if (counterBt == 0){
Bangle.buzz(200,0.5);
if (intervalBt){
clearInterval(intervalBt);
}
counterBt += 3;
eventBt = e;
intervalBt = setInterval(()=>{
clear(100);
}, 2000);
}
function onHrm(e) {
//print("Event for Int " + JSON.stringify(e));
counterInt += 3;
eventInt = e;
if (firstEventInt){
clear(24);
firstEventInt = false;
}
draw(24, "HRM", e);
if (intervalInt){
clearInterval(intervalInt);
}
intervalInt = setInterval(()=>{
clear(24);
}, 2000);
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm);
Bangle.setHRMPower(1,'bthrm');
if (!(settings.startWithHrm)){
Bangle.setBTHRMPower(1,'bthrm');
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
if (Bangle.setBTHRMPower){
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 > 3) counterInt = 3;
draw(24, eventInt, "HRM", counterInt);
}
function drawBt(){
counterBt--;
if (counterBt < 0) counterBt = 0;
if (counterBt > 3) counterBt = 3;
draw(100, eventBt, "BTHRM", counterBt);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24);
} else {
g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32);
}
var interval = setInterval(drawInt, 1000);
var interval = setInterval(drawBt, 1000);
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm'));

13
apps/bthrm/default.json Normal file
View File

@ -0,0 +1,13 @@
{
"enabled": true,
"replace": true,
"debuglog": false,
"startWithHrm": true,
"allowFallback": true,
"warnDisconnect": false,
"fallbackTimeout": 10,
"gracePeriodNotification": 0,
"gracePeriodConnect": 0,
"gracePeriodService": 0,
"gracePeriodRequest": 0
}

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.04",
"version": "0.05",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
@ -14,6 +14,7 @@
{"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.settings.js","url":"settings.js"}
{"name":"bthrm.settings.js","url":"settings.js"},
{"name":"bthrm.default.json","url":"default.json"}
]
}

View File

@ -1,26 +1,38 @@
(function(recorders) {
recorders.bthrm = function() {
var bpm = "";
var bat = "";
var energy = "";
var contact = "";
var rr= "";
function onHRM(h) {
bpm = h.bpm;
bat = h.bat;
energy = h.energy;
contact = h.contact;
if (h.rr) rr = h.rr.join(";");
}
return {
name : "BT HR",
fields : ["BT Heartrate"],
fields : ["BT Heartrate", "BT Battery", "Energy expended", "Contact", "RR"],
getValues : () => {
result = [bpm];
result = [bpm,bat,energy,contact,rr];
bpm = "";
rr = "";
bat = "";
energy = "";
contact = "";
return result;
},
start : () => {
Bangle.on('BTHRM', onHRM);
Bangle.setBTHRMPower(1,"recorder");
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder");
},
stop : () => {
Bangle.removeListener('BTHRM', onHRM);
Bangle.setBTHRMPower(0,"recorder");
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
},
draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
draw : (x,y) => g.setColor((Bangle.isBTHRMConnected && Bangle.isBTHRMConnected())?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
};
}
})

View File

@ -1,33 +1,247 @@
(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);
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
E.showMenu({
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
var FILE="bthrm.json";
var settings;
readSettings();
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
onchange: v => {
settings.enabled = v;
writeSettings();
writeSettings("enabled",v);
}
},
'Use HRM event': {
'Replace HRM': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
onchange: v => {
settings.replace = v;
writeSettings();
writeSettings("replace",v);
}
},
'Start with HRM': {
value: !!settings.startWithHrm,
format: v => settings.startWithHrm ? "On" : "Off",
onchange: v => {(function(back) {
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
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();
var mainmenu = {
'': { 'title': 'Bluetooth HRM' },
'< Back': back,
'Use BT HRM': {
value: !!settings.enabled,
format: v => settings.enabled ? "On" : "Off",
onchange: v => {
writeSettings("enabled",v);
}
},
'Replace HRM': {
value: !!settings.replace,
format: v => settings.replace ? "On" : "Off",
onchange: v => {
writeSettings("replace",v);
}
},
'Start w. HRM': {
value: !!settings.startWithHrm,
format: v => settings.startWithHrm ? "On" : "Off",
onchange: v => {
writeSettings("startWithHrm",v);
}
},
'HRM Fallback': {
value: !!settings.allowFallback,
format: v => settings.allowFallback ? "On" : "Off",
onchange: v => {
writeSettings("allowFallback",v);
}
},
'Fallback Timeout': {
value: settings.fallbackTimeout,
min: 5,
max: 60,
step: 5,
format: v=>v+"s",
onchange: v => {
writeSettings("fallbackTimout",v*1000);
}
},
'Conn. Alert': {
value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
onchange: v => {
writeSettings("warnDisconnect",v);
}
},
'Debug log': {
value: !!settings.debuglog,
format: v => settings.debuglog ? "On" : "Off",
onchange: v => {
writeSettings("debuglog",v);
}
},
'Grace periods >': function() { E.showMenu(submenu); }
};
var submenu = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(mainmenu); },
'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(mainmenu);
})
writeSettings("startWithHrm",v);
}
},
'Fallback to HRM': {
value: !!settings.allowFallback,
format: v => settings.allowFallback ? "On" : "Off",
onchange: v => {
writeSettings("allowFallback",v);
}
},
'Fallback Timeout': {
value: settings.fallbackTimeout,
min: 5,
max: 60,
step: 5,
format: v=>v+"s",
onchange: v => {
writeSettings("fallbackTimout",v*1000);
}
},
'Conn. Alert': {
value: !!settings.warnDisconnect,
format: v => settings.warnDisconnect ? "On" : "Off",
onchange: v => {
writeSettings("warnDisconnect",v);
}
},
'Debug log': {
value: !!settings.debuglog,
format: v => settings.debuglog ? "On" : "Off",
onchange: v => {
writeSettings("debuglog",v);
}
},
'Grace periods': function() { E.showMenu(submenu); }
};
var submenu = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(mainmenu); },
'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(mainmenu);
})