Merge branch 'master' of github.com:espruino/BangleApps
commit
c972abe75c
|
|
@ -0,0 +1,60 @@
|
||||||
|
name: Bangle bug report
|
||||||
|
description: "Bangle: Create a issue to help us improve!"
|
||||||
|
title: "Short description and provide the affected app/widget"
|
||||||
|
labels: ["bug"]
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Attention: If you have a question then ask it at the [bangle forum](http://forum.espruino.com/microcosms/1424/), please!
|
||||||
|
- type: dropdown
|
||||||
|
id: hwversion
|
||||||
|
attributes:
|
||||||
|
label: Affected hardware version
|
||||||
|
description: |
|
||||||
|
Which Bangle hardware version(s) is/are affected?
|
||||||
|
_Hint: You can select multiple entries._
|
||||||
|
options:
|
||||||
|
- Bangle 1
|
||||||
|
- Bangle 2
|
||||||
|
multiple: true
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: fwversion
|
||||||
|
attributes:
|
||||||
|
label: Your firmware version
|
||||||
|
description: |
|
||||||
|
## Please make sure you installed the latest (released) firmware!
|
||||||
|
How to see FW version? Within the AppLoader at "More..."-page: Device info; via the "about"-app or the "firmware"-widget.
|
||||||
|
If the issue will occur in "Cutting Edge build" only, please mention this.
|
||||||
|
|
||||||
|
FW Update instructions:
|
||||||
|
**Bangle 2: [firmware update instructions](https://www.espruino.com/Bangle.js2#firmware-updates)**
|
||||||
|
**Bangle 1: [firmware update instructions](https://www.espruino.com/Bangle.js#firmware-updates)**
|
||||||
|
_Hint: The links will open inplace hold e.g. ctrl/cmd-key and click to open in a new tab instead._
|
||||||
|
placeholder: e.g. 2v12
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: report
|
||||||
|
attributes:
|
||||||
|
label: The bug
|
||||||
|
description: |
|
||||||
|
## Please also mention the expected behaviour and steps to reproduce
|
||||||
|
placeholder: |
|
||||||
|
###Describe the bug
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
###Expected behavior
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
###Steps to reproduce
|
||||||
|
1. Start app xy
|
||||||
|
2. choose abc
|
||||||
|
3. bug occurs
|
||||||
|
|
||||||
|
It could be helpfull for us to provide the devopment folder in 'bangle apps' folder
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
@ -7,3 +7,10 @@
|
||||||
Show actual source of event in app
|
Show actual source of event in app
|
||||||
0.04: Automatically reconnect BT sensor
|
0.04: Automatically reconnect BT sensor
|
||||||
App buzzes if no BTHRM events for more than 3 seconds
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,218 +1,557 @@
|
||||||
(function() {
|
(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 log = function(text, param){
|
||||||
/*var logline = Date.now().toFixed(3) + " - " + text;
|
if (settings.debuglog){
|
||||||
if (param){
|
var logline = new Date().toISOString() + " - " + text;
|
||||||
logline += " " + JSON.stringify(param);
|
if (param){
|
||||||
|
logline += " " + JSON.stringify(param);
|
||||||
|
}
|
||||||
|
print(logline);
|
||||||
}
|
}
|
||||||
sf.write(logline + "\n");
|
|
||||||
print(logline);*/
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Start");
|
|
||||||
|
|
||||||
var blockInit = false;
|
|
||||||
var gatt;
|
|
||||||
var currentRetryTimeout;
|
|
||||||
var initialRetryTime = 40;
|
|
||||||
var maxRetryTime = 60000;
|
|
||||||
var retryTime = initialRetryTime;
|
|
||||||
|
|
||||||
var origIsHRMOn = Bangle.isHRMOn;
|
|
||||||
|
|
||||||
Bangle.isBTHRMOn = function(){
|
|
||||||
return (gatt!==undefined && gatt.connected);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Bangle.isHRMOn = function() {
|
log("Settings: ", settings);
|
||||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
|
||||||
|
|
||||||
if (settings.enabled && !settings.replace){
|
|
||||||
return origIsHRMOn();
|
|
||||||
} else if (settings.enabled && settings.replace){
|
|
||||||
return Bangle.isBTHRMOn();
|
|
||||||
}
|
|
||||||
return origIsHRMOn() || Bangle.isBTHRMOn();
|
|
||||||
};
|
|
||||||
|
|
||||||
var serviceFilters = [{
|
if (settings.enabled){
|
||||||
services: [
|
|
||||||
"180d"
|
|
||||||
]
|
|
||||||
}];
|
|
||||||
|
|
||||||
function retry(){
|
function clearCache(){
|
||||||
log("Retry with time " + retryTime);
|
return require('Storage').erase("bthrm.cache.json");
|
||||||
if (currentRetryTimeout){
|
}
|
||||||
log("Clearing timeout " + currentRetryTimeout);
|
|
||||||
clearTimeout(currentRetryTimeout);
|
function getCache(){
|
||||||
currentRetryTimeout = undefined;
|
return require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
var clampedTime = retryTime < 200 ? 200 : initialRetryTime;
|
function addNotificationHandler(characteristic){
|
||||||
currentRetryTimeout = setTimeout(() => {
|
log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler);
|
||||||
log("Set timeout for retry as " + clampedTime);
|
characteristic.on('characteristicvaluechanged', supportedCharacteristics[characteristic.uuid].handler);
|
||||||
initBt();
|
|
||||||
}, clampedTime);
|
|
||||||
|
|
||||||
retryTime = Math.pow(retryTime, 1.1);
|
|
||||||
if (retryTime > maxRetryTime){
|
|
||||||
retryTime = maxRetryTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDisconnect(reason) {
|
|
||||||
log("Disconnect: " + reason);
|
|
||||||
log("Gatt: ", gatt);
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var reUseCounter=0;
|
|
||||||
|
|
||||||
function initBt() {
|
|
||||||
log("initBt with blockInit: " + blockInit);
|
|
||||||
if (blockInit){
|
|
||||||
retry();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockInit = true;
|
function writeCache(cache){
|
||||||
|
var oldCache = getCache();
|
||||||
var connectionPromise;
|
if (oldCache != cache) {
|
||||||
|
log("Writing cache");
|
||||||
|
require('Storage').writeJSON("bthrm.cache.json", cache)
|
||||||
|
} else {
|
||||||
|
log("No changes, don't write cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (reUseCounter > 3){
|
function characteristicsToCache(characteristics){
|
||||||
log("Reuse counter to high")
|
log("Cache characteristics");
|
||||||
if (gatt.connected == true){
|
var cache = getCache();
|
||||||
try {
|
if (!cache.characteristics) cache.characteristics = {};
|
||||||
log("Force disconnect with gatt: ", gatt);
|
for (var c of characteristics){
|
||||||
gatt.disconnect();
|
//"handle_value":16,"handle_decl":15
|
||||||
} catch(e) {
|
log("Saving handle " + c.handle_value + " for characteristic: ", c);
|
||||||
log("Error during force disconnect", e);
|
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 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gatt=undefined;
|
|
||||||
reUseCounter = 0;
|
};
|
||||||
}
|
|
||||||
|
var device;
|
||||||
if (!gatt){
|
var gatt;
|
||||||
var requestPromise = NRF.requestDevice({ filters: serviceFilters });
|
var characteristics = [];
|
||||||
connectionPromise = requestPromise.then(function(device) {
|
var blockInit = false;
|
||||||
gatt = device.gatt;
|
var currentRetryTimeout;
|
||||||
log("Gatt after request:", gatt);
|
var initialRetryTime = 40;
|
||||||
gatt.device.on('gattserverdisconnected', onDisconnect);
|
var maxRetryTime = 60000;
|
||||||
|
var retryTime = initialRetryTime;
|
||||||
|
|
||||||
|
var connectSettings = {
|
||||||
|
minInterval: 7.5,
|
||||||
|
maxInterval: 1500
|
||||||
|
};
|
||||||
|
|
||||||
|
function waitingPromise(timeout) {
|
||||||
|
return new Promise(function(resolve){
|
||||||
|
log("Start waiting for " + timeout);
|
||||||
|
setTimeout(()=>{
|
||||||
|
log("Done waiting for " + timeout);
|
||||||
|
resolve();
|
||||||
|
}, timeout);
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
reUseCounter++;
|
|
||||||
log("Reusing gatt:", gatt);
|
if (settings.enabled){
|
||||||
connectionPromise = gatt.connect();
|
Bangle.isBTHRMOn = function(){
|
||||||
|
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.isBTHRMConnected = function(){
|
||||||
|
return gatt && gatt.connected;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var servicePromise = connectionPromise.then(function() {
|
if (settings.replace){
|
||||||
return gatt.getPrimaryService(0x180d);
|
var origIsHRMOn = Bangle.isHRMOn;
|
||||||
});
|
|
||||||
|
|
||||||
var characteristicPromise = servicePromise.then(function(service) {
|
Bangle.isHRMOn = function() {
|
||||||
log("Got service:", service);
|
if (settings.enabled && !settings.replace){
|
||||||
return service.getCharacteristic(0x2A37);
|
return origIsHRMOn();
|
||||||
});
|
} else if (settings.enabled && settings.replace){
|
||||||
|
return Bangle.isBTHRMOn();
|
||||||
var notificationPromise = characteristicPromise.then(function(c) {
|
|
||||||
log("Got characteristic:", c);
|
|
||||||
c.on('characteristicvaluechanged', onCharacteristic);
|
|
||||||
return c.startNotifications();
|
|
||||||
});
|
|
||||||
notificationPromise.then(()=>{
|
|
||||||
log("Wait for notifications");
|
|
||||||
retryTime = initialRetryTime;
|
|
||||||
blockInit=false;
|
|
||||||
});
|
|
||||||
notificationPromise.catch((e) => {
|
|
||||||
log("Error:", e);
|
|
||||||
blockInit = false;
|
|
||||||
retry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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={};
|
|
||||||
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);
|
|
||||||
isOn = Bangle._PWR.BTHRM.length;
|
|
||||||
// so now we know if we're really on
|
|
||||||
if (isOn) {
|
|
||||||
if (!Bangle.isBTHRMOn()) {
|
|
||||||
initBt();
|
|
||||||
}
|
|
||||||
} else { // not on
|
|
||||||
log("Power off for " + app);
|
|
||||||
if (gatt) {
|
|
||||||
try {
|
|
||||||
log("Disconnect with gatt: ", gatt);
|
|
||||||
gatt.disconnect();
|
|
||||||
} catch(e) {
|
|
||||||
log("Error during disconnect", e);
|
|
||||||
}
|
}
|
||||||
blockInit = false;
|
return origIsHRMOn() || Bangle.isBTHRMOn();
|
||||||
gatt = undefined;
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRetryTimeout(){
|
||||||
|
if (currentRetryTimeout){
|
||||||
|
log("Clearing timeout " + currentRetryTimeout);
|
||||||
|
clearTimeout(currentRetryTimeout);
|
||||||
|
currentRetryTimeout = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
function retry(){
|
||||||
var origSetHRMPower = Bangle.setHRMPower;
|
log("Retry");
|
||||||
|
|
||||||
|
if (!currentRetryTimeout){
|
||||||
|
|
||||||
|
var clampedTime = retryTime < 100 ? 100 : retryTime;
|
||||||
|
|
||||||
|
log("Set timeout for retry as " + clampedTime);
|
||||||
|
clearRetryTimeout();
|
||||||
|
currentRetryTimeout = setTimeout(() => {
|
||||||
|
log("Retrying");
|
||||||
|
currentRetryTimeout = undefined;
|
||||||
|
initBt();
|
||||||
|
}, clampedTime);
|
||||||
|
|
||||||
|
retryTime = Math.pow(retryTime, 1.1);
|
||||||
|
if (retryTime > maxRetryTime){
|
||||||
|
retryTime = maxRetryTime;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log("Already in retry...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buzzing = false;
|
||||||
|
function onDisconnect(reason) {
|
||||||
|
log("Disconnect: " + reason);
|
||||||
|
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 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));
|
||||||
|
}
|
||||||
|
|
||||||
Bangle.setHRMPower = function(isOn, app) {
|
function attachCharacteristicPromise(promise, characteristic){
|
||||||
log("setHRMPower for " + app + ":" + (isOn?"on":"off"));
|
return promise.then(()=>{
|
||||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
log("Handling characteristic:", characteristic);
|
||||||
if (settings.enabled || !isOn){
|
return createCharacteristicPromise(characteristic);
|
||||||
log("Enable BTHRM power");
|
});
|
||||||
Bangle.setBTHRMPower(isOn, app);
|
|
||||||
}
|
}
|
||||||
if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){
|
|
||||||
log("Enable HRM power");
|
function createCharacteristicsPromise(newCharacteristics){
|
||||||
origSetHRMPower(isOn, app);
|
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){
|
||||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
log("Create service promise: ", service);
|
||||||
if (settings.enabled && settings.replace){
|
var result = Promise.resolve();
|
||||||
log("Replace HRM event");
|
result = result.then(()=>{
|
||||||
if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){
|
log("Handling service: " + service.uuid);
|
||||||
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
|
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
|
||||||
var app = Bangle._PWR.HRM[i];
|
});
|
||||||
log("Moving app " + app);
|
return result.then(()=>log("Handled service" + service.uuid));
|
||||||
origSetHRMPower(0, app);
|
}
|
||||||
Bangle.setBTHRMPower(1, app);
|
|
||||||
if (Bangle._PWR.HRM===undefined) break;
|
function attachServicePromise(promise, service){
|
||||||
|
return promise.then(()=>createServicePromise(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
var reUseCounter = 0;
|
||||||
|
|
||||||
|
function initBt() {
|
||||||
|
log("initBt with blockInit: " + blockInit);
|
||||||
|
if (blockInit){
|
||||||
|
retry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockInit = true;
|
||||||
|
|
||||||
|
if (reUseCounter > 10){
|
||||||
|
log("Reuse counter to high");
|
||||||
|
gatt=undefined;
|
||||||
|
reUseCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(gatt);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
promise = promise.then(()=>{
|
||||||
|
if (!characteristics || characteristics.length == 0){
|
||||||
|
characteristics = characteristicsFromCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
promise = promise.then(()=>{
|
||||||
|
var getCharacteristicsPromise = Promise.resolve();
|
||||||
|
if (characteristics.length == 0){
|
||||||
|
getCharacteristicsPromise = getCharacteristicsPromise.then(()=>{
|
||||||
|
log("Getting services");
|
||||||
|
return gatt.getPrimaryServices();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
onDisconnect(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.setBTHRMPower = function(isOn, app) {
|
||||||
|
// Do app power handling
|
||||||
|
if (!app) app="?";
|
||||||
|
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);
|
||||||
|
isOn = Bangle._PWR.BTHRM.length;
|
||||||
|
// so now we know if we're really on
|
||||||
|
if (isOn) {
|
||||||
|
if (!Bangle.isBTHRMConnected()) initBt();
|
||||||
|
} else { // not on
|
||||||
|
log("Power off for " + app);
|
||||||
|
if (gatt) {
|
||||||
|
if (gatt.connected){
|
||||||
|
log("Disconnect with gatt: ", gatt);
|
||||||
|
gatt.disconnect().then(()=>{
|
||||||
|
log("Successful disconnect", e);
|
||||||
|
}).catch(()=>{
|
||||||
|
log("Error during disconnect", e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var origSetHRMPower = Bangle.setHRMPower;
|
||||||
|
|
||||||
|
if (settings.startWithHrm){
|
||||||
|
|
||||||
|
Bangle.setHRMPower = function(isOn, app) {
|
||||||
|
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
|
||||||
|
if (settings.enabled){
|
||||||
|
Bangle.setBTHRMPower(isOn, app);
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.replace){
|
||||||
|
log("Replace HRM event");
|
||||||
|
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);
|
||||||
|
origSetHRMPower(0, app);
|
||||||
|
Bangle.setBTHRMPower(1, app);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,95 @@
|
||||||
var btm = g.getHeight()-1;
|
var btm = g.getHeight()-1;
|
||||||
var eventInt = null;
|
var intervalInt;
|
||||||
var eventBt = null;
|
var intervalBt;
|
||||||
var counterInt = 0;
|
|
||||||
var counterBt = 0;
|
|
||||||
|
|
||||||
|
function clear(y){
|
||||||
function draw(y, event, type, counter) {
|
|
||||||
var px = g.getWidth()/2;
|
|
||||||
g.reset();
|
g.reset();
|
||||||
g.setFontAlign(0,0);
|
|
||||||
g.clearRect(0,y,g.getWidth(),y+75);
|
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");
|
|
||||||
g.setFontVector(12).drawString(str,px,y+60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function draw(y, type, event) {
|
||||||
|
clear(y);
|
||||||
|
var px = g.getWidth()/2;
|
||||||
|
var str = event.bpm + "";
|
||||||
|
g.reset();
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setFontVector(40).drawString(str,px,y+20);
|
||||||
|
str = "Event: " + type;
|
||||||
|
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) {
|
function onBtHrm(e) {
|
||||||
//print("Event for BT " + JSON.stringify(e));
|
if (firstEventBt){
|
||||||
|
clear(24);
|
||||||
|
firstEventBt = false;
|
||||||
|
}
|
||||||
|
draw(100, "BTHRM", e);
|
||||||
if (e.bpm == 0){
|
if (e.bpm == 0){
|
||||||
Bangle.buzz(100,0.2);
|
Bangle.buzz(100,0.2);
|
||||||
}
|
}
|
||||||
if (counterBt == 0){
|
if (intervalBt){
|
||||||
Bangle.buzz(200,0.5);
|
clearInterval(intervalBt);
|
||||||
}
|
}
|
||||||
counterBt += 3;
|
intervalBt = setInterval(()=>{
|
||||||
eventBt = e;
|
clear(100);
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onHrm(e) {
|
function onHrm(e) {
|
||||||
//print("Event for Int " + JSON.stringify(e));
|
if (firstEventInt){
|
||||||
counterInt += 3;
|
clear(24);
|
||||||
eventInt = e;
|
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('BTHRM', onBtHrm);
|
||||||
Bangle.on('HRM', onHrm);
|
Bangle.on('HRM', onHrm);
|
||||||
|
|
||||||
Bangle.setHRMPower(1,'bthrm');
|
Bangle.setHRMPower(1,'bthrm');
|
||||||
|
if (!(settings.startWithHrm)){
|
||||||
|
Bangle.setBTHRMPower(1,'bthrm');
|
||||||
|
}
|
||||||
|
|
||||||
g.clear();
|
g.clear();
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
if (Bangle.setBTHRMPower){
|
||||||
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||||
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
|
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24);
|
||||||
|
} else {
|
||||||
function drawInt(){
|
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||||
counterInt--;
|
g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var interval = setInterval(drawInt, 1000);
|
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm'));
|
||||||
var interval = setInterval(drawBt, 1000);
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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.04",
|
"version": "0.05",
|
||||||
"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",
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
{"name":"bthrm.recorder.js","url":"recorder.js"},
|
{"name":"bthrm.recorder.js","url":"recorder.js"},
|
||||||
{"name":"bthrm.boot.js","url":"boot.js"},
|
{"name":"bthrm.boot.js","url":"boot.js"},
|
||||||
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
|
{"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"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,38 @@
|
||||||
(function(recorders) {
|
(function(recorders) {
|
||||||
recorders.bthrm = function() {
|
recorders.bthrm = function() {
|
||||||
var bpm = "";
|
var bpm = "";
|
||||||
|
var bat = "";
|
||||||
|
var energy = "";
|
||||||
|
var contact = "";
|
||||||
|
var rr= "";
|
||||||
function onHRM(h) {
|
function onHRM(h) {
|
||||||
bpm = h.bpm;
|
bpm = h.bpm;
|
||||||
|
bat = h.bat;
|
||||||
|
energy = h.energy;
|
||||||
|
contact = h.contact;
|
||||||
|
if (h.rr) rr = h.rr.join(";");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name : "BTHR",
|
name : "BT HR",
|
||||||
fields : ["BT Heartrate"],
|
fields : ["BT Heartrate", "BT Battery", "Energy expended", "Contact", "RR"],
|
||||||
getValues : () => {
|
getValues : () => {
|
||||||
result = [bpm];
|
result = [bpm,bat,energy,contact,rr];
|
||||||
bpm = "";
|
bpm = "";
|
||||||
|
rr = "";
|
||||||
|
bat = "";
|
||||||
|
energy = "";
|
||||||
|
contact = "";
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
start : () => {
|
start : () => {
|
||||||
Bangle.on('BTHRM', onHRM);
|
Bangle.on('BTHRM', onHRM);
|
||||||
Bangle.setBTHRMPower(1,"recorder");
|
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder");
|
||||||
},
|
},
|
||||||
stop : () => {
|
stop : () => {
|
||||||
Bangle.removeListener('BTHRM', onHRM);
|
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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,247 @@
|
||||||
(function(back) {
|
(function(back) {
|
||||||
var FILE = "bthrm.json";
|
function writeSettings(key, value) {
|
||||||
|
var s = require('Storage').readJSON(FILE, true) || {};
|
||||||
var settings = Object.assign({
|
s[key] = value;
|
||||||
enabled: true,
|
require('Storage').writeJSON(FILE, s);
|
||||||
replace: true,
|
readSettings();
|
||||||
}, require('Storage').readJSON(FILE, true) || {});
|
|
||||||
|
|
||||||
function writeSettings() {
|
|
||||||
require('Storage').writeJSON(FILE, settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readSettings(){
|
||||||
|
settings = Object.assign(
|
||||||
|
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||||
|
require('Storage').readJSON(FILE, true) || {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var FILE="bthrm.json";
|
||||||
|
var settings;
|
||||||
|
readSettings();
|
||||||
|
|
||||||
E.showMenu({
|
var mainmenu = {
|
||||||
'': { 'title': 'Bluetooth HRM' },
|
'': { 'title': 'Bluetooth HRM' },
|
||||||
'< Back': back,
|
'< Back': back,
|
||||||
'Use BT HRM': {
|
'Use BT HRM': {
|
||||||
value: !!settings.enabled,
|
value: !!settings.enabled,
|
||||||
format: v => settings.enabled ? "On" : "Off",
|
format: v => settings.enabled ? "On" : "Off",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
settings.enabled = v;
|
writeSettings("enabled",v);
|
||||||
writeSettings();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Use HRM event': {
|
'Replace HRM': {
|
||||||
value: !!settings.replace,
|
value: !!settings.replace,
|
||||||
format: v => settings.replace ? "On" : "Off",
|
format: v => settings.replace ? "On" : "Off",
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
settings.replace = v;
|
writeSettings("replace",v);
|
||||||
writeSettings();
|
}
|
||||||
|
},
|
||||||
|
'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);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
0.01: New App!
|
||||||
|
0.02: Make overriding the HRM event optional
|
||||||
|
Emit BTHRM event for external sensor
|
||||||
|
Add recorder app plugin
|
||||||
|
0.03: Prevent readings from internal sensor mixing into BT values
|
||||||
|
Mark events with src property
|
||||||
|
Show actual source of event in app
|
||||||
|
0.04: Allow reading additional data if available: HRM battery and position
|
||||||
|
Better caching of scanned BT device properties
|
||||||
|
New setting for not starting the BTHRM together with HRM
|
||||||
|
Save some RAM by not definining functions if disabled in settings
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Bluetooth Heart Rate Variance
|
||||||
|
|
||||||
|
This app uses [BTHRM](https://banglejs.com/apps/#bthrm) and can calculate the HRV if the used bluetooth heart rate monitor delivers interval data.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Just install and start the app. Select button resets the already measured values.
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
[halemmerich](https://github.com/halemmerich)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwJC/ABUMAokcAq0eAok+Aok2AgcCm0EAoUHmw2DAoMOAgMDh9jEgPAg/98cfn/gg/58cbv/ggcB8cz8HADIPjmIECgHB8OAAoVB8AFDgPgIQcBCwYFMAH4ARA"))
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
var btm = g.getHeight()-1;
|
||||||
|
var ui = false;
|
||||||
|
|
||||||
|
function clear(y){
|
||||||
|
g.reset();
|
||||||
|
g.clearRect(0,y,g.getWidth(),g.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingTime;
|
||||||
|
var currentSlot = 0;
|
||||||
|
var hrvSlots = [10,20,30,60,120,300];
|
||||||
|
var hrvValues = {};
|
||||||
|
var rrRmsProgress;
|
||||||
|
var saved = false;
|
||||||
|
|
||||||
|
var rrNumberOfValues = 0;
|
||||||
|
var rrSquared = 0;
|
||||||
|
var rrLastValue
|
||||||
|
var rrMax;
|
||||||
|
var rrMin;
|
||||||
|
|
||||||
|
function calcHrv(rr){
|
||||||
|
//Calculate HRV with RMSSD method: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5624990/
|
||||||
|
for (currentRr of rr){
|
||||||
|
if (!rrMax) rrMax = currentRr;
|
||||||
|
if (!rrMin) rrMin = currentRr;
|
||||||
|
rrMax = Math.max(rrMax, currentRr);
|
||||||
|
rrMin = Math.min(rrMin, currentRr);
|
||||||
|
//print("Calc for: " + currentRr);
|
||||||
|
rrNumberOfValues++;
|
||||||
|
if (!rrLastValue){
|
||||||
|
rrLastValue = currentRr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rrSquared += (rrLastValue - currentRr)*(rrLastValue - currentRr);
|
||||||
|
|
||||||
|
//print("rr²: " + rrSquared);
|
||||||
|
rrLastValue = currentRr;
|
||||||
|
}
|
||||||
|
var rms = Math.sqrt(rrSquared / rrNumberOfValues);
|
||||||
|
//print("rms: " + rms);
|
||||||
|
return rms;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function draw(y, hrv) {
|
||||||
|
clear(y);
|
||||||
|
var px = g.getWidth()/2;
|
||||||
|
var str = hrv.toFixed(1) + "ms";
|
||||||
|
g.reset();
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.setFontVector(40).drawString(str,px,y+20);
|
||||||
|
|
||||||
|
for (var i = 0; i < hrvSlots.length; i++){
|
||||||
|
str = hrvSlots[i] + "s: ";
|
||||||
|
if (hrvValues[hrvSlots[i]]) str += hrvValues[hrvSlots[i]].toFixed(1) + "ms";
|
||||||
|
g.setFontVector(16).drawString(str,px,y+44+(i*17));
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setRotation(3);
|
||||||
|
g.setFontVector(12).drawString("Reset",g.getHeight()/2, g.getWidth()-10);
|
||||||
|
g.setRotation(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBtHrm(e) {
|
||||||
|
if (e.rr && !startingTime) Bangle.buzz(500);
|
||||||
|
if (e.rr && !startingTime) startingTime=Date.now();
|
||||||
|
//print("Event:" + e.rr);
|
||||||
|
|
||||||
|
var hrv = calcHrv(e.rr);
|
||||||
|
if (hrv){
|
||||||
|
if (currentSlot <= hrvSlots.length && (Date.now() - startingTime) > (hrvSlots[currentSlot] * 1000) && !hrvValues[hrvSlots[currentSlot]]){
|
||||||
|
hrvValues[hrvSlots[currentSlot]] = hrv;
|
||||||
|
currentSlot++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!saved && currentSlot == hrvSlots.length){
|
||||||
|
var file = require('Storage').open("bthrv.csv", "a");
|
||||||
|
var data = new Date(startingTime).toISOString();
|
||||||
|
for (var c of hrvSlots){
|
||||||
|
data+=","+hrvValues[c];
|
||||||
|
}
|
||||||
|
data+="," + rrMax + "," + rrMin + ","+rrNumberOfValues;
|
||||||
|
data+="\n";
|
||||||
|
file.write(data);
|
||||||
|
saved = true;
|
||||||
|
Bangle.buzz(500);
|
||||||
|
}
|
||||||
|
if (hrv){
|
||||||
|
if (!ui){
|
||||||
|
Bangle.setUI("leftright", ()=>{
|
||||||
|
resetHrv();
|
||||||
|
clear(30);
|
||||||
|
});
|
||||||
|
ui = true;
|
||||||
|
}
|
||||||
|
draw(30, hrv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHrv(){
|
||||||
|
hrvValues={};
|
||||||
|
startingTime=undefined;
|
||||||
|
currentSlot=0;
|
||||||
|
saved=false;
|
||||||
|
rrNumberOfValues = 0;
|
||||||
|
rrSquared = 0;
|
||||||
|
rrLastValue = undefined;
|
||||||
|
rrMax = undefined;
|
||||||
|
rrMin = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
|
||||||
|
if (Bangle.setBTHRMPower){
|
||||||
|
Bangle.on('BTHRM', onBtHrm);
|
||||||
|
Bangle.setBTHRMPower(1,'bthrv');
|
||||||
|
|
||||||
|
if (require('Storage').list(/bthrv.csv/).length == 0){
|
||||||
|
var file = require('Storage').open("bthrv.csv", "a");
|
||||||
|
var data = "Time";
|
||||||
|
for (var c of hrvSlots){
|
||||||
|
data+="," + c + "s";
|
||||||
|
}
|
||||||
|
data+=",RR_max,RR_min,Measurements";
|
||||||
|
data+="\n";
|
||||||
|
file.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||||
|
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
|
||||||
|
} else {
|
||||||
|
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||||
|
g.drawString("Missing BT HRM",g.getWidth()/2,g.getHeight()/2 - 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrv'));
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 670 B |
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"id": "bthrv",
|
||||||
|
"name": "Bluetooth Heart Rate variance calculator",
|
||||||
|
"shortName": "BT HRV",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Calculates HRV from a a BT HRM with interval data",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "health,bluetooth",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"bthrv.app.js","url":"app.js"},
|
||||||
|
{"name":"bthrv.recorder.js","url":"recorder.js"},
|
||||||
|
{"name":"bthrv.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
(function(recorders) {
|
||||||
|
recorders.bthrv = function() {
|
||||||
|
var lastGetValue = 0;
|
||||||
|
var lastUpdate = 0;
|
||||||
|
var rrHistory = [];
|
||||||
|
var hrv = "";
|
||||||
|
function onHRM(h) {
|
||||||
|
if(!h.rr) return;
|
||||||
|
if (lastUpdate + 3000 < Date.now()){
|
||||||
|
rrHistory = [];
|
||||||
|
}
|
||||||
|
rrHistory = rrHistory.concat(h.rr);
|
||||||
|
lastUpdate=Date.now();
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name : "BT HRV",
|
||||||
|
fields : ["BT HRV"],
|
||||||
|
getValues : () => {
|
||||||
|
if (lastGetValue + 10000 < Date.now()){
|
||||||
|
lastGetValue = Date.now();
|
||||||
|
|
||||||
|
if (rrHistory.length > 0){
|
||||||
|
if (rrHistory.length > 1){
|
||||||
|
var squaredSum = 0;
|
||||||
|
var last = rrHistory[0]
|
||||||
|
for (var i = 1; i < rrHistory.length; i++){
|
||||||
|
squaredSum += (last - rrHistory[i])*(last - rrHistory[i]);
|
||||||
|
last = rrHistory[i];
|
||||||
|
}
|
||||||
|
hrv = Math.sqrt(squaredSum/rrHistory.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = [hrv];
|
||||||
|
hrv = "";
|
||||||
|
rrHistory = [];
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
start : () => {
|
||||||
|
Bangle.on('BTHRM', onHRM);
|
||||||
|
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(1,"recorder");
|
||||||
|
},
|
||||||
|
stop : () => {
|
||||||
|
Bangle.removeListener('BTHRM', onHRM);
|
||||||
|
if (Bangle.setBTRHMPower) Bangle.setBTHRMPower(0,"recorder");
|
||||||
|
},
|
||||||
|
draw : (x,y) => g.setColor((rrHistory.length > 0)?"#00f":"#008").drawImage(atob("DAwBAAAACECECECEDGClacEEAAAA"),x,y)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -11,6 +11,11 @@
|
||||||
settings.recording = false;
|
settings.recording = false;
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSettings(settings) {
|
||||||
|
require("Storage").writeJSON("recorder.json", settings);
|
||||||
|
if (WIDGETS["recorder"]) WIDGETS["recorder"].reload();
|
||||||
|
}
|
||||||
|
|
||||||
function getRecorders() {
|
function getRecorders() {
|
||||||
var recorders = {
|
var recorders = {
|
||||||
|
|
@ -52,17 +57,18 @@
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
hrm:function() {
|
hrm:function() {
|
||||||
var bpm = "", bpmConfidence = "";
|
var bpm = "", bpmConfidence = "", src="";
|
||||||
function onHRM(h) {
|
function onHRM(h) {
|
||||||
bpmConfidence = h.confidence;
|
bpmConfidence = h.confidence;
|
||||||
bpm = h.bpm;
|
bpm = h.bpm;
|
||||||
|
srv = h.src;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name : "HR",
|
name : "HR",
|
||||||
fields : ["Heartrate", "Confidence"],
|
fields : ["Heartrate", "Confidence", "Source"],
|
||||||
getValues : () => {
|
getValues : () => {
|
||||||
var r = [bpm,bpmConfidence];
|
var r = [bpm,bpmConfidence,src];
|
||||||
bpm = ""; bpmConfidence = "";
|
bpm = ""; bpmConfidence = ""; src="";
|
||||||
return r;
|
return r;
|
||||||
},
|
},
|
||||||
start : () => {
|
start : () => {
|
||||||
|
|
@ -227,15 +233,32 @@
|
||||||
Bangle.drawWidgets(); // relayout all widgets
|
Bangle.drawWidgets(); // relayout all widgets
|
||||||
},setRecording:function(isOn) {
|
},setRecording:function(isOn) {
|
||||||
var settings = loadSettings();
|
var settings = loadSettings();
|
||||||
if (isOn && !settings.recording && require("Storage").list(settings.file).length)
|
if (isOn && !settings.recording && require("Storage").list(settings.file).length){
|
||||||
return E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:{Yes:"yes",No:"no"}}).then(selection=>{
|
var logfiles=require("Storage").list(/recorder.log.*/);
|
||||||
|
var maxNumber=0;
|
||||||
|
for (var c of logfiles){
|
||||||
|
maxNumber = Math.max(maxNumber, c.match(/\d+/)[0]);
|
||||||
|
}
|
||||||
|
var newFileName;
|
||||||
|
if (maxNumber < 99){
|
||||||
|
newFileName="recorder.log" + (maxNumber + 1) + ".csv";
|
||||||
|
updateSettings(settings);
|
||||||
|
}
|
||||||
|
var buttons={Yes:"yes",No:"no"};
|
||||||
|
if (newFileName) buttons["New"] = "new";
|
||||||
|
var prompt = E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{
|
||||||
if (selection=="no") return false; // just cancel
|
if (selection=="no") return false; // just cancel
|
||||||
if (selection=="yes") require("Storage").open(settings.file,"r").erase();
|
if (selection=="yes") require("Storage").open(settings.file,"r").erase();
|
||||||
// TODO: Add 'new file' option
|
if (selection=="new"){
|
||||||
|
settings.file = newFileName;
|
||||||
|
updateSettings(settings);
|
||||||
|
}
|
||||||
return WIDGETS["recorder"].setRecording(1);
|
return WIDGETS["recorder"].setRecording(1);
|
||||||
});
|
});
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
settings.recording = isOn;
|
settings.recording = isOn;
|
||||||
require("Storage").write("recorder.json", settings);
|
updateSettings(settings);
|
||||||
WIDGETS["recorder"].reload();
|
WIDGETS["recorder"].reload();
|
||||||
return Promise.resolve(settings.recording);
|
return Promise.resolve(settings.recording);
|
||||||
}/*,plotTrack:function(m) { // m=instance of openstmap module
|
}/*,plotTrack:function(m) { // m=instance of openstmap module
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue