diff --git a/android.html b/android.html
new file mode 100644
index 000000000..93999008f
--- /dev/null
+++ b/android.html
@@ -0,0 +1,352 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bangle.js App Loader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sort by:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog
index 41eec666a..7ca8319b6 100644
--- a/apps/bthrm/ChangeLog
+++ b/apps/bthrm/ChangeLog
@@ -21,3 +21,4 @@
Adds some preset modes and a custom one
Restructure the settings menu
0.08: Allow scanning for devices in settings
+0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655)
diff --git a/apps/bthrm/README.md b/apps/bthrm/README.md
index 42ad619bd..8d5872670 100644
--- a/apps/bthrm/README.md
+++ b/apps/bthrm/README.md
@@ -2,7 +2,7 @@
When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.
-HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor.
+HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM')` event as if it came from the on board monitor.
This means it's compatible with many Bangle.js apps including:
@@ -16,19 +16,23 @@ as that requires live sensor data (rather than just BPM readings).
Just install the app, then install an app that uses the heart rate monitor.
-Once installed it'll automatically try and connect to the first bluetooth
-heart rate monitor it finds.
+Once installed you will have to go into this app's settings while your heart rate monitor
+ is available for bluetooth pairing and scan for devices.
**To disable this and return to normal HRM, uninstall the app**
## Compatible Heart Rate Monitors
This works with any heart rate monitor providing the standard Bluetooth
-Heart Rate Service (`180D`) and characteristic (`2A37`).
+Heart Rate Service (`180D`) and characteristic (`2A37`). It additionally supports
+the location (`2A38`) characteristic and the Battery Service (`180F`), reporting
+that information in the `BTHRM` event when they are available.
So far it has been tested on:
* CooSpo Bluetooth Heart Rate Monitor
+* Polar H10
+* Polar OH1
* Wahoo TICKR X 2
## Internals
@@ -38,7 +42,6 @@ This replaces `Bangle.setHRMPower` with its own implementation.
## TODO
* A widget to show connection state?
-* Specify a specific device by address?
## Creator
diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js
index 3a1f1cc4c..e9e640563 100644
--- a/apps/bthrm/boot.js
+++ b/apps/bthrm/boot.js
@@ -3,7 +3,7 @@
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
-
+
var log = function(text, param){
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
@@ -13,39 +13,38 @@
print(logline);
}
};
-
+
log("Settings: ", settings);
-
+
if (settings.enabled){
- function clearCache(){
+ var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
- }
+ };
- function getCache(){
+ var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
- if (settings.btname && settings.btname == cache.name) return cache;
+ if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
- }
-
- function addNotificationHandler(characteristic){
+ };
+
+ var addNotificationHandler = function(characteristic) {
log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler);
- characteristic.on('characteristicvaluechanged', supportedCharacteristics[characteristic.uuid].handler);
- }
-
- function writeCache(cache){
+ characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
+ };
+
+ var writeCache = function(cache) {
var oldCache = getCache();
- if (oldCache != cache) {
+ if (oldCache !== cache) {
log("Writing cache");
- require('Storage').writeJSON("bthrm.cache.json", cache)
+ require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
-
- }
+ };
- function characteristicsToCache(characteristics){
+ var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
@@ -60,9 +59,9 @@
};
}
writeCache(cache);
- }
+ };
- function characteristicsFromCache(){
+ var characteristicsFromCache = function() {
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
@@ -81,38 +80,34 @@
restored.push(r);
}
return restored;
- }
+ };
log("Start");
var lastReceivedData={
};
- var serviceFilters = [{
- services: [ "180d" ]
- }];
-
- supportedServices = [
- "0x180d", "0x180f"
+ var supportedServices = [
+ "0x180d", // Heart Rate
+ "0x180f", // Battery
];
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
- handler: function (event){
- var dv = event.target.value;
+ handler: function (dv){
var flags = dv.getUint8(0);
-
+
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
-
+
var sensorContact;
-
+
if (flags & 2){
- sensorContact = (flags & 4) ? true : false;
+ sensorContact = !!(flags & 4);
}
-
+
var idx = 2 + (flags&1);
-
+
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
@@ -121,11 +116,11 @@
var interval;
if (flags & 16) {
interval = [];
- maxIntervalBytes = (dv.byteLength - idx);
+ var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
- idx += 2
+ idx += 2;
}
}
@@ -140,45 +135,44 @@
}
if (settings.replace){
- var newEvent = {
+ var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
-
- log("Emitting HRM: ", newEvent);
- Bangle.emit("HRM", newEvent);
+
+ log("Emitting HRM: ", repEvent);
+ Bangle.emit("HRM", repEvent);
}
var newEvent = {
bpm: bpm
};
-
+
if (location) newEvent.location = location;
if (interval) newEvent.rr = interval;
if (energyExpended) newEvent.energy = energyExpended;
if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact;
-
+
log("Emitting BTHRM: ", newEvent);
Bangle.emit("BTHRM", newEvent);
}
},
"0x2a38": {
//Body sensor location
- handler: function(data){
+ handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
- if (!lastReceivedData["0x180d"]["0x2a38"]) lastReceivedData["0x180d"]["0x2a38"] = data.target.value;
+ lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
- handler: function (event){
+ handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
- if (!lastReceivedData["0x180f"]["0x2a19"]) lastReceivedData["0x180f"]["0x2a19"] = event.target.value.getUint8(0);
+ lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
-
};
var device;
@@ -195,7 +189,7 @@
maxInterval: 1500
};
- function waitingPromise(timeout) {
+ var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
@@ -203,7 +197,7 @@
resolve();
}, timeout);
});
- }
+ };
if (settings.enabled){
Bangle.isBTHRMOn = function(){
@@ -215,7 +209,6 @@
};
}
-
if (settings.replace){
var origIsHRMOn = Bangle.isHRMOn;
@@ -229,15 +222,15 @@
};
}
- function clearRetryTimeout(){
+ var clearRetryTimeout = function() {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
- }
+ };
- function retry(){
+ var retry = function() {
log("Retry");
if (!currentRetryTimeout){
@@ -252,17 +245,17 @@
initBt();
}, clampedTime);
- retryTime = Math.pow(retryTime, 1.1);
+ retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
- }
+ };
var buzzing = false;
- function onDisconnect(reason) {
+ var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT: ", gatt);
log("Characteristics: ", characteristics);
@@ -277,11 +270,23 @@
if (Bangle.isBTHRMOn()){
retry();
}
- }
+ };
- function createCharacteristicPromise(newCharacteristic){
+ var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise: ", newCharacteristic);
var result = Promise.resolve();
+ // For values that can be read, go ahead and read them, even if we might be notified in the future
+ // Allows for getting initial state of infrequently updating characteristics, like battery
+ if (newCharacteristic.readValue){
+ result = result.then(()=>{
+ log("Reading data for " + JSON.stringify(newCharacteristic));
+ return newCharacteristic.readValue().then((data)=>{
+ if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
+ supportedCharacteristics[newCharacteristic.uuid].handler(data);
+ }
+ });
+ });
+ }
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications for: ", newCharacteristic);
@@ -290,31 +295,23 @@
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
- waitingPromise(settings.gracePeriodNotification)
+ return waitingPromise(settings.gracePeriodNotification);
});
}
return startPromise;
});
- } else if (newCharacteristic.read){
- result = result.then(()=>{
- readData(newCharacteristic);
- log("Reading data for " + newCharacteristic);
- return newCharacteristic.read().then((data)=>{
- supportedCharacteristics[newCharacteristic.uuid].handler(data);
- });
- });
}
return result.then(()=>log("Handled characteristic: ", newCharacteristic));
- }
-
- function attachCharacteristicPromise(promise, characteristic){
+ };
+
+ var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
- }
-
- function createCharacteristicsPromise(newCharacteristics){
+ };
+
+ var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promise: ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
@@ -324,13 +321,13 @@
if (c.properties.notify){
addNotificationHandler(c);
}
-
+
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
- }
-
- function createServicePromise(service){
+ };
+
+ var createServicePromise = function(service) {
log("Create service promise: ", service);
var result = Promise.resolve();
result = result.then(()=>{
@@ -338,15 +335,13 @@
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
- }
-
- function attachServicePromise(promise, service){
- return promise.then(()=>createServicePromise(service));
- }
-
- var reUseCounter = 0;
+ };
- function initBt() {
+ var attachServicePromise = function(promise, service) {
+ return promise.then(()=>createServicePromise(service));
+ };
+
+ var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
@@ -355,63 +350,58 @@
blockInit = true;
- if (reUseCounter > 10){
- log("Reuse counter to high");
- gatt=undefined;
- reUseCounter = 0;
- }
-
var promise;
-
+ var filters;
+
if (!device){
- var filters = serviceFilters;
- if (settings.btname){
- log("Configured device name", settings.btname);
- filters = [{name: settings.btname}];
+ if (settings.btid){
+ log("Configured device id", settings.btid);
+ filters = [{ id: settings.btid }];
+ } else {
+ return;
}
log("Requesting device with filters", filters);
- promise = NRF.requestDevice({ filters: filters });
-
+ promise = NRF.requestDevice({ filters: filters, active: true });
+
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
}
-
+
promise = promise.then((d)=>{
log("Got device: ", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
-
+
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
-
} else {
promise = Promise.resolve();
log("Reuse device: ", device);
}
-
+
promise = promise.then(()=>{
if (gatt){
log("Reuse GATT: ", gatt);
} else {
log("GATT is new: ", gatt);
characteristics = [];
- var cachedName = getCache().name;
- if (device.name != cachedName){
- log("Device name changed from " + cachedName + " to " + device.name + ", clearing cache");
+ var cachedId = getCache().id;
+ if (device.id !== cachedId){
+ log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
- newCache.name = device.name;
+ newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
-
+
return Promise.resolve(gatt);
});
-
+
promise = promise.then((gatt)=>{
if (!gatt.connected){
var connectPromise = gatt.connect(connectSettings);
@@ -427,16 +417,28 @@
return Promise.resolve();
}
});
-
+
+/* promise = promise.then(() => {
+ log(JSON.stringify(gatt.getSecurityStatus()));
+ if (gatt.getSecurityStatus()['bonded']) {
+ log("Already bonded");
+ return Promise.resolve();
+ } else {
+ log("Start bonding");
+ return gatt.startBonding()
+ .then(() => console.log(gatt.getSecurityStatus()));
+ }
+ });*/
+
promise = promise.then(()=>{
- if (!characteristics || characteristics.length == 0){
+ if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache();
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
- if (characteristics.length == 0){
+ if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
@@ -454,24 +456,22 @@
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
- return waitingPromise(settings.gracePeriodService)
+ return waitingPromise(settings.gracePeriodService);
});
}
return result;
});
-
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
-
+
return characteristicsPromise;
});
-
- promise = promise.then(()=>{
+
+ return promise.then(()=>{
log("Connection established, waiting for notifications");
- reUseCounter = 0;
characteristicsToCache(characteristics);
clearRetryTimeout();
}).catch((e) => {
@@ -479,7 +479,7 @@
log("Error:", e);
onDisconnect(e);
});
- }
+ };
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
@@ -487,7 +487,7 @@
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
- if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
+ if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
@@ -510,7 +510,7 @@
}
}
};
-
+
var origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
@@ -525,11 +525,10 @@
}
};
}
-
-
+
var fallbackInterval;
-
- function switchInternalHrm(){
+
+ var switchInternalHrm = function() {
if (settings.allowFallback && !fallbackInterval){
log("Fallback to HRM enabled");
origSetHRMPower(1, "bthrm_fallback");
@@ -542,7 +541,7 @@
}
}, settings.fallbackTimeout);
}
- }
+ };
if (settings.replace){
log("Replace HRM event");
@@ -557,11 +556,11 @@
}
switchInternalHrm();
}
-
+
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
- var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
+ gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}
diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js
index cc533eedd..dd9230386 100644
--- a/apps/bthrm/bthrm.js
+++ b/apps/bthrm/bthrm.js
@@ -1,7 +1,16 @@
-var btm = g.getHeight()-1;
var intervalInt;
var intervalBt;
+var BODY_LOCS = {
+ 0: 'Other',
+ 1: 'Chest',
+ 2: 'Wrist',
+ 3: 'Finger',
+ 4: 'Hand',
+ 5: 'Ear Lobe',
+ 6: 'Foot',
+}
+
function clear(y){
g.reset();
g.clearRect(0,y,g.getWidth(),y+75);
@@ -15,17 +24,17 @@ function draw(y, type, event) {
g.setFontAlign(0,0);
g.setFontVector(40).drawString(str,px,y+20);
str = "Event: " + type;
- if (type == "HRM") {
+ if (type === "HRM") {
str += " Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+40);
str = " Source: " + (event.src ? event.src : "internal");
g.setFontVector(12).drawString(str,px,y+50);
}
- if (type == "BTHRM"){
+ if (type === "BTHRM"){
if (event.battery) str += " Bat: " + (event.battery ? event.battery : "");
g.setFontVector(12).drawString(str,px,y+40);
str= "";
- if (event.location) str += "Loc: " + event.location.toFixed(0) + "ms";
+ if (event.location) str += "Loc: " + BODY_LOCS[event.location];
if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
g.setFontVector(12).drawString(str,px,y+50);
str= "";
@@ -45,7 +54,7 @@ function onBtHrm(e) {
firstEventBt = false;
}
draw(100, "BTHRM", e);
- if (e.bpm == 0){
+ if (e.bpm === 0){
Bangle.buzz(100,0.2);
}
if (intervalBt){
diff --git a/apps/bthrm/default.json b/apps/bthrm/default.json
index 64e638b8a..fb284bcd2 100644
--- a/apps/bthrm/default.json
+++ b/apps/bthrm/default.json
@@ -7,10 +7,10 @@
"allowFallback": true,
"warnDisconnect": false,
"fallbackTimeout": 10,
- "custom_replace": false,
+ "custom_replace": true,
"custom_debuglog": false,
- "custom_startWithHrm": false,
- "custom_allowFallback": false,
+ "custom_startWithHrm": true,
+ "custom_allowFallback": true,
"custom_warnDisconnect": false,
"custom_fallbackTimeout": 10,
"gracePeriodNotification": 0,
diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json
index b35ebd6af..39c1ff8bb 100644
--- a/apps/bthrm/metadata.json
+++ b/apps/bthrm/metadata.json
@@ -2,11 +2,11 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
- "version": "0.08",
+ "version": "0.09",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
- "tags": "health,bluetooth",
+ "tags": "health,bluetooth,hrm,bthrm",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
diff --git a/apps/bthrm/settings.js b/apps/bthrm/settings.js
index 4b564d670..b376d6a2d 100644
--- a/apps/bthrm/settings.js
+++ b/apps/bthrm/settings.js
@@ -5,14 +5,14 @@
require('Storage').writeJSON(FILE, s);
readSettings();
}
-
+
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
-
+
var FILE="bthrm.json";
var settings;
readSettings();
@@ -61,12 +61,13 @@
}
};
- if (settings.btname){
- var name = "Clear " + settings.btname;
+ if (settings.btname || settings.btid){
+ var name = "Clear " + (settings.btname || settings.btid);
mainmenu[name] = function() {
- E.showPrompt("Clear current device name?").then((r)=>{
+ E.showPrompt("Clear current device?").then((r)=>{
if (r) {
writeSettings("btname",undefined);
+ writeSettings("btid",undefined);
}
E.showMenu(buildMainMenu());
});
@@ -78,9 +79,7 @@
mainmenu.Debug = function() { E.showMenu(submenu_debug); };
return mainmenu;
}
-
-
var submenu_debug = {
'' : { title: "Debug"},
'< Back': function() { E.showMenu(buildMainMenu()); },
@@ -103,35 +102,39 @@
function createMenuFromScan(){
E.showMenu();
- E.showMessage("Scanning");
+ E.showMessage("Scanning for 4 seconds");
var submenu_scan = {
- '' : { title: "Scan"},
'< Back': function() { E.showMenu(buildMainMenu()); }
};
- var packets=10;
- var scanStart=Date.now();
- NRF.setScan(function(d) {
- packets--;
- if (packets<=0 || Date.now() - scanStart > 5000){
- NRF.setScan();
- E.showMenu(submenu_scan);
- } else if (d.name){
- print("Found device", d);
- submenu_scan[d.name] = function(){
- E.showPrompt("Set "+d.name+"?").then((r)=>{
- if (r) {
- writeSettings("btname",d.name);
- }
- E.showMenu(buildMainMenu());
+ NRF.findDevices(function(devices) {
+ submenu_scan[''] = { title: `Scan (${devices.length} found)`};
+ if (devices.length === 0) {
+ E.showAlert("No devices found")
+ .then(() => E.showMenu(buildMainMenu()));
+ return;
+ } else {
+ devices.forEach((d) => {
+ print("Found device", d);
+ var shown = (d.name || d.id.substr(0, 17));
+ submenu_scan[shown] = function () {
+ E.showPrompt("Set " + shown + "?").then((r) => {
+ if (r) {
+ writeSettings("btid", d.id);
+ // Store the name for displaying later. Will connect by ID
+ if (d.name) {
+ writeSettings("btname", d.name);
+ }
+ }
+ E.showMenu(buildMainMenu());
+ });
+ };
});
- };
}
- }, { filters: [{services: [ "180d" ]}]});
+ E.showMenu(submenu_scan);
+ }, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]});
}
-
-
var submenu_custom = {
'' : { title: "Custom mode"},
'< Back': function() { E.showMenu(buildMainMenu()); },
@@ -167,7 +170,7 @@
}
},
};
-
+
var submenu_grace = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(submenu_debug); },
@@ -212,51 +215,6 @@
}
}
};
-
- var submenu = {
- '' : { title: "Grace periods"},
- '< Back': function() { E.showMenu(buildMainMenu()); },
- 'Request': {
- value: settings.gracePeriodRequest,
- min: 0,
- max: 3000,
- step: 100,
- format: v=>v+"ms",
- onchange: v => {
- writeSettings("gracePeriodRequest",v);
- }
- },
- 'Connect': {
- value: settings.gracePeriodConnect,
- min: 0,
- max: 3000,
- step: 100,
- format: v=>v+"ms",
- onchange: v => {
- writeSettings("gracePeriodConnect",v);
- }
- },
- 'Notification': {
- value: settings.gracePeriodNotification,
- min: 0,
- max: 3000,
- step: 100,
- format: v=>v+"ms",
- onchange: v => {
- writeSettings("gracePeriodNotification",v);
- }
- },
- 'Service': {
- value: settings.gracePeriodService,
- min: 0,
- max: 3000,
- step: 100,
- format: v=>v+"ms",
- onchange: v => {
- writeSettings("gracePeriodService",v);
- }
- }
- };
-
+
E.showMenu(buildMainMenu());
-})
+});
diff --git a/apps/health/README.md b/apps/health/README.md
index c6b379c0a..3cc234a3f 100644
--- a/apps/health/README.md
+++ b/apps/health/README.md
@@ -2,7 +2,7 @@
Logs health data to a file every 10 minutes, and provides an app to view it
-**BETA - requires firmware 2v11**
+**BETA - requires firmware 2v11 or later**
## Usage
diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog
new file mode 100644
index 000000000..4a72a9f28
--- /dev/null
+++ b/apps/iconlaunch/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Initial release
+0.02: implemented "direct launch" and "one click exit" settings
\ No newline at end of file
diff --git a/apps/iconlaunch/README.md b/apps/iconlaunch/README.md
new file mode 100644
index 000000000..0d36fdeb4
--- /dev/null
+++ b/apps/iconlaunch/README.md
@@ -0,0 +1,12 @@
+# Icon launcher
+
+A launcher inspired by smartphones, with an icon-only scrollable menu.
+
+This launcher shows 9 apps per screen, making it much faster to navigate versus the default launcher.
+
+
+
+
+## Technical note
+
+The app uses `E.showScroller`'s code in the app but not the function itself because `E.showScroller` doesn't report the position of a press to the select function.
diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js
new file mode 100644
index 000000000..4eeaff589
--- /dev/null
+++ b/apps/iconlaunch/app.js
@@ -0,0 +1,209 @@
+const s = require("Storage");
+const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false };
+
+if( settings.oneClickExit)
+ setWatch(_=> load(), BTN1);
+
+if (!settings.fullscreen) {
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+}
+
+var apps = s
+ .list(/\.info$/)
+ .map((app) => {
+ var a = s.readJSON(app, 1);
+ return (
+ a && {
+ name: a.name,
+ type: a.type,
+ icon: a.icon,
+ sortorder: a.sortorder,
+ src: a.src,
+ }
+ );
+ })
+ .filter(
+ (app) =>
+ app &&
+ (app.type == "app" ||
+ (app.type == "clock" && settings.showClocks) ||
+ !app.type)
+ );
+apps.sort((a, b) => {
+ var n = (0 | a.sortorder) - (0 | b.sortorder);
+ if (n) return n; // do sortorder first
+ if (a.name < b.name) return -1;
+ if (a.name > b.name) return 1;
+ return 0;
+});
+apps.forEach((app) => {
+ if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area
+});
+
+let scroll = 0;
+let selectedItem = -1;
+const R = Bangle.appRect;
+
+const iconSize = 48;
+
+const appsN = Math.floor(R.w / iconSize);
+const whitespace = (R.w - appsN * iconSize) / (appsN + 1);
+
+const itemSize = iconSize + whitespace;
+
+function drawItem(itemI, r) {
+ g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
+ let x = 0;
+ for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) {
+ if (!apps[i]) break;
+ x += whitespace;
+ if (!apps[i].icon) {
+ g.setFontAlign(0,0,0).setFont("12x20:2").drawString("?", x + r.x+iconSize/2, r.y + iconSize/2);
+ } else {
+ g.drawImage(apps[i].icon, x + r.x, r.y);
+ }
+ if (selectedItem == i) {
+ g.drawRect(
+ x + r.x - 1,
+ r.y - 1,
+ x + r.x + iconSize + 1,
+ r.y + iconSize + 1
+ );
+ }
+ x += iconSize;
+ }
+ drawText(itemI);
+}
+
+function drawItemAuto(i) {
+ var y = idxToY(i);
+ g.reset().setClipRect(R.x, y, R.x2, y + itemSize);
+ drawItem(i, {
+ x: R.x,
+ y: y,
+ w: R.w,
+ h: itemSize
+ });
+ g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
+}
+
+let lastIsDown = false;
+
+function drawText(i) {
+ const selectedApp = apps[selectedItem];
+ const idy = (selectedItem - (selectedItem % 3)) / 3;
+ if (!selectedApp || i != idy) return;
+ const appY = idxToY(idy) + iconSize / 2;
+ g.setFontAlign(0, 0, 0);
+ g.setFont("12x20");
+ const rect = g.stringMetrics(selectedApp.name);
+ g.clearRect(
+ R.w / 2 - rect.width / 2,
+ appY - rect.height / 2,
+ R.w / 2 + rect.width / 2,
+ appY + rect.height / 2
+ );
+ g.drawString(selectedApp.name, R.w / 2, appY);
+}
+
+function selectItem(id, e) {
+ const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1);
+ const appId = id * appsN + iconN;
+ if( settings.direct && apps[appId])
+ {
+ load(apps[appId].src);
+ return;
+ }
+ if (appId == selectedItem && apps[appId]) {
+ const app = apps[appId];
+ if (!app.src || s.read(app.src) === undefined) {
+ E.showMessage( /*LANG*/ "App Source\nNot found");
+ } else {
+ load(app.src);
+ }
+ }
+ selectedItem = appId;
+ drawItems();
+}
+
+function idxToY(i) {
+ return i * itemSize + R.y - (scroll & ~1);
+}
+
+function YtoIdx(y) {
+ return Math.floor((y + (scroll & ~1) - R.y) / itemSize);
+}
+
+function drawItems() {
+ g.reset().clearRect(R.x, R.y, R.x2, R.y2);
+ g.setClipRect(R.x, R.y, R.x2, R.y2);
+ var a = YtoIdx(R.y);
+ var b = Math.min(YtoIdx(R.y2), 99);
+ for (var i = a; i <= b; i++)
+ drawItem(i, {
+ x: R.x,
+ y: idxToY(i),
+ w: R.w,
+ h: itemSize,
+ });
+ g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
+}
+
+drawItems();
+g.flip();
+
+const itemsN = Math.ceil(apps.length / appsN);
+
+Bangle.setUI({
+ mode: "custom",
+ drag: (e) => {
+ let dy = e.dy;
+ if (scroll + R.h - dy > itemsN * itemSize) {
+ dy = scroll + R.h - itemsN * itemSize;
+ }
+ if (scroll - dy < 0) {
+ dy = scroll;
+ }
+ scroll -= dy;
+ scroll = E.clip(scroll, 0, itemSize * (itemsN - 1));
+ g.setClipRect(R.x, R.y, R.x2, R.y2);
+ g.scroll(0, dy);
+ if (dy < 0) {
+ g.setClipRect(R.x, R.y2 - (1 - dy), R.x2, R.y2);
+ let i = YtoIdx(R.y2 - (1 - dy));
+ let y = idxToY(i);
+ while (y < R.y2) {
+ drawItem(i, {
+ x: R.x,
+ y: y,
+ w: R.w,
+ h: itemSize,
+ });
+ i++;
+ y += itemSize;
+ }
+ } else {
+ // d>0
+ g.setClipRect(R.x, R.y, R.x2, R.y + dy);
+ let i = YtoIdx(R.y + dy);
+ let y = idxToY(i);
+ while (y > R.y - itemSize) {
+ drawItem(i, {
+ x: R.x,
+ y: y,
+ w: R.w,
+ h: itemSize,
+ });
+ y -= itemSize;
+ i--;
+ }
+ }
+ g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
+ },
+ touch: (_, e) => {
+ if (e.y < R.y - 4) return;
+ var i = YtoIdx(e.y);
+ selectItem(i, e);
+ },
+});
diff --git a/apps/iconlaunch/app.png b/apps/iconlaunch/app.png
new file mode 100644
index 000000000..1c8068c50
Binary files /dev/null and b/apps/iconlaunch/app.png differ
diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json
new file mode 100644
index 000000000..01e447672
--- /dev/null
+++ b/apps/iconlaunch/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "iconlaunch",
+ "name": "Icon Launcher",
+ "shortName" : "Icon launcher",
+ "version": "0.02",
+ "icon": "app.png",
+ "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
+ "tags": "tool,system,launcher",
+ "type": "launch",
+ "supports": ["BANGLEJS2"],
+ "storage": [
+ { "name": "iconlaunch.app.js", "url": "app.js" },
+ { "name": "iconlaunch.settings.js", "url": "settings.js" }
+ ],
+ "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
+ "readme": "README.md",
+ "sortorder": -10
+}
diff --git a/apps/iconlaunch/screenshot1.png b/apps/iconlaunch/screenshot1.png
new file mode 100644
index 000000000..8695ead7a
Binary files /dev/null and b/apps/iconlaunch/screenshot1.png differ
diff --git a/apps/iconlaunch/screenshot2.png b/apps/iconlaunch/screenshot2.png
new file mode 100644
index 000000000..b17efa78b
Binary files /dev/null and b/apps/iconlaunch/screenshot2.png differ
diff --git a/apps/iconlaunch/settings.js b/apps/iconlaunch/settings.js
new file mode 100644
index 000000000..e9667047c
--- /dev/null
+++ b/apps/iconlaunch/settings.js
@@ -0,0 +1,38 @@
+// make sure to enclose the function in parentheses
+(function(back) {
+ let settings = Object.assign({
+ showClocks: true,
+ fullscreen: false
+ }, require("Storage").readJSON("launch.json", true) || {});
+
+ let fonts = g.getFonts();
+ function save(key, value) {
+ settings[key] = value;
+ require("Storage").write("launch.json",settings);
+ }
+ const appMenu = {
+ "": { "title": /*LANG*/"Launcher" },
+ /*LANG*/"< Back": back,
+ /*LANG*/"Show Clocks": {
+ value: settings.showClocks == true,
+ format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
+ onchange: (m) => { save("showClocks", m) }
+ },
+ /*LANG*/"Fullscreen": {
+ value: settings.fullscreen == true,
+ format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
+ onchange: (m) => { save("fullscreen", m) }
+ },
+ /*LANG*/"Direct launch": {
+ value: settings.direct == true,
+ format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
+ onchange: (m) => { save("direct", m) }
+ },
+ /*LANG*/"One click exit": {
+ value: settings.oneClickExit == true,
+ format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
+ onchange: (m) => { save("oneClickExit", m) }
+ }
+ };
+ E.showMenu(appMenu);
+});
diff --git a/apps/multitimer/ChangeLog b/apps/multitimer/ChangeLog
index 624f1b0fb..9b60f403a 100644
--- a/apps/multitimer/ChangeLog
+++ b/apps/multitimer/ChangeLog
@@ -1 +1,2 @@
-0.01: Initial version
\ No newline at end of file
+0.01: Initial version
+0.02: Update for time_utils module
diff --git a/apps/multitimer/alarm.js b/apps/multitimer/alarm.js
index fc0195455..97cbaa5fa 100644
--- a/apps/multitimer/alarm.js
+++ b/apps/multitimer/alarm.js
@@ -73,7 +73,7 @@ function showAlarm(alarm) {
const settings = require("sched").getSettings();
let msg = "";
- msg += require("sched").formatTime(alarm.timer);
+ if (alarm.timer) msg += require("time_utils").formatTime(alarm.timer);
if (alarm.msg) {
msg += "\n"+alarm.msg;
}
@@ -86,7 +86,7 @@ function showAlarm(alarm) {
if (alarm.data.hm && alarm.data.hm == true) {
//hard mode extends auto-snooze time
- buzzCount = buzzCount * 2;
+ buzzCount = buzzCount * 3;
startHM();
}
diff --git a/apps/multitimer/app.js b/apps/multitimer/app.js
index becaf6169..e5d77d860 100644
--- a/apps/multitimer/app.js
+++ b/apps/multitimer/app.js
@@ -258,7 +258,7 @@ function editTimer(idx, a) {
a.last = 0;
a.data.ot = a.timer;
a.appid = "multitimer";
- a.js = "load('multitimer.alarm.js')";
+ a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')";
if (idx < 0) alarms.push(a);
else alarms[timerIdx[idx]] = a;
require("sched").setAlarms(alarms);
@@ -585,7 +585,7 @@ function editAlarm(idx, a) {
var menu = {
"": { "title": "Alarm" },
"< Back": () => {
- if (a.data.hm == true) a.js = "load('multitimer.alarm.js')";
+ if (a.data.hm == true) a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')";
if (a.data.hm == false && a.js) delete a.js;
if (idx >= 0) alarms[alarmIdx[idx]] = a;
else alarms.push(a);
diff --git a/apps/multitimer/metadata.json b/apps/multitimer/metadata.json
index 6e53e2c8c..abb958b90 100644
--- a/apps/multitimer/metadata.json
+++ b/apps/multitimer/metadata.json
@@ -1,7 +1,7 @@
{
"id": "multitimer",
"name": "Multi Timer",
- "version": "0.01",
+ "version": "0.02",
"description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.",
"icon": "app.png",
"screenshots": [
@@ -19,4 +19,4 @@
],
"data": [{"name":"multitimer.json"}],
"dependencies": {"scheduler":"type"}
-}
\ No newline at end of file
+}
diff --git a/apps/speedalt2/ChangeLog b/apps/speedalt2/ChangeLog
index 73e9bfc40..9e2abb4ef 100644
--- a/apps/speedalt2/ChangeLog
+++ b/apps/speedalt2/ChangeLog
@@ -13,3 +13,4 @@
1.14: Add VMG and coordinates screens
1.43: Adds mirroring of the watch face to an Android device. See README.md
1.49: Droidscript mirroring prog automatically uses last connection address. Auto connects when run.
+1.50: Add configuration item Wpt File Suffix. A one character suffix to append to the waypoints.json file. A number of other apps also use this file name. Using the file name suffix allows the speedalt2 waypoints to be retained if one of these other apps is installed for a different use.
diff --git a/apps/speedalt2/README.md b/apps/speedalt2/README.md
index e1c6b0a5a..c124e0c00 100644
--- a/apps/speedalt2/README.md
+++ b/apps/speedalt2/README.md
@@ -78,6 +78,10 @@ Waypoints are used in Distance and VMG modes. Create a file waypoints.json and w
The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.)
+By default the waypoints file is called waypoints.json
+
+**Note** : The waypoints.json file is used by a number of different gps apps. The setting 'Wpt File Suffix' allows one of waypoints1.json, waypoints2.json or waypoints3.json to be used instead. This allows the other apps to be used with a different set of waypoints without losing the speedalt2 waypoint set.
+
Sample waypoints.json (My sailing waypoints)
diff --git a/apps/speedalt2/app.js b/apps/speedalt2/app.js
index ed16131a4..4cdf71913 100644
--- a/apps/speedalt2/app.js
+++ b/apps/speedalt2/app.js
@@ -5,8 +5,9 @@ Mike Bennett mike[at]kereru.com
1.14 : Add VMG screen
1.34 : Add bluetooth data stream for Droidscript
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
+1.50 : Add cfg.wptSfx one char suffix to append to waypoints.json filename. Protects speedalt2 waypoints from other apps that use the same file name for waypoints.
*/
-var v = '1.49';
+var v = '1.50';
var vDroid = '1.50'; // Required DroidScript program version
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
@@ -209,7 +210,7 @@ function nxtWp(){
}
function loadWp() {
- var w = require("Storage").readJSON('waypoints.json')||[{name:"NONE"}];
+ var w = require("Storage").readJSON('waypoints'+cfg.wptSfx+'.json')||[{name:"NONE"}];
if (cfg.wp>=w.length) cfg.wp=0;
if (cfg.wp<0) cfg.wp = w.length-1;
savSettings();
@@ -718,6 +719,7 @@ cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt;
cfg.touch = cfg.touch==undefined?true:cfg.touch;
+cfg.wptSfx = cfg.wptSfx==undefined?'':cfg.wptSfx;
if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
diff --git a/apps/speedalt2/metadata.json b/apps/speedalt2/metadata.json
index 4ace46854..2a111af28 100644
--- a/apps/speedalt2/metadata.json
+++ b/apps/speedalt2/metadata.json
@@ -2,7 +2,7 @@
"id": "speedalt2",
"name": "GPS Adventure Sports II",
"shortName":"GPS Adv Sport II",
- "version":"1.49",
+ "version":"1.50",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"icon": "app.png",
"type": "app",
@@ -15,5 +15,11 @@
{"name":"speedalt2.img","url":"app-icon.js","evaluate":true},
{"name":"speedalt2.settings.js","url":"settings.js"}
],
- "data": [{"name":"speedalt2.json"}]
+ "data": [
+ {"name":"speedalt2.json"},
+ {"name":"waypoints.json"},
+ {"name":"waypoints1.json"},
+ {"name":"waypoints2.json"},
+ {"name":"waypoints3.json"}
+ ]
}
diff --git a/apps/speedalt2/settings.js b/apps/speedalt2/settings.js
index babb03061..1bdb58f9d 100644
--- a/apps/speedalt2/settings.js
+++ b/apps/speedalt2/settings.js
@@ -30,6 +30,11 @@
writeSettings();
}
+ function setSfx(s) {
+ settings.wptSfx = s;
+ writeSettings();
+ }
+
const appMenu = {
'': {'title': 'GPS Adv Sprt II'},
@@ -38,6 +43,7 @@
'Units' : function() { E.showMenu(unitsMenu); },
'Colours' : function() { E.showMenu(colMenu); },
'Kalman Filter' : function() { E.showMenu(kalMenu); },
+ 'Wpt File Suffix' : function() { E.showMenu(sfxMenu); },
'Touch' : {
value : settings.touch,
format : v => v?"On":"Off",
@@ -69,6 +75,15 @@
'Inverted' : function() { setColour(3); }
};
+ const sfxMenu = {
+ '': {'title': 'Wpt File Suffix'},
+ '< Back': function() { E.showMenu(appMenu); },
+ 'Default' : function() { setSfx(''); },
+ '1' : function() { setSfx('1'); },
+ '2' : function() { setSfx('2'); },
+ '3' : function() { setSfx('3'); }
+ };
+
const kalMenu = {
'': {'title': 'Kalman Filter'},
'< Back': function() { E.showMenu(appMenu); },
diff --git a/core b/core
index 147892754..404e98183 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 147892754eaf50c8581ebfb4d8651b9ec24aa44e
+Subproject commit 404e981834f2e8df9c505a8fab12ae12fe3bd562
diff --git a/typescript/types/globals.d.ts b/typescript/types/globals.d.ts
index 2ef52dcdf..e82c3da3d 100644
--- a/typescript/types/globals.d.ts
+++ b/typescript/types/globals.d.ts
@@ -140,7 +140,7 @@ declare const require: ((module: 'heatshrink') => {
declare const Bangle: {
// functions
- buzz: () => void;
+ buzz: (duration?: number, intensity?: number) => Promise;
drawWidgets: () => void;
isCharging: () => boolean;
// events
@@ -158,9 +158,9 @@ declare type Image = {
};
declare type GraphicsApi = {
- reset: () => void;
+ reset: () => GraphicsApi;
flip: () => void;
- setColor: (color: string) => void; // TODO we can most likely type color more usefully than this
+ setColor: (color: string) => GraphicsApi; // TODO we can most likely type color more usefully than this
drawImage: (
image: string | Image | ArrayBuffer,
xOffset: number,
@@ -169,7 +169,7 @@ declare type GraphicsApi = {
rotate?: number;
scale?: number;
}
- ) => void;
+ ) => GraphicsApi;
// TODO add more
};