Merge pull request #2208 from halemmerich/performance

Boot time tweaks
master
Gordon Williams 2022-10-31 10:21:53 +00:00 committed by GitHub
commit 2110fc0b34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1017 additions and 1014 deletions

View File

@ -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

View File

@ -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();

633
apps/bthrm/lib.js Normal file
View File

@ -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));
}
});
}
};

View File

@ -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"}
]
}

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Do first update request 5s after boot to boot up faster

View File

@ -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);
}
})();
}

View File

@ -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",

View File

@ -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

View File

@ -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); }
});
}

View File

@ -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",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Less time used during boot if disabled

View File

@ -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();

348
apps/sensortools/lib.js Normal file
View File

@ -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,
};});
}
}
}
};

View File

@ -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"}
]
}