commit
2110fc0b34
|
|
@ -29,3 +29,4 @@
|
|||
Use default boolean formatter in custom menu and directly apply config if useful
|
||||
Allow recording unmodified internal HR
|
||||
Better connection retry handling
|
||||
0.13: Less time used during boot if disabled
|
||||
|
|
|
|||
|
|
@ -1,633 +1 @@
|
|||
(function() {
|
||||
var settings = Object.assign(
|
||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON("bthrm.json", true) || {}
|
||||
);
|
||||
|
||||
var log = function(text, param){
|
||||
if (global.showStatusInfo)
|
||||
showStatusInfo(text);
|
||||
if (settings.debuglog){
|
||||
var logline = new Date().toISOString() + " - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
}
|
||||
};
|
||||
|
||||
log("Settings: ", settings);
|
||||
|
||||
if (settings.enabled){
|
||||
|
||||
var clearCache = function() {
|
||||
return require('Storage').erase("bthrm.cache.json");
|
||||
};
|
||||
|
||||
var getCache = function() {
|
||||
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||
if (settings.btid && settings.btid === cache.id) return cache;
|
||||
clearCache();
|
||||
return {};
|
||||
};
|
||||
|
||||
var addNotificationHandler = function(characteristic) {
|
||||
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
|
||||
var writeCache = function(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");
|
||||
}
|
||||
};
|
||||
|
||||
var characteristicsToCache = function(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);
|
||||
};
|
||||
|
||||
var characteristicsFromCache = function(device) {
|
||||
var service = { device : device }; // fake a BluetoothRemoteGATTService
|
||||
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;
|
||||
r.service = service;
|
||||
addNotificationHandler(r);
|
||||
log("Restored characteristic: ", r);
|
||||
restored.push(r);
|
||||
}
|
||||
return restored;
|
||||
};
|
||||
|
||||
log("Start");
|
||||
|
||||
var lastReceivedData={
|
||||
};
|
||||
|
||||
var supportedServices = [
|
||||
"0x180d", // Heart Rate
|
||||
"0x180f", // Battery
|
||||
];
|
||||
|
||||
var bpmTimeout;
|
||||
|
||||
var supportedCharacteristics = {
|
||||
"0x2a37": {
|
||||
//Heart rate measurement
|
||||
active: false,
|
||||
handler: function (dv){
|
||||
var flags = dv.getUint8(0);
|
||||
|
||||
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
|
||||
supportedCharacteristics["0x2a37"].active = bpm > 0;
|
||||
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
|
||||
if (supportedCharacteristics["0x2a37"].active) stopFallback();
|
||||
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||
bpmTimeout = setTimeout(()=>{
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
}, 3000);
|
||||
|
||||
var sensorContact;
|
||||
|
||||
if (flags & 2){
|
||||
sensorContact = !!(flags & 4);
|
||||
}
|
||||
|
||||
var idx = 2 + (flags&1);
|
||||
|
||||
var energyExpended;
|
||||
if (flags & 8){
|
||||
energyExpended = dv.getUint16(idx,1);
|
||||
idx += 2;
|
||||
}
|
||||
var interval;
|
||||
if (flags & 16) {
|
||||
interval = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 repEvent = {
|
||||
bpm: bpm,
|
||||
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
|
||||
src: "bthrm"
|
||||
};
|
||||
|
||||
log("Emitting HRM", repEvent);
|
||||
Bangle.emit("HRM_int", 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(dv){
|
||||
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
|
||||
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
|
||||
}
|
||||
},
|
||||
"0x2a19": {
|
||||
//Battery
|
||||
handler: function (dv){
|
||||
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
|
||||
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var device;
|
||||
var gatt;
|
||||
var characteristics = [];
|
||||
var blockInit = false;
|
||||
var currentRetryTimeout;
|
||||
var initialRetryTime = 40;
|
||||
var maxRetryTime = 60000;
|
||||
var retryTime = initialRetryTime;
|
||||
|
||||
var connectSettings = {
|
||||
minInterval: 7.5,
|
||||
maxInterval: 1500
|
||||
};
|
||||
|
||||
var waitingPromise = function(timeout) {
|
||||
return new Promise(function(resolve){
|
||||
log("Start waiting for " + timeout);
|
||||
setTimeout(()=>{
|
||||
log("Done waiting for " + timeout);
|
||||
resolve();
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.enabled){
|
||||
Bangle.isBTHRMActive = function (){
|
||||
return supportedCharacteristics["0x2a37"].active;
|
||||
};
|
||||
|
||||
Bangle.isBTHRMOn = function(){
|
||||
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
||||
};
|
||||
|
||||
Bangle.isBTHRMConnected = function(){
|
||||
return gatt && gatt.connected;
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
if (settings.enabled && !settings.replace){
|
||||
return Bangle.origIsHRMOn();
|
||||
} else if (settings.enabled && settings.replace){
|
||||
return Bangle.isBTHRMOn();
|
||||
}
|
||||
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
|
||||
};
|
||||
}
|
||||
|
||||
var clearRetryTimeout = function(resetTime) {
|
||||
if (currentRetryTimeout){
|
||||
log("Clearing timeout " + currentRetryTimeout);
|
||||
clearTimeout(currentRetryTimeout);
|
||||
currentRetryTimeout = undefined;
|
||||
}
|
||||
if (resetTime) {
|
||||
log("Resetting retry time");
|
||||
retryTime = initialRetryTime;
|
||||
}
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
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(clampedTime, 1.1);
|
||||
if (retryTime > maxRetryTime){
|
||||
retryTime = maxRetryTime;
|
||||
}
|
||||
} else {
|
||||
log("Already in retry...");
|
||||
}
|
||||
};
|
||||
|
||||
var buzzing = false;
|
||||
var onDisconnect = function(reason) {
|
||||
log("Disconnect: " + reason);
|
||||
log("GATT", gatt);
|
||||
log("Characteristics", characteristics);
|
||||
clearRetryTimeout(reason != "Connection Timeout");
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
blockInit = false;
|
||||
if (settings.warnDisconnect && !buzzing){
|
||||
buzzing = true;
|
||||
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
|
||||
}
|
||||
if (Bangle.isBTHRMOn()){
|
||||
retry();
|
||||
}
|
||||
};
|
||||
|
||||
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", 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", newCharacteristic);
|
||||
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
|
||||
if (settings.gracePeriodNotification > 0){
|
||||
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
|
||||
startPromise = startPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodNotification);
|
||||
});
|
||||
}
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
return result.then(()=>log("Handled characteristic", newCharacteristic));
|
||||
};
|
||||
|
||||
var attachCharacteristicPromise = function(promise, characteristic) {
|
||||
return promise.then(()=>{
|
||||
log("Handling characteristic:", characteristic);
|
||||
return createCharacteristicPromise(characteristic);
|
||||
});
|
||||
};
|
||||
|
||||
var createCharacteristicsPromise = function(newCharacteristics) {
|
||||
log("Create characteristics promis ", 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"));
|
||||
};
|
||||
|
||||
var createServicePromise = function(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));
|
||||
};
|
||||
|
||||
var attachServicePromise = function(promise, service) {
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
};
|
||||
|
||||
var initBt = function () {
|
||||
log("initBt with blockInit: " + blockInit);
|
||||
if (blockInit){
|
||||
retry();
|
||||
return;
|
||||
}
|
||||
|
||||
blockInit = true;
|
||||
|
||||
var promise;
|
||||
var filters;
|
||||
|
||||
if (!device){
|
||||
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, 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 cachedId = getCache().id;
|
||||
if (device.id !== cachedId){
|
||||
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
|
||||
clearCache();
|
||||
}
|
||||
var newCache = getCache();
|
||||
newCache.id = device.id;
|
||||
writeCache(newCache);
|
||||
gatt = device.gatt;
|
||||
}
|
||||
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
promise = promise.then((gatt)=>{
|
||||
if (!gatt.connected){
|
||||
log("Connecting...");
|
||||
var connectPromise = gatt.connect(connectSettings).then(function() {
|
||||
log("Connected.");
|
||||
});
|
||||
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(() => {
|
||||
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){
|
||||
characteristics = characteristicsFromCache(device);
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
var characteristicsPromise = Promise.resolve();
|
||||
if (characteristics.length === 0){
|
||||
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
});
|
||||
|
||||
characteristicsPromise = characteristicsPromise.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){
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
}
|
||||
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
return promise.then(()=>{
|
||||
log("Connection established, waiting for notifications");
|
||||
characteristicsToCache(characteristics);
|
||||
clearRetryTimeout(true);
|
||||
}).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) {
|
||||
switchFallback();
|
||||
if (!Bangle.isBTHRMConnected()) initBt();
|
||||
} else { // not on
|
||||
log("Power off for " + app);
|
||||
clearRetryTimeout(true);
|
||||
if (gatt) {
|
||||
if (gatt.connected){
|
||||
log("Disconnect with gatt", gatt);
|
||||
try{
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect promise", e);
|
||||
});
|
||||
} catch (e){
|
||||
log("Error during disconnect attempt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.on("HRM", (e) => {
|
||||
e.modified = true;
|
||||
Bangle.emit("HRM_int", e);
|
||||
});
|
||||
|
||||
Bangle.origOn = Bangle.on;
|
||||
Bangle.on = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origOn("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.origRemoveListener = Bangle.removeListener;
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origRemoveListener("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Bangle.origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
||||
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){
|
||||
Bangle.origSetHRMPower(isOn, app);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var fallbackActive = false;
|
||||
var inSwitch = false;
|
||||
|
||||
var stopFallback = function(){
|
||||
if (fallbackActive){
|
||||
Bangle.origSetHRMPower(0, "bthrm_fallback");
|
||||
fallbackActive = false;
|
||||
log("Fallback to HRM disabled");
|
||||
}
|
||||
};
|
||||
|
||||
var startFallback = function(){
|
||||
if (!fallbackActive && settings.allowFallback) {
|
||||
fallbackActive = true;
|
||||
Bangle.origSetHRMPower(1, "bthrm_fallback");
|
||||
log("Fallback to HRM enabled");
|
||||
}
|
||||
};
|
||||
|
||||
var switchFallback = function() {
|
||||
log("Check falling back to HRM");
|
||||
if (!inSwitch){
|
||||
inSwitch = true;
|
||||
if (Bangle.isBTHRMActive()){
|
||||
stopFallback();
|
||||
} else {
|
||||
startFallback();
|
||||
}
|
||||
}
|
||||
inSwitch = false;
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
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);
|
||||
Bangle.origSetHRMPower(0, app);
|
||||
Bangle.setBTHRMPower(1, app);
|
||||
if (Bangle._PWR.HRM===undefined) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E.on("kill", ()=>{
|
||||
if (gatt && gatt.connected){
|
||||
log("Got killed, trying to disconnect");
|
||||
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
if ((require('Storage').readJSON("bthrm.json", true) || {}).enabled != false) require("bthrm").enable();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,633 @@
|
|||
exports.enable = () => {
|
||||
var settings = Object.assign(
|
||||
require('Storage').readJSON("bthrm.default.json", true) || {},
|
||||
require('Storage').readJSON("bthrm.json", true) || {}
|
||||
);
|
||||
|
||||
var log = function(text, param){
|
||||
if (global.showStatusInfo)
|
||||
showStatusInfo(text);
|
||||
if (settings.debuglog){
|
||||
var logline = new Date().toISOString() + " - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
}
|
||||
};
|
||||
|
||||
log("Settings: ", settings);
|
||||
|
||||
if (settings.enabled){
|
||||
|
||||
var clearCache = function() {
|
||||
return require('Storage').erase("bthrm.cache.json");
|
||||
};
|
||||
|
||||
var getCache = function() {
|
||||
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||
if (settings.btid && settings.btid === cache.id) return cache;
|
||||
clearCache();
|
||||
return {};
|
||||
};
|
||||
|
||||
var addNotificationHandler = function(characteristic) {
|
||||
log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/);
|
||||
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
|
||||
};
|
||||
|
||||
var writeCache = function(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");
|
||||
}
|
||||
};
|
||||
|
||||
var characteristicsToCache = function(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);
|
||||
};
|
||||
|
||||
var characteristicsFromCache = function(device) {
|
||||
var service = { device : device }; // fake a BluetoothRemoteGATTService
|
||||
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;
|
||||
r.service = service;
|
||||
addNotificationHandler(r);
|
||||
log("Restored characteristic: ", r);
|
||||
restored.push(r);
|
||||
}
|
||||
return restored;
|
||||
};
|
||||
|
||||
log("Start");
|
||||
|
||||
var lastReceivedData={
|
||||
};
|
||||
|
||||
var supportedServices = [
|
||||
"0x180d", // Heart Rate
|
||||
"0x180f", // Battery
|
||||
];
|
||||
|
||||
var bpmTimeout;
|
||||
|
||||
var supportedCharacteristics = {
|
||||
"0x2a37": {
|
||||
//Heart rate measurement
|
||||
active: false,
|
||||
handler: function (dv){
|
||||
var flags = dv.getUint8(0);
|
||||
|
||||
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
|
||||
supportedCharacteristics["0x2a37"].active = bpm > 0;
|
||||
log("BTHRM BPM " + supportedCharacteristics["0x2a37"].active);
|
||||
if (supportedCharacteristics["0x2a37"].active) stopFallback();
|
||||
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||
bpmTimeout = setTimeout(()=>{
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
}, 3000);
|
||||
|
||||
var sensorContact;
|
||||
|
||||
if (flags & 2){
|
||||
sensorContact = !!(flags & 4);
|
||||
}
|
||||
|
||||
var idx = 2 + (flags&1);
|
||||
|
||||
var energyExpended;
|
||||
if (flags & 8){
|
||||
energyExpended = dv.getUint16(idx,1);
|
||||
idx += 2;
|
||||
}
|
||||
var interval;
|
||||
if (flags & 16) {
|
||||
interval = [];
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 repEvent = {
|
||||
bpm: bpm,
|
||||
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
|
||||
src: "bthrm"
|
||||
};
|
||||
|
||||
log("Emitting HRM", repEvent);
|
||||
Bangle.emit("HRM_int", 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(dv){
|
||||
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
|
||||
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
|
||||
}
|
||||
},
|
||||
"0x2a19": {
|
||||
//Battery
|
||||
handler: function (dv){
|
||||
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
|
||||
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var device;
|
||||
var gatt;
|
||||
var characteristics = [];
|
||||
var blockInit = false;
|
||||
var currentRetryTimeout;
|
||||
var initialRetryTime = 40;
|
||||
var maxRetryTime = 60000;
|
||||
var retryTime = initialRetryTime;
|
||||
|
||||
var connectSettings = {
|
||||
minInterval: 7.5,
|
||||
maxInterval: 1500
|
||||
};
|
||||
|
||||
var waitingPromise = function(timeout) {
|
||||
return new Promise(function(resolve){
|
||||
log("Start waiting for " + timeout);
|
||||
setTimeout(()=>{
|
||||
log("Done waiting for " + timeout);
|
||||
resolve();
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.enabled){
|
||||
Bangle.isBTHRMActive = function (){
|
||||
return supportedCharacteristics["0x2a37"].active;
|
||||
};
|
||||
|
||||
Bangle.isBTHRMOn = function(){
|
||||
return (Bangle._PWR && Bangle._PWR.BTHRM && Bangle._PWR.BTHRM.length > 0);
|
||||
};
|
||||
|
||||
Bangle.isBTHRMConnected = function(){
|
||||
return gatt && gatt.connected;
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
if (settings.enabled && !settings.replace){
|
||||
return Bangle.origIsHRMOn();
|
||||
} else if (settings.enabled && settings.replace){
|
||||
return Bangle.isBTHRMOn();
|
||||
}
|
||||
return Bangle.origIsHRMOn() || Bangle.isBTHRMOn();
|
||||
};
|
||||
}
|
||||
|
||||
var clearRetryTimeout = function(resetTime) {
|
||||
if (currentRetryTimeout){
|
||||
log("Clearing timeout " + currentRetryTimeout);
|
||||
clearTimeout(currentRetryTimeout);
|
||||
currentRetryTimeout = undefined;
|
||||
}
|
||||
if (resetTime) {
|
||||
log("Resetting retry time");
|
||||
retryTime = initialRetryTime;
|
||||
}
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
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(clampedTime, 1.1);
|
||||
if (retryTime > maxRetryTime){
|
||||
retryTime = maxRetryTime;
|
||||
}
|
||||
} else {
|
||||
log("Already in retry...");
|
||||
}
|
||||
};
|
||||
|
||||
var buzzing = false;
|
||||
var onDisconnect = function(reason) {
|
||||
log("Disconnect: " + reason);
|
||||
log("GATT", gatt);
|
||||
log("Characteristics", characteristics);
|
||||
clearRetryTimeout(reason != "Connection Timeout");
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
blockInit = false;
|
||||
if (settings.warnDisconnect && !buzzing){
|
||||
buzzing = true;
|
||||
Bangle.buzz(500,0.3).then(()=>waitingPromise(4500)).then(()=>{buzzing = false;});
|
||||
}
|
||||
if (Bangle.isBTHRMOn()){
|
||||
retry();
|
||||
}
|
||||
};
|
||||
|
||||
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", 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", newCharacteristic);
|
||||
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
|
||||
if (settings.gracePeriodNotification > 0){
|
||||
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
|
||||
startPromise = startPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodNotification);
|
||||
});
|
||||
}
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
return result.then(()=>log("Handled characteristic", newCharacteristic));
|
||||
};
|
||||
|
||||
var attachCharacteristicPromise = function(promise, characteristic) {
|
||||
return promise.then(()=>{
|
||||
log("Handling characteristic:", characteristic);
|
||||
return createCharacteristicPromise(characteristic);
|
||||
});
|
||||
};
|
||||
|
||||
var createCharacteristicsPromise = function(newCharacteristics) {
|
||||
log("Create characteristics promis ", 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"));
|
||||
};
|
||||
|
||||
var createServicePromise = function(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));
|
||||
};
|
||||
|
||||
var attachServicePromise = function(promise, service) {
|
||||
return promise.then(()=>createServicePromise(service));
|
||||
};
|
||||
|
||||
var initBt = function () {
|
||||
log("initBt with blockInit: " + blockInit);
|
||||
if (blockInit){
|
||||
retry();
|
||||
return;
|
||||
}
|
||||
|
||||
blockInit = true;
|
||||
|
||||
var promise;
|
||||
var filters;
|
||||
|
||||
if (!device){
|
||||
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, 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 cachedId = getCache().id;
|
||||
if (device.id !== cachedId){
|
||||
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
|
||||
clearCache();
|
||||
}
|
||||
var newCache = getCache();
|
||||
newCache.id = device.id;
|
||||
writeCache(newCache);
|
||||
gatt = device.gatt;
|
||||
}
|
||||
|
||||
return Promise.resolve(gatt);
|
||||
});
|
||||
|
||||
promise = promise.then((gatt)=>{
|
||||
if (!gatt.connected){
|
||||
log("Connecting...");
|
||||
var connectPromise = gatt.connect(connectSettings).then(function() {
|
||||
log("Connected.");
|
||||
});
|
||||
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(() => {
|
||||
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){
|
||||
characteristics = characteristicsFromCache(device);
|
||||
}
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
var characteristicsPromise = Promise.resolve();
|
||||
if (characteristics.length === 0){
|
||||
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
});
|
||||
|
||||
characteristicsPromise = characteristicsPromise.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){
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
}
|
||||
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
return promise.then(()=>{
|
||||
log("Connection established, waiting for notifications");
|
||||
characteristicsToCache(characteristics);
|
||||
clearRetryTimeout(true);
|
||||
}).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) {
|
||||
switchFallback();
|
||||
if (!Bangle.isBTHRMConnected()) initBt();
|
||||
} else { // not on
|
||||
log("Power off for " + app);
|
||||
clearRetryTimeout(true);
|
||||
if (gatt) {
|
||||
if (gatt.connected){
|
||||
log("Disconnect with gatt", gatt);
|
||||
try{
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect promise", e);
|
||||
});
|
||||
} catch (e){
|
||||
log("Error during disconnect attempt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
Bangle.on("HRM", (e) => {
|
||||
e.modified = true;
|
||||
Bangle.emit("HRM_int", e);
|
||||
});
|
||||
|
||||
Bangle.origOn = Bangle.on;
|
||||
Bangle.on = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origOn("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.origRemoveListener = Bangle.removeListener;
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origRemoveListener("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Bangle.origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
||||
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){
|
||||
Bangle.origSetHRMPower(isOn, app);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var fallbackActive = false;
|
||||
var inSwitch = false;
|
||||
|
||||
var stopFallback = function(){
|
||||
if (fallbackActive){
|
||||
Bangle.origSetHRMPower(0, "bthrm_fallback");
|
||||
fallbackActive = false;
|
||||
log("Fallback to HRM disabled");
|
||||
}
|
||||
};
|
||||
|
||||
var startFallback = function(){
|
||||
if (!fallbackActive && settings.allowFallback) {
|
||||
fallbackActive = true;
|
||||
Bangle.origSetHRMPower(1, "bthrm_fallback");
|
||||
log("Fallback to HRM enabled");
|
||||
}
|
||||
};
|
||||
|
||||
var switchFallback = function() {
|
||||
log("Check falling back to HRM");
|
||||
if (!inSwitch){
|
||||
inSwitch = true;
|
||||
if (Bangle.isBTHRMActive()){
|
||||
stopFallback();
|
||||
} else {
|
||||
startFallback();
|
||||
}
|
||||
}
|
||||
inSwitch = false;
|
||||
};
|
||||
|
||||
if (settings.replace){
|
||||
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);
|
||||
Bangle.origSetHRMPower(0, app);
|
||||
Bangle.setBTHRMPower(1, app);
|
||||
if (Bangle._PWR.HRM===undefined) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E.on("kill", ()=>{
|
||||
if (gatt && gatt.connected){
|
||||
log("Got killed, trying to disconnect");
|
||||
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.12",
|
||||
"version": "0.13",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
{"name":"bthrm.0.boot.js","url":"boot.js"},
|
||||
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"bthrm.settings.js","url":"settings.js"},
|
||||
{"name":"bthrm","url":"lib.js"},
|
||||
{"name":"bthrm.default.json","url":"default.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Do first update request 5s after boot to boot up faster
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
(function() {
|
||||
{
|
||||
let waiting = false;
|
||||
let settings = require("Storage").readJSON("owmweather.json", 1) || {
|
||||
enabled: false
|
||||
};
|
||||
|
||||
function completion(){
|
||||
let completion = function(){
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
|
||||
if (settings.enabled) {
|
||||
let weather = require("Storage").readJSON('weather.json') || {};
|
||||
let lastUpdate;
|
||||
if (weather && weather.weather && weather.weather.time) lastUpdate = weather.weather.time;
|
||||
|
||||
if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){
|
||||
if (!waiting){
|
||||
waiting = true;
|
||||
require("owmweather").pull(completion);
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!waiting){
|
||||
waiting = true;
|
||||
require("owmweather").pull(completion);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
setInterval(() => {
|
||||
if (!waiting && NRF.getSecurityStatus().connected){
|
||||
|
|
@ -25,4 +28,4 @@
|
|||
}
|
||||
}, settings.refresh * 1000 * 60);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "owmweather",
|
||||
"name": "OpenWeatherMap weather provider",
|
||||
"shortName":"OWM Weather",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Pulls weather from OpenWeatherMap (OWM) API",
|
||||
"icon": "app.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: Initial version
|
||||
0.02: Moved settings from launcher to settings->apps menu
|
||||
0.03: Better performance by not scanning on every boot
|
||||
0.04: Better performace by not scanning on boot at all
|
||||
|
|
|
|||
|
|
@ -1,29 +1,24 @@
|
|||
{
|
||||
let settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
|
||||
let settings = require("Storage").readJSON("quicklaunch.json", true) || {};
|
||||
const storage = require("Storage");
|
||||
|
||||
let hash = require("Storage").hash(/\.info/);
|
||||
if (settings.hash!=hash) {
|
||||
//apps changed, rescan and remove no longer existing apps
|
||||
let apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{src:a.src};});
|
||||
|
||||
for (let c of ["leftapp","rightapp","upapp","downapp","tapapp"]){
|
||||
if (!settings[c]) settings[c] = {"name":"(none)"};
|
||||
if (!require("Storage").read(settings[c].src)) settings[c] = {"name":"(none)"};
|
||||
}
|
||||
settings.hash = hash;
|
||||
require("Storage").write("quicklaunch.json",settings);
|
||||
}
|
||||
let reset = function(name){
|
||||
if (!settings[name]) settings[name] = {"name":"(none)"};
|
||||
if (!require("Storage").read(settings[name].src)) settings[name] = {"name":"(none)"};
|
||||
storage.write("quicklaunch.json", settings);
|
||||
};
|
||||
|
||||
Bangle.on("touch", () => {
|
||||
if (!Bangle.CLOCK) return;
|
||||
if (settings.tapapp.src) load(settings.tapapp.src);
|
||||
if (settings.tapapp.src){ if (!storage.read(settings.tapapp.src)) reset("tapapp"); else load(settings.tapapp.src); }
|
||||
});
|
||||
|
||||
Bangle.on("swipe", (lr,ud) => {
|
||||
if (!Bangle.CLOCK) return;
|
||||
|
||||
if (lr == -1 && settings.leftapp.src) load(settings.leftapp.src);
|
||||
if (lr == 1 && settings.rightapp.src) load(settings.rightapp.src);
|
||||
if (ud == 1 && settings.upapp.src) load(settings.upapp.src);
|
||||
if (ud == -1 && settings.downapp.src) load(settings.downapp.src);
|
||||
if (lr == -1 && settings.leftapp && settings.leftapp.src){ if (!storage.read(settings.leftapp.src)) reset("leftapp"); else load(settings.leftapp.src); }
|
||||
if (lr == 1 && settings.rightapp && settings.rightapp.src){ if (!storage.read(settings.rightapp.src)) reset("rightapp"); else load(settings.rightapp.src); }
|
||||
if (ud == -1 && settings.upapp && settings.upapp.src){ if (!storage.read(settings.upapp.src)) reset("upapp"); else load(settings.upapp.src); }
|
||||
if (ud == 1 && settings.downapp && settings.downapp.src){ if (!storage.read(settings.downapp.src)) reset("downapp"); else load(settings.downapp.src); }
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "quicklaunch",
|
||||
"name": "Quick Launch",
|
||||
"icon": "app.png",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.",
|
||||
"type": "bootloader",
|
||||
"tags": "tools, system",
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Less time used during boot if disabled
|
||||
|
|
|
|||
|
|
@ -1,351 +1 @@
|
|||
(function() {
|
||||
var settings = Object.assign(
|
||||
require('Storage').readJSON("sensortools.default.json", true) || {},
|
||||
require('Storage').readJSON("sensortools.json", true) || {}
|
||||
);
|
||||
|
||||
var log = function(text, param) {
|
||||
var logline = new Date().toISOString() + " - " + "Sensortools - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
};
|
||||
|
||||
if (settings.enabled) {
|
||||
|
||||
log("Enabled");
|
||||
const POWER_DELAY = 10000;
|
||||
|
||||
var onEvents = [];
|
||||
|
||||
Bangle.sensortoolsOrigOn = Bangle.on;
|
||||
Bangle.sensortoolsOrigEmit = Bangle.emit;
|
||||
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
|
||||
|
||||
Bangle.on = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Redirecting listener for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigOn(name + "_mod", callback);
|
||||
Bangle.sensortoolsOrigOn(name, (e) => {
|
||||
log("Redirected event for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
|
||||
});
|
||||
} else {
|
||||
log("Pass through on call for", name, callback);
|
||||
Bangle.sensortoolsOrigOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Removing augmented listener for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
|
||||
} else {
|
||||
log("Pass through remove listener for", name);
|
||||
Bangle.sensortoolsOrigRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.emit = function(name, event) {
|
||||
if (onEvents[name]) {
|
||||
log("Augmenting emit call for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", event);
|
||||
} else {
|
||||
log("Pass through emit call for", name);
|
||||
Bangle.sensortoolsOrigEmit(name, event);
|
||||
}
|
||||
};
|
||||
|
||||
var createPowerFunction = function(type, name, origPower) {
|
||||
return function(isOn, app) {
|
||||
if (type == "nop") {
|
||||
return true;
|
||||
}else if (type == "delay") {
|
||||
setTimeout(() => {
|
||||
origPower(isOn, app);
|
||||
}, POWER_DELAY);
|
||||
} else if (type == "on") {
|
||||
origPower(1, "sensortools_force_on");
|
||||
} else if (type == "passthrough"){
|
||||
origPower(isOn, "app");
|
||||
} else if (type == "emulate"){
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR[name]) Bangle._PWR[name] = [];
|
||||
if (!app) app="?";
|
||||
if (isOn) {
|
||||
Bangle._PWR[name].push(app);
|
||||
return true;
|
||||
} else {
|
||||
Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (settings.hrm && settings.hrm.enabled) {
|
||||
log("HRM", settings.hrm);
|
||||
if (settings.hrm.power) {
|
||||
log("HRM power");
|
||||
Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower;
|
||||
Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower);
|
||||
}
|
||||
if (settings.hrm.mode == "modify") {
|
||||
if (settings.hrm.name == "bpmtrippled") {
|
||||
onEvents.HRM = (e) => {
|
||||
return {
|
||||
bpm: e.bpm * 3
|
||||
};
|
||||
};
|
||||
}
|
||||
} else if (settings.hrm.mode == "emulate") {
|
||||
if (settings.hrm.name == "sin") {
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000));
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (settings.gps && settings.gps.enabled) {
|
||||
log("GPS", settings.gps);
|
||||
let modGps = function(dataProvider) {
|
||||
Bangle.getGPSFix = dataProvider;
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit("GPS", dataProvider());
|
||||
}, 1000);
|
||||
};
|
||||
if (settings.gps.power) {
|
||||
Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower;
|
||||
Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower);
|
||||
}
|
||||
if (settings.gps.mode == "emulate") {
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
var d = a*180/Math.PI;
|
||||
return (d+360)%360;
|
||||
}
|
||||
|
||||
function bearing(a,b){
|
||||
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
|
||||
var delta = radians(b.lon-a.lon);
|
||||
var alat = radians(a.lat);
|
||||
var blat = radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
|
||||
function interpolate(a,b,progress){
|
||||
return {
|
||||
lat: a.lat * progress + b.lat * (1-progress),
|
||||
lon: a.lon * progress + b.lon * (1-progress),
|
||||
ele: a.ele * progress + b.ele * (1-progress)
|
||||
}
|
||||
}
|
||||
|
||||
function getSquareRoute(){
|
||||
return [
|
||||
{lat:"47.2577411",lon:"11.9927442",ele:2273},
|
||||
{lat:"47.266761",lon:"11.9926673",ele:2166},
|
||||
{lat:"47.2667605",lon:"12.0059511",ele:2245},
|
||||
{lat:"47.2577516",lon:"12.0059925",ele:1994}
|
||||
];
|
||||
}
|
||||
function getSquareRouteFuzzy(){
|
||||
return [
|
||||
{lat:"47.2578455",lon:"11.9929891",ele:2265},
|
||||
{lat:"47.258592",lon:"11.9923341",ele:2256},
|
||||
{lat:"47.2594506",lon:"11.9927412",ele:2230},
|
||||
{lat:"47.2603323",lon:"11.9924949",ele:2219},
|
||||
{lat:"47.2612056",lon:"11.9928175",ele:2199},
|
||||
{lat:"47.2621002",lon:"11.9929817",ele:2182},
|
||||
{lat:"47.2629025",lon:"11.9923915",ele:2189},
|
||||
{lat:"47.2637828",lon:"11.9926486",ele:2180},
|
||||
{lat:"47.2646733",lon:"11.9928167",ele:2191},
|
||||
{lat:"47.2655617",lon:"11.9930357",ele:2185},
|
||||
{lat:"47.2662862",lon:"11.992252",ele:2186},
|
||||
{lat:"47.2669305",lon:"11.993173",ele:2166},
|
||||
{lat:"47.266666",lon:"11.9944419",ele:2171},
|
||||
{lat:"47.2667579",lon:"11.99576",ele:2194},
|
||||
{lat:"47.2669409",lon:"11.9970579",ele:2207},
|
||||
{lat:"47.2666562",lon:"11.9983128",ele:2212},
|
||||
{lat:"47.2666027",lon:"11.9996335",ele:2262},
|
||||
{lat:"47.2667245",lon:"12.0009395",ele:2278},
|
||||
{lat:"47.2668457",lon:"12.002256",ele:2297},
|
||||
{lat:"47.2666126",lon:"12.0035373",ele:2303},
|
||||
{lat:"47.2664554",lon:"12.004841",ele:2251},
|
||||
{lat:"47.2669461",lon:"12.005948",ele:2245},
|
||||
{lat:"47.2660877",lon:"12.006323",ele:2195},
|
||||
{lat:"47.2652729",lon:"12.0057552",ele:2163},
|
||||
{lat:"47.2643926",lon:"12.0060123",ele:2131},
|
||||
{lat:"47.2634978",lon:"12.0058302",ele:2095},
|
||||
{lat:"47.2626129",lon:"12.0060759",ele:2066},
|
||||
{lat:"47.2617325",lon:"12.0058188",ele:2037},
|
||||
{lat:"47.2608668",lon:"12.0061784",ele:1993},
|
||||
{lat:"47.2600155",lon:"12.0057392",ele:1967},
|
||||
{lat:"47.2591203",lon:"12.0058233",ele:1949},
|
||||
{lat:"47.2582307",lon:"12.0059718",ele:1972},
|
||||
{lat:"47.2578014",lon:"12.004804",ele:2011},
|
||||
{lat:"47.2577232",lon:"12.0034834",ele:2044},
|
||||
{lat:"47.257745",lon:"12.0021656",ele:2061},
|
||||
{lat:"47.2578682",lon:"12.0008597",ele:2065},
|
||||
{lat:"47.2577082",lon:"11.9995526",ele:2071},
|
||||
{lat:"47.2575917",lon:"11.9982348",ele:2102},
|
||||
{lat:"47.2577401",lon:"11.996924",ele:2147},
|
||||
{lat:"47.257715",lon:"11.9956061",ele:2197},
|
||||
{lat:"47.2578996",lon:"11.9943081",ele:2228}
|
||||
];
|
||||
}
|
||||
|
||||
if (settings.gps.name == "staticfix") {
|
||||
modGps(() => { return {
|
||||
"lat": 52,
|
||||
"lon": 8,
|
||||
"alt": 100,
|
||||
"speed": 10,
|
||||
"course": 12,
|
||||
"time": Date.now(),
|
||||
"satellites": 7,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
} else if (settings.gps.name.includes("route")) {
|
||||
let route;
|
||||
let interpSteps;
|
||||
if (settings.gps.name == "routeFuzzy"){
|
||||
route = getSquareRouteFuzzy();
|
||||
interpSteps = 5;
|
||||
} else {
|
||||
route = getSquareRoute();
|
||||
interpSteps = 50;
|
||||
}
|
||||
|
||||
let step = 0;
|
||||
let routeIndex = 0;
|
||||
modGps(() => {
|
||||
let newIndex = (routeIndex + 1)%route.length;
|
||||
|
||||
let result = {
|
||||
"speed": Math.random() * 3 + 2,
|
||||
"time": Date.now(),
|
||||
"satellites": Math.floor(Math.random()*5)+3,
|
||||
"fix": 1,
|
||||
"hdop": Math.floor(Math.random(30)+1)
|
||||
};
|
||||
|
||||
let oldPos = route[routeIndex];
|
||||
if (step != 0){
|
||||
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
|
||||
}
|
||||
let newPos = route[newIndex];
|
||||
if (step < interpSteps - 1){
|
||||
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
|
||||
}
|
||||
|
||||
if (step == interpSteps - 1){
|
||||
let followingIndex = (routeIndex + 2)%route.length;
|
||||
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
|
||||
}
|
||||
|
||||
result.lat = oldPos.lat;
|
||||
result.lon = oldPos.lon;
|
||||
result.alt = oldPos.ele;
|
||||
|
||||
result.course = bearing(oldPos,newPos);
|
||||
|
||||
step++;
|
||||
if (step == interpSteps){
|
||||
routeIndex = (routeIndex + 1) % route.length;
|
||||
step = 0;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else if (settings.gps.name == "nofix") {
|
||||
modGps(() => { return {
|
||||
"lat": NaN,
|
||||
"lon": NaN,
|
||||
"alt": NaN,
|
||||
"speed": NaN,
|
||||
"course": NaN,
|
||||
"time": Date.now(),
|
||||
"satellites": 2,
|
||||
"fix": 0,
|
||||
"hdop": NaN
|
||||
};});
|
||||
} else if (settings.gps.name == "changingfix") {
|
||||
let currentSpeed=1;
|
||||
let currentLat=20;
|
||||
let currentLon=10;
|
||||
let currentCourse=10;
|
||||
let currentAlt=-100;
|
||||
let currentSats=5;
|
||||
modGps(() => {
|
||||
currentLat += 0.1;
|
||||
if (currentLat > 50) currentLat = 20;
|
||||
currentLon += 0.1;
|
||||
if (currentLon > 20) currentLon = 10;
|
||||
currentSpeed *= 10;
|
||||
if (currentSpeed > 1000) currentSpeed = 1;
|
||||
currentCourse += 12;
|
||||
if (currentCourse > 360) currentCourse -= 360;
|
||||
currentSats += 1;
|
||||
if (currentSats > 10) currentSats = 5;
|
||||
currentAlt *= 10;
|
||||
if (currentAlt > 1000) currentAlt = -100;
|
||||
return {
|
||||
"lat": currentLat,
|
||||
"lon": currentLon,
|
||||
"alt": currentAlt,
|
||||
"speed": currentSpeed,
|
||||
"course": currentCourse,
|
||||
"time": Date.now(),
|
||||
"satellites": currentSats,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.mag && settings.mag.enabled) {
|
||||
log("MAG", settings.mag);
|
||||
let modMag = function(data) {
|
||||
setInterval(() => {
|
||||
Bangle.getCompass = data;
|
||||
Bangle.sensortoolsOrigEmit("mag", data());
|
||||
}, 100);
|
||||
};
|
||||
if (settings.mag.power) {
|
||||
Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower;
|
||||
Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower);
|
||||
}
|
||||
if (settings.mag.mode == "emulate") {
|
||||
if (settings.mag.name == "static") {
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: 0
|
||||
};});
|
||||
} else if (settings.mag.name == "rotate"){
|
||||
let last = 0;
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: last = (last+1)%360,
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
if ((require('Storage').readJSON("sensortools.json", true) || {}).enabled) require("sensortools").enable();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,348 @@
|
|||
exports.enable = () => {
|
||||
let settings = Object.assign(
|
||||
require('Storage').readJSON("sensortools.default.json", true) || {},
|
||||
require('Storage').readJSON("sensortools.json", true) || {}
|
||||
);
|
||||
|
||||
let log = function(text, param) {
|
||||
let logline = new Date().toISOString() + " - " + "Sensortools - " + text;
|
||||
if (param) logline += ": " + JSON.stringify(param);
|
||||
print(logline);
|
||||
};
|
||||
|
||||
log("Enabled");
|
||||
const POWER_DELAY = 10000;
|
||||
|
||||
let onEvents = [];
|
||||
|
||||
Bangle.sensortoolsOrigOn = Bangle.on;
|
||||
Bangle.sensortoolsOrigEmit = Bangle.emit;
|
||||
Bangle.sensortoolsOrigRemoveListener = Bangle.removeListener;
|
||||
|
||||
Bangle.on = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Redirecting listener for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigOn(name + "_mod", callback);
|
||||
Bangle.sensortoolsOrigOn(name, (e) => {
|
||||
log("Redirected event for", name, "to", name + "_mod");
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", onEvents[name](e));
|
||||
});
|
||||
} else {
|
||||
log("Pass through on call for", name, callback);
|
||||
Bangle.sensortoolsOrigOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (onEvents[name]) {
|
||||
log("Removing augmented listener for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigRemoveListener(name + "_mod", callback);
|
||||
} else {
|
||||
log("Pass through remove listener for", name);
|
||||
Bangle.sensortoolsOrigRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.emit = function(name, event) {
|
||||
if (onEvents[name]) {
|
||||
log("Augmenting emit call for", name, onEvents[name]);
|
||||
Bangle.sensortoolsOrigEmit(name + "_mod", event);
|
||||
} else {
|
||||
log("Pass through emit call for", name);
|
||||
Bangle.sensortoolsOrigEmit(name, event);
|
||||
}
|
||||
};
|
||||
|
||||
let createPowerFunction = function(type, name, origPower) {
|
||||
return function(isOn, app) {
|
||||
if (type == "nop") {
|
||||
return true;
|
||||
}else if (type == "delay") {
|
||||
setTimeout(() => {
|
||||
origPower(isOn, app);
|
||||
}, POWER_DELAY);
|
||||
} else if (type == "on") {
|
||||
origPower(1, "sensortools_force_on");
|
||||
} else if (type == "passthrough"){
|
||||
origPower(isOn, "app");
|
||||
} else if (type == "emulate"){
|
||||
if (!Bangle._PWR) Bangle._PWR={};
|
||||
if (!Bangle._PWR[name]) Bangle._PWR[name] = [];
|
||||
if (!app) app="?";
|
||||
if (isOn) {
|
||||
Bangle._PWR[name].push(app);
|
||||
return true;
|
||||
} else {
|
||||
Bangle._PWR[name] = Bangle._PWR[name].filter((v)=>{return v == app;});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (settings.hrm && settings.hrm.enabled) {
|
||||
log("HRM", settings.hrm);
|
||||
if (settings.hrm.power) {
|
||||
log("HRM power");
|
||||
Bangle.sensortoolsOrigSetHRMPower = Bangle.setHRMPower;
|
||||
Bangle.setHRMPower = createPowerFunction(settings.hrm.power, "HRM", Bangle.sensortoolsOrigSetHRMPower);
|
||||
}
|
||||
if (settings.hrm.mode == "modify") {
|
||||
if (settings.hrm.name == "bpmtrippled") {
|
||||
onEvents.HRM = (e) => {
|
||||
return {
|
||||
bpm: e.bpm * 3
|
||||
};
|
||||
};
|
||||
}
|
||||
} else if (settings.hrm.mode == "emulate") {
|
||||
if (settings.hrm.name == "sin") {
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit(60 + 3 * Math.sin(Date.now() / 10000));
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (settings.gps && settings.gps.enabled) {
|
||||
log("GPS", settings.gps);
|
||||
let modGps = function(dataProvider) {
|
||||
Bangle.getGPSFix = dataProvider;
|
||||
setInterval(() => {
|
||||
Bangle.sensortoolsOrigEmit("GPS", dataProvider());
|
||||
}, 1000);
|
||||
};
|
||||
if (settings.gps.power) {
|
||||
Bangle.sensortoolsOrigSetGPSPower = Bangle.setGPSPower;
|
||||
Bangle.setGPSPower = createPowerFunction(settings.gps.power, "GPS", Bangle.sensortoolsOrigSetGPSPower);
|
||||
}
|
||||
if (settings.gps.mode == "emulate") {
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
let d = a*180/Math.PI;
|
||||
return (d+360)%360;
|
||||
}
|
||||
|
||||
function bearing(a,b){
|
||||
if (!a || !b || !a.lon || !a.lat || !b.lon || !b.lat) return Infinity;
|
||||
let delta = radians(b.lon-a.lon);
|
||||
let alat = radians(a.lat);
|
||||
let blat = radians(b.lat);
|
||||
let y = Math.sin(delta) * Math.cos(blat);
|
||||
let x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
|
||||
function interpolate(a,b,progress){
|
||||
return {
|
||||
lat: a.lat * progress + b.lat * (1-progress),
|
||||
lon: a.lon * progress + b.lon * (1-progress),
|
||||
ele: a.ele * progress + b.ele * (1-progress)
|
||||
}
|
||||
}
|
||||
|
||||
function getSquareRoute(){
|
||||
return [
|
||||
{lat:"47.2577411",lon:"11.9927442",ele:2273},
|
||||
{lat:"47.266761",lon:"11.9926673",ele:2166},
|
||||
{lat:"47.2667605",lon:"12.0059511",ele:2245},
|
||||
{lat:"47.2577516",lon:"12.0059925",ele:1994}
|
||||
];
|
||||
}
|
||||
function getSquareRouteFuzzy(){
|
||||
return [
|
||||
{lat:"47.2578455",lon:"11.9929891",ele:2265},
|
||||
{lat:"47.258592",lon:"11.9923341",ele:2256},
|
||||
{lat:"47.2594506",lon:"11.9927412",ele:2230},
|
||||
{lat:"47.2603323",lon:"11.9924949",ele:2219},
|
||||
{lat:"47.2612056",lon:"11.9928175",ele:2199},
|
||||
{lat:"47.2621002",lon:"11.9929817",ele:2182},
|
||||
{lat:"47.2629025",lon:"11.9923915",ele:2189},
|
||||
{lat:"47.2637828",lon:"11.9926486",ele:2180},
|
||||
{lat:"47.2646733",lon:"11.9928167",ele:2191},
|
||||
{lat:"47.2655617",lon:"11.9930357",ele:2185},
|
||||
{lat:"47.2662862",lon:"11.992252",ele:2186},
|
||||
{lat:"47.2669305",lon:"11.993173",ele:2166},
|
||||
{lat:"47.266666",lon:"11.9944419",ele:2171},
|
||||
{lat:"47.2667579",lon:"11.99576",ele:2194},
|
||||
{lat:"47.2669409",lon:"11.9970579",ele:2207},
|
||||
{lat:"47.2666562",lon:"11.9983128",ele:2212},
|
||||
{lat:"47.2666027",lon:"11.9996335",ele:2262},
|
||||
{lat:"47.2667245",lon:"12.0009395",ele:2278},
|
||||
{lat:"47.2668457",lon:"12.002256",ele:2297},
|
||||
{lat:"47.2666126",lon:"12.0035373",ele:2303},
|
||||
{lat:"47.2664554",lon:"12.004841",ele:2251},
|
||||
{lat:"47.2669461",lon:"12.005948",ele:2245},
|
||||
{lat:"47.2660877",lon:"12.006323",ele:2195},
|
||||
{lat:"47.2652729",lon:"12.0057552",ele:2163},
|
||||
{lat:"47.2643926",lon:"12.0060123",ele:2131},
|
||||
{lat:"47.2634978",lon:"12.0058302",ele:2095},
|
||||
{lat:"47.2626129",lon:"12.0060759",ele:2066},
|
||||
{lat:"47.2617325",lon:"12.0058188",ele:2037},
|
||||
{lat:"47.2608668",lon:"12.0061784",ele:1993},
|
||||
{lat:"47.2600155",lon:"12.0057392",ele:1967},
|
||||
{lat:"47.2591203",lon:"12.0058233",ele:1949},
|
||||
{lat:"47.2582307",lon:"12.0059718",ele:1972},
|
||||
{lat:"47.2578014",lon:"12.004804",ele:2011},
|
||||
{lat:"47.2577232",lon:"12.0034834",ele:2044},
|
||||
{lat:"47.257745",lon:"12.0021656",ele:2061},
|
||||
{lat:"47.2578682",lon:"12.0008597",ele:2065},
|
||||
{lat:"47.2577082",lon:"11.9995526",ele:2071},
|
||||
{lat:"47.2575917",lon:"11.9982348",ele:2102},
|
||||
{lat:"47.2577401",lon:"11.996924",ele:2147},
|
||||
{lat:"47.257715",lon:"11.9956061",ele:2197},
|
||||
{lat:"47.2578996",lon:"11.9943081",ele:2228}
|
||||
];
|
||||
}
|
||||
|
||||
if (settings.gps.name == "staticfix") {
|
||||
modGps(() => { return {
|
||||
"lat": 52,
|
||||
"lon": 8,
|
||||
"alt": 100,
|
||||
"speed": 10,
|
||||
"course": 12,
|
||||
"time": Date.now(),
|
||||
"satellites": 7,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
} else if (settings.gps.name.includes("route")) {
|
||||
let route;
|
||||
let interpSteps;
|
||||
if (settings.gps.name == "routeFuzzy"){
|
||||
route = getSquareRouteFuzzy();
|
||||
interpSteps = 5;
|
||||
} else {
|
||||
route = getSquareRoute();
|
||||
interpSteps = 50;
|
||||
}
|
||||
|
||||
let step = 0;
|
||||
let routeIndex = 0;
|
||||
modGps(() => {
|
||||
let newIndex = (routeIndex + 1)%route.length;
|
||||
|
||||
let result = {
|
||||
"speed": Math.random() * 3 + 2,
|
||||
"time": Date.now(),
|
||||
"satellites": Math.floor(Math.random()*5)+3,
|
||||
"fix": 1,
|
||||
"hdop": Math.floor(Math.random(30)+1)
|
||||
};
|
||||
|
||||
let oldPos = route[routeIndex];
|
||||
if (step != 0){
|
||||
oldPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,step/interpSteps));
|
||||
}
|
||||
let newPos = route[newIndex];
|
||||
if (step < interpSteps - 1){
|
||||
newPos = interpolate(route[routeIndex], route[newIndex], E.clip(0,1,(step+1)%interpSteps/interpSteps));
|
||||
}
|
||||
|
||||
if (step == interpSteps - 1){
|
||||
let followingIndex = (routeIndex + 2)%route.length;
|
||||
newPos = interpolate(route[newIndex], route[followingIndex], E.clip(0,1,1/interpSteps));
|
||||
}
|
||||
|
||||
result.lat = oldPos.lat;
|
||||
result.lon = oldPos.lon;
|
||||
result.alt = oldPos.ele;
|
||||
|
||||
result.course = bearing(oldPos,newPos);
|
||||
|
||||
step++;
|
||||
if (step == interpSteps){
|
||||
routeIndex = (routeIndex + 1) % route.length;
|
||||
step = 0;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
} else if (settings.gps.name == "nofix") {
|
||||
modGps(() => { return {
|
||||
"lat": NaN,
|
||||
"lon": NaN,
|
||||
"alt": NaN,
|
||||
"speed": NaN,
|
||||
"course": NaN,
|
||||
"time": Date.now(),
|
||||
"satellites": 2,
|
||||
"fix": 0,
|
||||
"hdop": NaN
|
||||
};});
|
||||
} else if (settings.gps.name == "changingfix") {
|
||||
let currentSpeed=1;
|
||||
let currentLat=20;
|
||||
let currentLon=10;
|
||||
let currentCourse=10;
|
||||
let currentAlt=-100;
|
||||
let currentSats=5;
|
||||
modGps(() => {
|
||||
currentLat += 0.1;
|
||||
if (currentLat > 50) currentLat = 20;
|
||||
currentLon += 0.1;
|
||||
if (currentLon > 20) currentLon = 10;
|
||||
currentSpeed *= 10;
|
||||
if (currentSpeed > 1000) currentSpeed = 1;
|
||||
currentCourse += 12;
|
||||
if (currentCourse > 360) currentCourse -= 360;
|
||||
currentSats += 1;
|
||||
if (currentSats > 10) currentSats = 5;
|
||||
currentAlt *= 10;
|
||||
if (currentAlt > 1000) currentAlt = -100;
|
||||
return {
|
||||
"lat": currentLat,
|
||||
"lon": currentLon,
|
||||
"alt": currentAlt,
|
||||
"speed": currentSpeed,
|
||||
"course": currentCourse,
|
||||
"time": Date.now(),
|
||||
"satellites": currentSats,
|
||||
"fix": 1,
|
||||
"hdop": 1
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.mag && settings.mag.enabled) {
|
||||
log("MAG", settings.mag);
|
||||
let modMag = function(data) {
|
||||
setInterval(() => {
|
||||
Bangle.getCompass = data;
|
||||
Bangle.sensortoolsOrigEmit("mag", data());
|
||||
}, 100);
|
||||
};
|
||||
if (settings.mag.power) {
|
||||
Bangle.sensortoolsOrigSetCompassPower = Bangle.setCompassPower;
|
||||
Bangle.setCompassPower = createPowerFunction(settings.mag.power, "Compass", Bangle.sensortoolsOrigSetCompassPower);
|
||||
}
|
||||
if (settings.mag.mode == "emulate") {
|
||||
if (settings.mag.name == "static") {
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: 0
|
||||
};});
|
||||
} else if (settings.mag.name == "rotate"){
|
||||
let last = 0;
|
||||
modMag(()=>{return {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
dx: 1,
|
||||
dy: 1,
|
||||
dz: 1,
|
||||
heading: last = (last+1)%360,
|
||||
};});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "sensortools",
|
||||
"name": "Sensor tools",
|
||||
"shortName": "Sensor tools",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Tools for testing and debugging apps that use sensor input",
|
||||
"icon": "icon.png",
|
||||
"type": "bootloader",
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
"storage": [
|
||||
{"name":"sensortools.0.boot.js","url":"boot.js"},
|
||||
{"name":"sensortools.settings.js","url":"settings.js"},
|
||||
{"name":"sensortools","url":"lib.js"},
|
||||
{"name":"sensortools.default.json","url":"default.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue