diff --git a/README.md b/README.md
index 38ce09f75..a6c449cc7 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer.
},
```
+### Screenshots
+
+In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { url:"screenshot.png" } ],`
+
+To get a screenshot you can:
+
+* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then
+right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is
+unable to read data back from the LCD controller).
+* Run your code in the emulator and use the screenshot button in the bottom right of the window.
+
+
## Testing
### Online
diff --git a/apps/2047pp/2047pp.app.js b/apps/2047pp/2047pp.app.js
new file mode 100644
index 000000000..58738d04a
--- /dev/null
+++ b/apps/2047pp/2047pp.app.js
@@ -0,0 +1,140 @@
+class TwoK {
+ constructor() {
+ this.b = Array(4).fill().map(() => Array(4).fill(0));
+ this.score = 0;
+ this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
+ }
+ drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
+ g.setColor(c);
+ for (i=0; i
4) g.setColor(1, 1, 1);
+ else g.setColor(0, 0, 0);
+ g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
+ if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
+ }
+ }
+ shift(d) { // +/-1: shift x, +/- 2: shift y
+ var crc = E.CRC32(this.b.toString());
+ if (d==-1) { // shift x left
+ for (y=0; y<4; ++y) {
+ for (x=2; x>=0; x--)
+ if (this.b[y][x]==0) {
+ for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
+ this.b[y][3] = 0;
+ }
+ for (x=0; x<3; ++x)
+ if (this.b[y][x]==this.b[y][x+1]) {
+ this.score += 2*this.b[y][x];
+ this.b[y][x] += this.b[y][x+1];
+ for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
+ this.b[y][3] = 0;
+ }
+ }
+ }
+ else if (d==1) { // shift x right
+ for (y=0; y<4; ++y) {
+ for (x=1; x<4; x++)
+ if (this.b[y][x]==0) {
+ for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
+ this.b[y][0] = 0;
+ }
+ for (x=3; x>0; --x)
+ if (this.b[y][x]==this.b[y][x-1]) {
+ this.score += 2*this.b[y][x];
+ this.b[y][x] += this.b[y][x-1] ;
+ for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
+ this.b[y][0] = 0;
+ }
+ }
+ }
+ else if (d==-2) { // shift y down
+ for (x=0; x<4; ++x) {
+ for (y=1; y<4; y++)
+ if (this.b[y][x]==0) {
+ for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
+ this.b[0][x] = 0;
+ }
+ for (y=3; y>0; y--)
+ if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
+ this.score += 2*this.b[y][x];
+ this.b[y][x] += this.b[y-1][x];
+ for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
+ this.b[0][x] = 0;
+ }
+ }
+ }
+ else if (d==2) { // shift y up
+ for (x=0; x<4; ++x) {
+ for (y=2; y>=0; y--)
+ if (this.b[y][x]==0) {
+ for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
+ this.b[3][x] = 0;
+ }
+ for (y=0; y<3; ++y)
+ if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
+ this.score += 2*this.b[y][x];
+ this.b[y][x] += this.b[y+1][x];
+ for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
+ this.b[3][x] = 0;
+ }
+ }
+ }
+ return (E.CRC32(this.b.toString())!=crc);
+ }
+ addDigit() {
+ var d = Math.random()>0.9 ? 4 : 2;
+ var id = Math.floor(Math.random()*16);
+ while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
+ this.b[Math.floor(id/4)][id%4] = d;
+ }
+}
+
+function dragHandler(e) {
+ if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
+ var res = false;
+ if (Math.abs(e.dx)>Math.abs(e.dy)) {
+ if (e.dx>0) res = twok.shift(1);
+ if (e.dx<0) res = twok.shift(-1);
+ }
+ else {
+ if (e.dy>0) res = twok.shift(-2);
+ if (e.dy<0) res = twok.shift(2);
+ }
+ if (res) twok.addDigit();
+ twok.render();
+ }
+}
+
+function swipeHandler() {
+
+}
+
+function buttonHandler() {
+
+}
+
+var twok = new TwoK();
+twok.addDigit(); twok.addDigit();
+twok.render();
+if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
+if (process.env.HWVERSION==1) {
+ Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
+ setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
+ setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
+}
diff --git a/apps/2047pp/2047pp_screenshot.png b/apps/2047pp/2047pp_screenshot.png
new file mode 100644
index 000000000..8c407fb6f
Binary files /dev/null and b/apps/2047pp/2047pp_screenshot.png differ
diff --git a/apps/2047pp/README.md b/apps/2047pp/README.md
new file mode 100644
index 000000000..cac3323a6
--- /dev/null
+++ b/apps/2047pp/README.md
@@ -0,0 +1,9 @@
+
+# Game of 2047pp (2047++)
+
+Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024.
+
+Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using
+the top/bottom button (Bangle 1).
+
+
diff --git a/apps/2047pp/app-icon.js b/apps/2047pp/app-icon.js
new file mode 100644
index 000000000..4086d1879
--- /dev/null
+++ b/apps/2047pp/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))
\ No newline at end of file
diff --git a/apps/2047pp/app.png b/apps/2047pp/app.png
new file mode 100644
index 000000000..d1fb4a5e5
Binary files /dev/null and b/apps/2047pp/app.png differ
diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json
new file mode 100644
index 000000000..f0fd6c1e3
--- /dev/null
+++ b/apps/2047pp/metadata.json
@@ -0,0 +1,15 @@
+{ "id": "2047pp",
+ "name": "2047pp",
+ "shortName":"2047pp",
+ "icon": "app.png",
+ "version":"0.01",
+ "description": "Bangle version of a tile shifting game",
+ "supports" : ["BANGLEJS","BANGLEJS2"],
+ "allow_emulator": true,
+ "readme": "README.md",
+ "tags": "game",
+ "storage": [
+ {"name":"2047pp.app.js","url":"2047pp.app.js"},
+ {"name":"2047pp.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/alpinenav/app-icon.js b/apps/alpinenav/app-icon.js
index dba084202..6708ee67f 100644
--- a/apps/alpinenav/app-icon.js
+++ b/apps/alpinenav/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A"))
\ No newline at end of file
+require("heatshrink").decompress(atob("mEkgIRO4AFJgPgAocDAoswAocHAokGjAFDhgFFhgFDjEOAoc4gxSE44FDuPjAod//+AAoXfn4FCgPMjJUCmIJBAoU7AoJUCv4CBsACBtwCBuACB4w3CEQIaCKgMBFgQFBgYFCLQMDMIfAg55D4BcDg/gNAcD+B0DSIMcOgiGEjCYEjgFEhhVCUgQ"))
diff --git a/apps/arrow/icon.js b/apps/arrow/icon.js
index 380728484..917a5c979 100644
--- a/apps/arrow/icon.js
+++ b/apps/arrow/icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))
+require("heatshrink").decompress(atob("kkkwIEBgf8AYMB//4AgN///ggEf4E/wED+EACQN8C4Pgh4TBh8BCYMAvEcEoWD4AEBnk4gFggPHwAXBj1wgIwB88An/Ah3gg/+gF+gH/+EH8Ef/+ABAPvuAIBgnyCIQjBBAMAJAIIEuAICFgIIBh14BAMB8eAg0Ajk8KAXBKAU4jwDBg+ADoIXBg4NBnxPBEgPAgP8gZaBg//KoKLBKAIEBMQMAA"))
diff --git a/apps/astral/app-icon.js b/apps/astral/app-icon.js
index 19d0998ff..d10e7a498 100644
--- a/apps/astral/app-icon.js
+++ b/apps/astral/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY="))
\ No newline at end of file
+require("heatshrink").decompress(atob("kUw4MA///xP5gEH/AMBh//4AHBwF4gEDwEHgEB4fw8EAsf/jEAjPh80AhngjnAgcwAIMB5kA50A+cAmfAtnAhnYmc//8zhln/+c4YjBg0w440Bxk38EB/cP/0B//Dwf/+FxwEf8EGIAJGB2BkCnhiB4EPgF//EDFQIpB+HGgOMnkxwFjh8MsEY4YQHn/x//j//8n/wHYItBCAKFBhgKBKAIQBBgIQC4AQCmAQChkD/v8gcA/wCBBoMA7+39kAPwP/WIMP4aYBCAYhCCAkHAYOAA="))
diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog
index bb2945db4..655916170 100644
--- a/apps/authentiwatch/ChangeLog
+++ b/apps/authentiwatch/ChangeLog
@@ -4,3 +4,4 @@
0.04: Fix tapping at very bottom of list, exit on inactivity
0.05: Add support for bulk importing and exporting tokens
0.06: Add spaces to codes for improved readability (thanks @BartS23)
+0.07: Bangle 2: Improve drag responsiveness and exit on button press
diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js
index 73b8bdeea..05d94fc46 100644
--- a/apps/authentiwatch/app.js
+++ b/apps/authentiwatch/app.js
@@ -1,6 +1,10 @@
-const tokenextraheight = 16;
-var tokendigitsheight = 30;
-var tokenheight = tokendigitsheight + tokenextraheight;
+const COUNTER_TRIANGLE_SIZE = 10;
+const TOKEN_EXTRA_HEIGHT = 16;
+var TOKEN_DIGITS_HEIGHT = 30;
+var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT;
+const PROGRESSBAR_HEIGHT = 3;
+const IDLE_REPEATS = 1; // when idle, the number of extra timed periods to show before hiding
+const SETTINGS = "authentiwatch.json";
// Hash functions
const crypto = require("crypto");
const algos = {
@@ -8,33 +12,24 @@ const algos = {
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
};
-const calculating = "Calculating";
-const notokens = "No tokens";
-const notsupported = "Not supported";
+const CALCULATING = /*LANG*/"Calculating";
+const NO_TOKENS = /*LANG*/"No tokens";
+const NOT_SUPPORTED = /*LANG*/"Not supported";
// sample settings:
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
-var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}};
+var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[], misc:{}};
if (settings.data ) tokens = settings.data ; /* v0.02 settings */
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
-// QR Code Text
-//
-// Example:
-//
-// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret}
-//
-// ${algorithm} : one of SHA1 / SHA256 / SHA512
-// ${digits} : one of 6 / 8
-// ${period} : one of 30 / 60
-// ${url} : a domain name "example.com"
-// ${secret} : the seed code
-
function b32decode(seedstr) {
- // RFC4648
- var i, buf = 0, bitcount = 0, retstr = "";
- for (i in seedstr) {
- var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0);
+ // RFC4648 Base16/32/64 Data Encodings
+ let buf = 0, bitcount = 0, retstr = "";
+ for (let c of seedstr.toUpperCase()) {
+ if (c == '0') c = 'O';
+ if (c == '1') c = 'I';
+ if (c == '8') c = 'B';
+ c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
if (c != -1) {
buf <<= 5;
buf |= c;
@@ -46,195 +41,127 @@ function b32decode(seedstr) {
}
}
}
- var retbuf = new Uint8Array(retstr.length);
- for (i in retstr) {
+ let retbuf = new Uint8Array(retstr.length);
+ for (let i in retstr) {
retbuf[i] = retstr.charCodeAt(i);
}
return retbuf;
}
-function do_hmac(key, message, algo) {
- var a = algos[algo];
- // RFC2104
+
+function hmac(key, message, algo) {
+ let a = algos[algo.toUpperCase()];
+ // RFC2104 HMAC
if (key.length > a.blksz) {
key = a.sha(key);
}
- var istr = new Uint8Array(a.blksz + message.length);
- var ostr = new Uint8Array(a.blksz + a.retsz);
- for (var i = 0; i < a.blksz; ++i) {
- var c = (i < key.length) ? key[i] : 0;
+ let istr = new Uint8Array(a.blksz + message.length);
+ let ostr = new Uint8Array(a.blksz + a.retsz);
+ for (let i = 0; i < a.blksz; ++i) {
+ let c = (i < key.length) ? key[i] : 0;
istr[i] = c ^ 0x36;
ostr[i] = c ^ 0x5C;
}
istr.set(message, a.blksz);
ostr.set(a.sha(istr), a.blksz);
- var ret = a.sha(ostr);
- // RFC4226 dynamic truncation
- var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
+ let ret = a.sha(ostr);
+ // RFC4226 HOTP (dynamic truncation)
+ let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
return v.getUint32(0) & 0x7FFFFFFF;
}
-function hotp(d, token, dohmac) {
- var tick;
+
+function formatOtp(otp, digits) {
+ // add 0 padding
+ let ret = "" + otp % Math.pow(10, digits);
+ while (ret.length < digits) {
+ ret = "0" + ret;
+ }
+ // add a space after every 3rd or 4th digit
+ let re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : ".";
+ return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
+}
+
+function hotp(token) {
+ let d = Date.now();
+ let tick, next;
if (token.period > 0) {
// RFC6238 - timed
- var seconds = Math.floor(d.getTime() / 1000);
- tick = Math.floor(seconds / token.period);
+ tick = Math.floor(Math.floor(d / 1000) / token.period);
+ next = (tick + 1) * token.period * 1000;
} else {
// RFC4226 - counter
tick = -token.period;
+ next = d + 30000;
}
- var msg = new Uint8Array(8);
- var v = new DataView(msg.buffer);
+ let msg = new Uint8Array(8);
+ let v = new DataView(msg.buffer);
v.setUint32(0, tick >> 16 >> 16);
v.setUint32(4, tick & 0xFFFFFFFF);
- var ret = calculating;
- if (dohmac) {
- try {
- var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase());
- ret = "" + hash % Math.pow(10, token.digits);
- while (ret.length < token.digits) {
- ret = "0" + ret;
- }
- // add a space after every 3rd or 4th digit
- var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : ".";
- ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
- } catch(err) {
- ret = notsupported;
- }
+ let ret;
+ try {
+ ret = hmac(b32decode(token.secret), msg, token.algorithm);
+ ret = formatOtp(ret, token.digits);
+ } catch(err) {
+ ret = NOT_SUPPORTED;
}
- return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)};
+ return {hotp:ret, next:next};
}
+// Tokens are displayed in three states:
+// 1. Unselected (state.id<0)
+// 2. Selected, inactive (no code) (state.id>=0,state.hotp.hotp=="")
+// 3. Selected, active (code showing) (state.id>=0,state.hotp.hotp!="")
+var fontszCache = {};
var state = {
- listy: 0,
- prevcur:0,
- curtoken:-1,
- nextTime:0,
- otp:"",
- rem:0,
- hide:0
+ listy:0, // list scroll position
+ id:-1, // current token ID
+ hotp:{hotp:"",next:0}
};
-function drawToken(id, r) {
- var x1 = r.x;
- var y1 = r.y;
- var x2 = r.x + r.w - 1;
- var y2 = r.y + r.h - 1;
- var adj, lbl, sz;
- g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
- Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
- lbl = tokens[id].label.substr(0, 10);
- if (id == state.curtoken) {
- // current token
- g.setColor(g.theme.fgH)
- .setBgColor(g.theme.bgH)
- .setFont("Vector", tokenextraheight)
- // center just below top line
- .setFontAlign(0, -1, 0);
- adj = y1;
- } else {
- g.setColor(g.theme.fg)
- .setBgColor(g.theme.bg);
- sz = tokendigitsheight;
+function sizeFont(id, txt, w) {
+ let sz = fontszCache[id];
+ if (!sz) {
+ sz = TOKEN_DIGITS_HEIGHT;
do {
g.setFont("Vector", sz--);
- } while (g.stringWidth(lbl) > r.w);
- // center in box
- g.setFontAlign(0, 0, 0);
- adj = (y1 + y2) / 2;
+ } while (g.stringWidth(txt) > w);
+ fontszCache[id] = ++sz;
}
- g.clearRect(x1, y1, x2, y2)
- .drawString(lbl, (x1 + x2) / 2, adj, false);
- if (id == state.curtoken) {
- if (tokens[id].period > 0) {
- // timed - draw progress bar
- let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period);
- g.fillRect(x1, y2 - 4, xr, y2 - 1);
- adj = 0;
- } else {
- // counter - draw triangle as swipe hint
- let yc = (y1 + y2) / 2;
- g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
- adj = 12;
- }
- // digits just below label
- sz = tokendigitsheight;
- do {
- g.setFont("Vector", sz--);
- } while (g.stringWidth(state.otp) > (r.w - adj));
- g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
- }
- // shaded lines top and bottom
- g.setColor(0.5, 0.5, 0.5)
- .drawLine(x1, y1, x2, y1)
- .drawLine(x1, y2, x2, y2)
- .setClipRect(0, 0, g.getWidth(), g.getHeight());
+ g.setFont("Vector", sz);
}
-function draw() {
- var timerfn = exitApp;
- var timerdly = 10000;
- var d = new Date();
- if (state.curtoken != -1) {
- var t = tokens[state.curtoken];
- if (state.otp == calculating) {
- state.otp = hotp(d, t, true).hotp;
- }
- if (d.getTime() > state.nextTime) {
- if (state.hide == 0) {
- // auto-hide the current token
- if (state.curtoken != -1) {
- state.prevcur = state.curtoken;
- state.curtoken = -1;
+tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
+half = n => Math.floor(n / 2);
+
+function timerCalc() {
+ let timerfn = exitApp;
+ let timerdly = 10000;
+ if (state.id >= 0 && state.hotp.hotp != "") {
+ if (tokens[state.id].period > 0) {
+ // timed HOTP
+ if (state.hotp.next < Date.now()) {
+ if (state.cnt > 0) {
+ state.cnt--;
+ state.hotp = hotp(tokens[state.id]);
+ } else {
+ state.hotp.hotp = "";
}
- state.nextTime = 0;
+ timerdly = 1;
+ timerfn = updateCurrentToken;
} else {
- // time to generate a new token
- var r = hotp(d, t, state.otp != "");
- state.nextTime = r.next;
- state.otp = r.hotp;
- if (t.period <= 0) {
- state.hide = 1;
- }
- state.hide--;
- }
- }
- state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000));
- }
- if (tokens.length > 0) {
- var drewcur = false;
- var id = Math.floor(state.listy / tokenheight);
- var y = id * tokenheight + Bangle.appRect.y - state.listy;
- while (id < tokens.length && y < Bangle.appRect.y2) {
- drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight});
- if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
- drewcur = true;
- }
- id += 1;
- y += tokenheight;
- }
- if (drewcur) {
- // the current token has been drawn - schedule a redraw
- if (tokens[state.curtoken].period > 0) {
- timerdly = (state.otp == calculating) ? 1 : 1000; // timed
- } else {
- timerdly = state.nexttime - d.getTime(); // counter
- }
- timerfn = draw;
- if (tokens[state.curtoken].period <= 0) {
- state.hide = 0;
+ timerdly = 1000;
+ timerfn = updateProgressBar;
}
} else {
- // de-select the current token if it is scrolled out of view
- if (state.curtoken != -1) {
- state.prevcur = state.curtoken;
- state.curtoken = -1;
+ // counter HOTP
+ if (state.cnt > 0) {
+ state.cnt--;
+ timerdly = 30000;
+ } else {
+ state.hotp.hotp = "";
+ timerdly = 1;
}
- state.nexttime = 0;
+ timerfn = updateCurrentToken;
}
- } else {
- g.setFont("Vector", tokendigitsheight)
- .setFontAlign(0, 0, 0)
- .drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
}
if (state.drawtimer) {
clearTimeout(state.drawtimer);
@@ -242,97 +169,236 @@ function draw() {
state.drawtimer = setTimeout(timerfn, timerdly);
}
-function onTouch(zone, e) {
- if (e) {
- var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight);
- if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
- id = -1;
- }
- if (state.curtoken != id) {
- if (id != -1) {
- var y = id * tokenheight - state.listy;
- if (y < 0) {
- state.listy += y;
- y = 0;
+function updateCurrentToken() {
+ drawToken(state.id);
+ timerCalc();
+}
+
+function updateProgressBar() {
+ drawProgressBar();
+ timerCalc();
+}
+
+function drawProgressBar() {
+ let id = state.id;
+ if (id >= 0 && tokens[id].period > 0) {
+ let rem = Math.min(tokens[id].period, Math.floor((state.hotp.next - Date.now()) / 1000));
+ if (rem >= 0) {
+ let y1 = tokenY(id);
+ let y2 = y1 + TOKEN_HEIGHT - 1;
+ if (y2 >= AR.y && y1 <= AR.y2) {
+ // token visible
+ y1 = y2 - PROGRESSBAR_HEIGHT;
+ if (y1 <= AR.y2)
+ {
+ // progress bar visible
+ y2 = Math.min(y2, AR.y2);
+ let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x;
+ g.setColor(g.theme.fgH)
+ .setBgColor(g.theme.bgH)
+ .fillRect(AR.x, y1, xr, y2)
+ .clearRect(xr + 1, y1, AR.x2, y2);
}
- y += tokenheight;
- if (y > Bangle.appRect.h) {
- state.listy += (y - Bangle.appRect.h);
- }
- state.otp = "";
+ } else {
+ // token not visible
+ state.id = -1;
}
- state.nextTime = 0;
- state.curtoken = id;
- state.hide = 2;
}
}
- draw();
+}
+
+// id = token ID number (0...)
+function drawToken(id) {
+ let x1 = AR.x;
+ let y1 = tokenY(id);
+ let x2 = AR.x2;
+ let y2 = y1 + TOKEN_HEIGHT - 1;
+ let lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : "";
+ let adj;
+ g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2));
+ if (id === state.id) {
+ g.setColor(g.theme.fgH)
+ .setBgColor(g.theme.bgH);
+ } else {
+ g.setColor(g.theme.fg)
+ .setBgColor(g.theme.bg);
+ }
+ if (id == state.id && state.hotp.hotp != "") {
+ // small label centered just below top line
+ g.setFont("Vector", TOKEN_EXTRA_HEIGHT)
+ .setFontAlign(0, -1, 0);
+ adj = y1;
+ } else {
+ // large label centered in box
+ sizeFont("l" + id, lbl, AR.w);
+ g.setFontAlign(0, 0, 0);
+ adj = half(y1 + y2);
+ }
+ g.clearRect(x1, y1, x2, y2)
+ .drawString(lbl, half(x1 + x2), adj, false);
+ if (id == state.id && state.hotp.hotp != "") {
+ adj = 0;
+ if (tokens[id].period <= 0) {
+ // counter - draw triangle as swipe hint
+ let yc = half(y1 + y2);
+ adj = COUNTER_TRIANGLE_SIZE;
+ g.fillPoly([AR.x, yc, AR.x + adj, yc - adj, AR.x + adj, yc + adj]);
+ adj += 2;
+ }
+ // digits just below label
+ x1 = half(x1 + adj + x2);
+ y1 += TOKEN_EXTRA_HEIGHT;
+ if (state.hotp.hotp == CALCULATING) {
+ sizeFont("c", CALCULATING, AR.w - adj);
+ g.drawString(CALCULATING, x1, y1, false)
+ .flip();
+ state.hotp = hotp(tokens[id]);
+ g.clearRect(AR.x + adj, y1, AR.x2, y2);
+ }
+ sizeFont("d" + id, state.hotp.hotp, AR.w - adj);
+ g.drawString(state.hotp.hotp, x1, y1, false);
+ if (tokens[id].period > 0) {
+ drawProgressBar();
+ }
+ }
+ g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
+}
+
+function changeId(id) {
+ if (id != state.id) {
+ state.hotp.hotp = CALCULATING;
+ let pid = state.id;
+ state.id = id;
+ if (pid >= 0) {
+ drawToken(pid);
+ }
+ if (id >= 0) {
+ drawToken( id);
+ }
+ }
}
function onDrag(e) {
- if (e.x > g.getWidth() || e.y > g.getHeight()) return;
- if (e.dx == 0 && e.dy == 0) return;
- var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h);
- state.listy = Math.max(0, newy);
- draw();
+ state.cnt = IDLE_REPEATS;
+ if (e.b != 0 && e.dy != 0) {
+ let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
+ if (state.listy != y) {
+ let id, dy = state.listy - y;
+ state.listy = y;
+ g.setClipRect(AR.x, AR.y, AR.x2, AR.y2)
+ .scroll(0, dy);
+ if (dy > 0) {
+ id = Math.floor((state.listy + dy) / TOKEN_HEIGHT);
+ y = tokenY(id + 1);
+ do {
+ drawToken(id);
+ id--;
+ y -= TOKEN_HEIGHT;
+ } while (y > AR.y);
+ }
+ if (dy < 0) {
+ id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT);
+ y = tokenY(id);
+ while (y < AR.y2) {
+ drawToken(id);
+ id++;
+ y += TOKEN_HEIGHT;
+ }
+ }
+ }
+ }
+ if (e.b == 0) {
+ timerCalc();
+ }
+}
+
+function onTouch(zone, e) {
+ state.cnt = IDLE_REPEATS;
+ if (e) {
+ let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT);
+ if (id == state.id || tokens.length == 0 || id >= tokens.length) {
+ id = -1;
+ }
+ if (state.id != id) {
+ if (id >= 0) {
+ // scroll token into view if necessary
+ let dy = 0;
+ let y = id * TOKEN_HEIGHT - state.listy;
+ if (y < 0) {
+ dy -= y;
+ y = 0;
+ }
+ y += TOKEN_HEIGHT;
+ if (y > AR.h) {
+ dy -= (y - AR.h);
+ }
+ onDrag({b:1, dy:dy});
+ }
+ changeId(id);
+ }
+ }
+ timerCalc();
}
function onSwipe(e) {
- if (e == 1) {
+ state.cnt = IDLE_REPEATS;
+ switch (e) {
+ case 1:
exitApp();
+ break;
+ case -1:
+ if (state.id >= 0 && tokens[state.id].period <= 0) {
+ tokens[state.id].period--;
+ require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc});
+ state.hotp.hotp = CALCULATING;
+ drawToken(state.id);
+ }
+ break;
}
- if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
- tokens[state.curtoken].period--;
- let newsettings={tokens:tokens,misc:settings.misc};
- require("Storage").writeJSON("authentiwatch.json", newsettings);
- state.nextTime = 0;
- state.otp = "";
- state.hide = 2;
- }
- draw();
+ timerCalc();
}
-function bangle1Btn(e) {
+function bangleBtn(e) {
+ state.cnt = IDLE_REPEATS;
if (tokens.length > 0) {
- if (state.curtoken == -1) {
- state.curtoken = state.prevcur;
- } else {
- switch (e) {
- case -1: state.curtoken--; break;
- case 1: state.curtoken++; break;
- }
- }
- state.curtoken = Math.max(state.curtoken, 0);
- state.curtoken = Math.min(state.curtoken, tokens.length - 1);
- state.listy = state.curtoken * tokenheight;
- state.listy -= (Bangle.appRect.h - tokenheight) / 2;
- state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h);
- state.listy = Math.max(state.listy, 0);
- var fakee = {};
- fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y;
- state.curtoken = -1;
- state.nextTime = 0;
- onTouch(0, fakee);
- } else {
- draw(); // resets idle timer
+ let id = E.clip(state.id + e, 0, tokens.length - 1);
+ onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))});
+ changeId(id);
+ drawProgressBar();
}
+ timerCalc();
}
function exitApp() {
+ if (state.drawtimer) {
+ clearTimeout(state.drawtimer);
+ }
Bangle.showLauncher();
}
Bangle.on('touch', onTouch);
Bangle.on('drag' , onDrag );
Bangle.on('swipe', onSwipe);
-if (typeof BTN2 == 'number') {
- setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
- setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
- setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true});
+if (typeof BTN1 == 'number') {
+ if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
+ setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true});
+ setWatch(()=>exitApp() , BTN2, {edge:"falling", debounce:50});
+ setWatch(()=>bangleBtn( 1), BTN3, {edge:"rising" , debounce:50, repeat:true});
+ } else {
+ setWatch(()=>exitApp() , BTN1, {edge:"falling", debounce:50});
+ }
}
Bangle.loadWidgets();
-
-// Clear the screen once, at startup
+const AR = Bangle.appRect;
+// draw the initial display
g.clear();
-draw();
+if (tokens.length > 0) {
+ state.listy = AR.h;
+ onDrag({b:1, dy:AR.h});
+} else {
+ g.setFont("Vector", TOKEN_DIGITS_HEIGHT)
+ .setFontAlign(0, 0, 0)
+ .drawString(NO_TOKENS, AR.x + half(AR.w), AR.y + half(AR.h), false);
+}
+timerCalc();
Bangle.drawWidgets();
diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html
index 5ee32fd8e..7d567d34f 100644
--- a/apps/authentiwatch/interface.html
+++ b/apps/authentiwatch/interface.html
@@ -54,9 +54,9 @@ var tokens = settings.tokens;
*/
function base32clean(val, nows) {
var ret = val.replaceAll(/\s+/g, ' ');
- ret = ret.replaceAll(/0/g, 'O');
- ret = ret.replaceAll(/1/g, 'I');
- ret = ret.replaceAll(/8/g, 'B');
+ ret = ret.replaceAll('0', 'O');
+ ret = ret.replaceAll('1', 'I');
+ ret = ret.replaceAll('8', 'B');
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
if (nows) {
ret = ret.replaceAll(/\s+/g, '');
@@ -81,9 +81,9 @@ function b32encode(str) {
function b32decode(seedstr) {
// RFC4648
- var i, buf = 0, bitcount = 0, ret = '';
- for (i in seedstr) {
- var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
+ var buf = 0, bitcount = 0, ret = '';
+ for (var c of seedstr.toUpperCase()) {
+ c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
if (c != -1) {
buf <<= 5;
buf |= c;
@@ -405,7 +405,7 @@ class proto3decoder {
constructor(str) {
this.buf = [];
for (let i in str) {
- this.buf = this.buf.concat(str.charCodeAt(i));
+ this.buf.push(str.charCodeAt(i));
}
}
getVarint() {
@@ -487,7 +487,7 @@ function startScan(handler,cancel) {
document.body.className = 'scanning';
navigator.mediaDevices
.getUserMedia({video:{facingMode:'environment'}})
- .then(function(stream){
+ .then(stream => {
scanning=true;
video.setAttribute('playsinline',true);
video.srcObject = stream;
diff --git a/apps/authentiwatch/metadata.json b/apps/authentiwatch/metadata.json
index b4ed34a12..36e1ea34d 100644
--- a/apps/authentiwatch/metadata.json
+++ b/apps/authentiwatch/metadata.json
@@ -4,7 +4,7 @@
"shortName": "AuthWatch",
"icon": "app.png",
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
- "version": "0.06",
+ "version": "0.07",
"description": "Google Authenticator compatible tool.",
"tags": "tool",
"interface": "interface.html",
diff --git a/apps/authentiwatch/screenshot1.png b/apps/authentiwatch/screenshot1.png
index c7ca744b4..c3ac0e3b7 100644
Binary files a/apps/authentiwatch/screenshot1.png and b/apps/authentiwatch/screenshot1.png differ
diff --git a/apps/authentiwatch/screenshot2.png b/apps/authentiwatch/screenshot2.png
index 8156dd3e8..26bafdbb2 100644
Binary files a/apps/authentiwatch/screenshot2.png and b/apps/authentiwatch/screenshot2.png differ
diff --git a/apps/authentiwatch/screenshot3.png b/apps/authentiwatch/screenshot3.png
index 6d14e0b96..80f2fb172 100644
Binary files a/apps/authentiwatch/screenshot3.png and b/apps/authentiwatch/screenshot3.png differ
diff --git a/apps/authentiwatch/screenshot4.png b/apps/authentiwatch/screenshot4.png
index 7576e1aff..00756228d 100644
Binary files a/apps/authentiwatch/screenshot4.png and b/apps/authentiwatch/screenshot4.png differ
diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog
new file mode 100644
index 000000000..5fb78710b
--- /dev/null
+++ b/apps/bikespeedo/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/bikespeedo/Hochrad120px.gif b/apps/bikespeedo/Hochrad120px.gif
new file mode 100644
index 000000000..1952cf44f
Binary files /dev/null and b/apps/bikespeedo/Hochrad120px.gif differ
diff --git a/apps/bikespeedo/Hochrad120px.png b/apps/bikespeedo/Hochrad120px.png
new file mode 100644
index 000000000..2c2d4e1ef
Binary files /dev/null and b/apps/bikespeedo/Hochrad120px.png differ
diff --git a/apps/bikespeedo/README.md b/apps/bikespeedo/README.md
new file mode 100644
index 000000000..7d271a022
--- /dev/null
+++ b/apps/bikespeedo/README.md
@@ -0,0 +1,12 @@
+## GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude...
+
+...all taken from internal sources.
+
+#### To speed-up GPS reception it is strongly recommended to upload AGPS data with ["Assisted GPS Update"](https://banglejs.com/apps/?id=assistedgps)
+
+#### If "CALIB!" is shown on the display or the compass heading differs too much from GPS heading, compass calibration should be done with the ["Navigation Compass" App](https://banglejs.com/apps/?id=magnav)
+
+**Credits:**
+Bike Speedometer App by github.com/HilmarSt
+Big parts of the software are based on github.com/espruino/BangleApps/tree/master/apps/speedalt
+Compass and Compass Calibration based on github.com/espruino/BangleApps/tree/master/apps/magnav
diff --git a/apps/bikespeedo/Screenshot.png b/apps/bikespeedo/Screenshot.png
new file mode 100644
index 000000000..fd27728e4
Binary files /dev/null and b/apps/bikespeedo/Screenshot.png differ
diff --git a/apps/bikespeedo/app-icon.js b/apps/bikespeedo/app-icon.js
new file mode 100644
index 000000000..c34f52cfb
--- /dev/null
+++ b/apps/bikespeedo/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+64A/AC+sF1uBgAwsq1W1krGEmswIFDlcAFoMrqyGjlcrGAQDB1guBBQJghKYZZCMYhqBlYugFAesgAuFYgQIHAE2sYMZDfwIABbgIuowMAqwABb4wAjFVQAEqyMrF4cAlYABqwypR4RgBwIyplYnF1hnBGIo8BAAQvhGIj6C1hpBgChBGCqGBqwdCRQQnCB4gJBGAgtWc4WBPoi9JH4ILBGYQATPoRHJRYoACwLFBLi4tGLIyLEA5QuPCoYpEMhBBBGDIuFgArIYQIUHA4b+GABLUBAwoQIXorDGI5RNGCB9WRQ0AJwwHGDxChOH4oDCRI4/GXpAaB1gyLEwlWKgTrBT46ALCogQKZoryFCwzgGBgz/NZpaQHHBCdEF5hKBBxWBUwoGBgEAEoIyHHYesBg7aBJQ7SBBAIvEIIJCBD4IFBgBIGEAcAUA8rGAIWHS4QvDCAJAHG4JfRCgKCFeAovCdRIiBDYq/NABi0Cfo5IEBgjUGACZ6BqwcGwLxBFYRsEHIKBIJwLkBNoIHDF468GYgIBBXY4EDE4IHDYwSwCN4IGBCIp5CJYtWgBZBHAgFEMoRjEE4QDCLYJUEUoaCBPYoQCgA4FGozxFLYwfEQgqrGexIYFBoxbDS4YHCIAYVEEAZcCYwwvGfoQHEcwQHHIg9WIAS9BIoYYESoowIABQuBUgg1DVwwACEpIwBChDLFDQ5JLlZnHJAajBQwgLEO4LDBHKAhBFxQxFCIIACAwadLHgJJBAAUrQJxYFAAbKPCwRGCCqAAm"))
diff --git a/apps/bikespeedo/app.js b/apps/bikespeedo/app.js
new file mode 100644
index 000000000..0c5680c9d
--- /dev/null
+++ b/apps/bikespeedo/app.js
@@ -0,0 +1,546 @@
+// Bike Speedometer by https://github.com/HilmarSt
+// Big parts of this software are based on https://github.com/espruino/BangleApps/tree/master/apps/speedalt
+// Compass and Compass Calibration based on https://github.com/espruino/BangleApps/tree/master/apps/magnav
+
+const BANGLEJS2 = 1;
+const screenH = g.getHeight();
+const screenYstart = 24; // 0..23 for widgets
+const screenY_Half = screenH / 2 + screenYstart;
+const screenW = g.getWidth();
+const screenW_Half = screenW / 2;
+const fontFactorB2 = 2/3;
+const colfg=g.theme.fg, colbg=g.theme.bg;
+const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
+
+var altiGPS=0, altiBaro=0;
+var hdngGPS=0, hdngCompass=0, calibrateCompass=false;
+
+/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
+var KalmanFilter = (function () {
+ 'use strict';
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ function _defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ function _createClass(Constructor, protoProps, staticProps) {
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) _defineProperties(Constructor, staticProps);
+ return Constructor;
+ }
+
+ /**
+ * KalmanFilter
+ * @class
+ * @author Wouter Bulten
+ * @see {@link http://github.com/wouterbulten/kalmanjs}
+ * @version Version: 1.0.0-beta
+ * @copyright Copyright 2015-2018 Wouter Bulten
+ * @license MIT License
+ * @preserve
+ */
+ var KalmanFilter =
+ /*#__PURE__*/
+ function () {
+ /**
+ * Create 1-dimensional kalman filter
+ * @param {Number} options.R Process noise
+ * @param {Number} options.Q Measurement noise
+ * @param {Number} options.A State vector
+ * @param {Number} options.B Control vector
+ * @param {Number} options.C Measurement vector
+ * @return {KalmanFilter}
+ */
+ function KalmanFilter() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref$R = _ref.R,
+ R = _ref$R === void 0 ? 1 : _ref$R,
+ _ref$Q = _ref.Q,
+ Q = _ref$Q === void 0 ? 1 : _ref$Q,
+ _ref$A = _ref.A,
+ A = _ref$A === void 0 ? 1 : _ref$A,
+ _ref$B = _ref.B,
+ B = _ref$B === void 0 ? 0 : _ref$B,
+ _ref$C = _ref.C,
+ C = _ref$C === void 0 ? 1 : _ref$C;
+
+ _classCallCheck(this, KalmanFilter);
+
+ this.R = R; // noise power desirable
+
+ this.Q = Q; // noise power estimated
+
+ this.A = A;
+ this.C = C;
+ this.B = B;
+ this.cov = NaN;
+ this.x = NaN; // estimated signal without noise
+ }
+ /**
+ * Filter a new value
+ * @param {Number} z Measurement
+ * @param {Number} u Control
+ * @return {Number}
+ */
+
+
+ _createClass(KalmanFilter, [{
+ key: "filter",
+ value: function filter(z) {
+ var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+
+ if (isNaN(this.x)) {
+ this.x = 1 / this.C * z;
+ this.cov = 1 / this.C * this.Q * (1 / this.C);
+ } else {
+ // Compute prediction
+ var predX = this.predict(u);
+ var predCov = this.uncertainty(); // Kalman gain
+
+ var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction
+
+ this.x = predX + K * (z - this.C * predX);
+ this.cov = predCov - K * this.C * predCov;
+ }
+
+ return this.x;
+ }
+ /**
+ * Predict next value
+ * @param {Number} [u] Control
+ * @return {Number}
+ */
+
+ }, {
+ key: "predict",
+ value: function predict() {
+ var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ return this.A * this.x + this.B * u;
+ }
+ /**
+ * Return uncertainty of filter
+ * @return {Number}
+ */
+
+ }, {
+ key: "uncertainty",
+ value: function uncertainty() {
+ return this.A * this.cov * this.A + this.R;
+ }
+ /**
+ * Return the last filtered measurement
+ * @return {Number}
+ */
+
+ }, {
+ key: "lastMeasurement",
+ value: function lastMeasurement() {
+ return this.x;
+ }
+ /**
+ * Set measurement noise Q
+ * @param {Number} noise
+ */
+
+ }, {
+ key: "setMeasurementNoise",
+ value: function setMeasurementNoise(noise) {
+ this.Q = noise;
+ }
+ /**
+ * Set the process noise R
+ * @param {Number} noise
+ */
+
+ }, {
+ key: "setProcessNoise",
+ value: function setProcessNoise(noise) {
+ this.R = noise;
+ }
+ }]);
+
+ return KalmanFilter;
+ }();
+
+ return KalmanFilter;
+
+}());
+
+
+//==================================== MAIN ====================================
+
+var lf = {fix:0,satellites:0};
+var showMax = 0; // 1 = display the max values. 0 = display the cur fix
+var canDraw = 1;
+var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
+var sec; // actual seconds for testing purposes
+
+var max = {};
+max.spd = 0;
+max.alt = 0;
+max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
+
+var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
+
+var wp = {}; // Waypoint to use for distance from cur position.
+var SATinView = 0;
+
+function radians(a) {
+ return a*Math.PI/180;
+}
+
+function distance(a,b){
+ var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
+ var y = radians(b.lat-a.lat);
+
+ // Distance in selected units
+ var d = Math.sqrt(x*x + y*y) * 6371000;
+ d = (d/parseFloat(cfg.dist)).toFixed(2);
+ if ( d >= 100 ) d = parseFloat(d).toFixed(1);
+ if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
+
+ return d;
+}
+
+function drawFix(dat) {
+
+ if (!canDraw) return;
+
+ g.clearRect(0,screenYstart,screenW,screenH);
+
+ var v = '';
+ var u='';
+
+ // Primary Display
+ v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
+
+ // Primary Units
+ u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units;
+
+ drawPrimary(v,u);
+
+ // Secondary Display
+ v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString();
+
+ // Secondary Units
+ u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit;
+
+ drawSecondary(v,u);
+
+ // Time
+ drawTime();
+
+ //Sats
+ if ( dat.age > 10 ) {
+ if ( dat.age > 90 ) dat.age = '>90';
+ drawSats('Age:'+dat.age);
+ }
+ else if (!BANGLEJS2) {
+ drawSats('Sats:'+dat.sats);
+ } else {
+ if (lf.fix) {
+ drawSats('Sats:'+dat.sats);
+ } else {
+ drawSats('View:' + SATinView);
+ }
+ }
+ g.reset();
+}
+
+
+function drawClock() {
+ if (!canDraw) return;
+ g.clearRect(0,screenYstart,screenW,screenH);
+ drawTime();
+ g.reset();
+}
+
+
+function drawPrimary(n,u) {
+ //if(emulator)console.log("\n1: " + n +" "+ u);
+ var s=40; // Font size
+ var l=n.length;
+
+ if ( l <= 7 ) s=48;
+ if ( l <= 6 ) s=55;
+ if ( l <= 5 ) s=66;
+ if ( l <= 4 ) s=85;
+ if ( l <= 3 ) s=110;
+
+ // X -1=left (default), 0=center, 1=right
+ // Y -1=top (default), 0=center, 1=bottom
+ g.setFontAlign(0,-1); // center, top
+ if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
+ if (BANGLEJS2) s *= fontFactorB2;
+ g.setFontVector(s);
+ g.drawString(n, screenW_Half - 10, screenYstart);
+
+ // Primary Units
+ s = 35; // Font size
+ g.setFontAlign(1,-1,3); // right, top, rotate
+ g.setColor(col1);
+ if (BANGLEJS2) s = 20;
+ g.setFontVector(s);
+ g.drawString(u, screenW - 20, screenYstart + 2);
+}
+
+
+function drawSecondary(n,u) {
+ //if(emulator)console.log("2: " + n +" "+ u);
+
+ if (calibrateCompass) hdngCompass = "CALIB!";
+ else hdngCompass +="°";
+
+ g.setFontAlign(0,1);
+ g.setColor(col1);
+
+ g.setFontVector(12).drawString("Altitude GPS / Barometer", screenW_Half - 5, screenY_Half - 10);
+ g.setFontVector(20);
+ g.drawString(n+" "+u+" / "+altiBaro+" "+u, screenW_Half, screenY_Half + 11);
+
+ g.setFontVector(12).drawString("Heading GPS / Compass", screenW_Half - 10, screenY_Half + 26);
+ g.setFontVector(20);
+ g.drawString(hdngGPS+"° / "+hdngCompass, screenW_Half, screenY_Half + 47);
+}
+
+
+function drawTime() {
+ var x = 0, y = screenH;
+ g.setFontAlign(-1,1); // left, bottom
+ g.setFont("6x8", 2);
+
+ g.setColor(colbg);
+ g.drawString(time,x+1,y); // clear old time
+
+ time = require("locale").time(new Date(),1);
+
+ g.setColor(colfg); // draw new time
+ g.drawString(time,x+2,y);
+}
+
+
+function drawSats(sats) {
+
+ g.setColor(col1);
+ g.setFont("6x8", 2);
+ g.setFontAlign(1,1); //right, bottom
+ g.drawString(sats,screenW,screenH);
+
+ g.setFontVector(18);
+ g.setColor(col1);
+
+ if ( cfg.modeA == 1 ) {
+ if ( showMax ) {
+ g.setFontAlign(0,1); //centre, bottom
+ g.drawString('MAX',120,164);
+ }
+ }
+}
+
+function onGPS(fix) {
+
+ if ( emulator ) {
+ fix.fix = 1;
+ fix.speed = Math.random()*30; // calmed by Kalman filter if cfg.spdFilt
+ fix.alt = Math.random()*200 -20; // calmed by Kalman filter if cfg.altFilt
+ fix.lat = 50.59; // google.de/maps/@50.59,8.53,17z
+ fix.lon = 8.53;
+ fix.course = 365;
+ fix.satellites = sec;
+ fix.time = new Date();
+ fix.smoothed = 0;
+ }
+
+ var m;
+
+ var sp = '---';
+ var al = '---';
+ var di = '---';
+ var age = '---';
+
+ if (fix.fix) lf = fix;
+
+ hdngGPS = lf.course;
+ if (isNaN(hdngGPS)) hdngGPS = "---";
+ else if (0 == hdngGPS) hdngGPS = "0?";
+ else hdngGPS = hdngGPS.toFixed(0);
+
+ if (emulator) hdngCompass = hdngGPS;
+ if (emulator) altiBaro = lf.alt.toFixed(0);
+
+ if (lf.fix) {
+
+ if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw);
+
+ // Smooth data
+ if ( lf.smoothed !== 1 ) {
+ if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
+ if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
+ lf.smoothed = 1;
+ if ( max.n <= 15 ) max.n++;
+ }
+
+
+ // Speed
+ if ( cfg.spd == 0 ) {
+ m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
+ sp = parseFloat(m[1]);
+ cfg.spd_unit = m[2];
+ }
+ else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
+
+ if ( sp < 10 ) sp = sp.toFixed(1);
+ else sp = Math.round(sp);
+ if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
+
+ // Altitude
+ al = lf.alt;
+ al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
+ if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al);
+
+ // Distance to waypoint
+ di = distance(lf,wp);
+ if (isNaN(di)) di = 0;
+
+ // Age of last fix (secs)
+ age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
+ }
+
+ if ( cfg.modeA == 1 ) {
+ if ( showMax )
+ drawFix({
+ speed:max.spd,
+ sats:lf.satellites,
+ alt:max.alt,
+ alt_units:cfg.alt_unit,
+ age:age,
+ fix:lf.fix
+ }); // Speed and alt maximums
+ else
+ drawFix({
+ speed:sp,
+ sats:lf.satellites,
+ alt:al,
+ alt_units:cfg.alt_unit,
+ age:age,
+ fix:lf.fix
+ }); // Show speed/altitude
+ }
+}
+
+function setButtons(){
+ setWatch(_=>load(), BTN1);
+
+onGPS(lf);
+}
+
+
+function updateClock() {
+ if (!canDraw) return;
+ drawTime();
+ g.reset();
+
+ if ( emulator ) {
+ max.spd++; max.alt++;
+ d=new Date(); sec=d.getSeconds();
+ onGPS(lf);
+ }
+}
+
+
+
+//###
+let cfg = {};
+cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
+cfg.spd_unit = 'km/h'; // Displayed speed unit
+cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
+cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
+cfg.dist = 1000; // Multiplier for distnce unit conversions.
+cfg.dist_unit = 'km'; // Displayed distnce units
+cfg.modeA = 1;
+cfg.primSpd = 1; // 1 = Spd in primary, 0 = Spd in secondary
+
+cfg.spdFilt = false;
+cfg.altFilt = false;
+
+if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
+if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
+
+function onGPSraw(nmea) {
+ var nofGP = 0, nofBD = 0, nofGL = 0;
+ if (nmea.slice(3,6) == "GSV") {
+ // console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
+ if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
+ if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
+ if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
+ SATinView = nofGP + nofBD + nofGL;
+ } }
+if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
+
+function onPressure(dat) { altiBaro = dat.altitude.toFixed(0); }
+
+Bangle.setBarometerPower(1); // needs some time...
+g.clearRect(0,screenYstart,screenW,screenH);
+onGPS(lf);
+Bangle.setGPSPower(1);
+Bangle.on('GPS', onGPS);
+Bangle.on('pressure', onPressure);
+
+Bangle.setCompassPower(1);
+var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
+if (!CALIBDATA) calibrateCompass = true;
+function Compass_tiltfixread(O,S){
+ "ram";
+ //console.log(O.x+" "+O.y+" "+O.z);
+ var m = Bangle.getCompass();
+ var g = Bangle.getAccel();
+ m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
+ var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
+ if (d<0) d+=360;
+ var phi = Math.atan(-g.x/-g.z);
+ var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
+ var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
+ var costheta = Math.cos(theta), sintheta = Math.sin(theta);
+ var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
+ var yh = m.dz*sinphi - m.dx*cosphi;
+ var psi = Math.atan2(yh,xh)*180/Math.PI;
+ if (psi<0) psi+=360;
+ return psi;
+}
+var Compass_heading = 0;
+function Compass_newHeading(m,h){
+ var s = Math.abs(m - h);
+ var delta = (m>h)?1:-1;
+ if (s>=180){s=360-s; delta = -delta;}
+ if (s<2) return h;
+ var hd = h + delta*(1 + Math.round(s/5));
+ if (hd<0) hd+=360;
+ if (hd>360)hd-= 360;
+ return hd;
+}
+function Compass_reading() {
+ "ram";
+ var d = Compass_tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
+ Compass_heading = Compass_newHeading(d,Compass_heading);
+ hdngCompass = Compass_heading.toFixed(0);
+}
+if (!calibrateCompass) setInterval(Compass_reading,200);
+
+setButtons();
+if (emulator) setInterval(updateClock, 2000);
+else setInterval(updateClock, 10000);
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/bikespeedo/app.png b/apps/bikespeedo/app.png
new file mode 100644
index 000000000..50f242b47
Binary files /dev/null and b/apps/bikespeedo/app.png differ
diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json
new file mode 100644
index 000000000..7dea28649
--- /dev/null
+++ b/apps/bikespeedo/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "bikespeedo",
+ "name": "Bike Speedometer (beta)",
+ "shortName": "Bike Speedomet.",
+ "version": "0.01",
+ "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
+ "icon": "app.png",
+ "screenshots": [{"url":"Screenshot.png"}],
+ "type": "app",
+ "tags": "tool,cycling,bicycle,outdoors,sport",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"bikespeedo.app.js","url":"app.js"},
+ {"name":"bikespeedo.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog
index e52015f04..e9b98e08c 100644
--- a/apps/bledetect/ChangeLog
+++ b/apps/bledetect/ChangeLog
@@ -1,3 +1,4 @@
0.01: New App!
0.02: Fixed issue with wrong device informations
0.03: Ensure manufacturer:undefined doesn't overflow screen
+0.04: Set Bangle.js 2 compatible, show widgets
diff --git a/apps/bledetect/bledetect.js b/apps/bledetect/bledetect.js
index ca8699f9a..f3fc70e92 100644
--- a/apps/bledetect/bledetect.js
+++ b/apps/bledetect/bledetect.js
@@ -5,6 +5,7 @@ let menu = {
function showMainMenu() {
menu["< Back"] = () => load();
+ Bangle.drawWidgets();
return E.showMenu(menu);
}
@@ -55,5 +56,6 @@ function waitMessage() {
E.showMessage("scanning");
}
+Bangle.loadWidgets();
scan();
waitMessage();
diff --git a/apps/bledetect/metadata.json b/apps/bledetect/metadata.json
index f5e0ffb19..0c30fe8f6 100644
--- a/apps/bledetect/metadata.json
+++ b/apps/bledetect/metadata.json
@@ -2,11 +2,11 @@
"id": "bledetect",
"name": "BLE Detector",
"shortName": "BLE Detector",
- "version": "0.03",
+ "version": "0.04",
"description": "Detect BLE devices and show some informations.",
"icon": "bledetect.png",
"tags": "app,bluetooth,tool",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bledetect.app.js","url":"bledetect.js"},
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index 4c3d3b930..87b5f7def 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -46,3 +46,7 @@
0.40: Bootloader now rebuilds for new firmware versions
0.41: Add Keyboard and Mouse Bluetooth HID option
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname..boot.js
+0.43: Fix Gadgetbridge handling with Programmable:off
+0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)
+0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
+0.46: Fix no clock found error on Bangle.js 2
diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js
index 3cf885ac9..45e271f30 100644
--- a/apps/boot/bootloader.js
+++ b/apps/boot/bootloader.js
@@ -14,6 +14,6 @@ if (!clockApp) {
if (clockApp)
clockApp = require("Storage").read(clockApp.src);
}
-if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, BTN2, {repeat:false,edge:"falling"});`;
+if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
eval(clockApp);
delete clockApp;
diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js
index 63424bfbf..c2ed5458d 100644
--- a/apps/boot/bootupdate.js
+++ b/apps/boot/bootupdate.js
@@ -4,7 +4,7 @@ of the time. */
E.showMessage("Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
-var boot = "";
+var boot = "", bootPost = "";
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
@@ -15,6 +15,7 @@ if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't chang
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
boot += `E.setFlags({pretokenise:1});\n`;
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
+bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
@@ -38,7 +39,7 @@ LoopbackA.setConsole(true);\n`;
boot += `
Bluetooth.line="";
Bluetooth.on('data',function(d) {
- var l = (Bluetooth.line + d).split("\n");
+ var l = (Bluetooth.line + d).split(/[\\n\\r]/);
Bluetooth.line = l.pop();
l.forEach(n=>Bluetooth.emit("line",n));
});
@@ -195,8 +196,8 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
-var getPriority = /.*\.(\d+)\.boot\.js$/;
-require('Storage').list(/\.boot\.js/).sort((a,b)=>{
+var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
+ var getPriority = /.*\.(\d+)\.boot\.js$/;
var aPriority = a.match(getPriority);
var bPriority = b.match(getPriority);
if (aPriority && bPriority){
@@ -206,18 +207,40 @@ require('Storage').list(/\.boot\.js/).sort((a,b)=>{
} else if (!aPriority && bPriority){
return 1;
}
- return a > b;
-}).forEach(bootFile=>{
+ return a==b ? 0 : (a>b ? 1 : -1);
+});
+// precalculate file size
+var fileSize = boot.length + bootPost.length;
+bootFiles.forEach(bootFile=>{
+ // match the size of data we're adding below in bootFiles.forEach
+ fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2;
+});
+// write file in chunks (so as not to use up all RAM)
+require('Storage').write('.boot0',boot,0,fileSize);
+var fileOffset = boot.length;
+bootFiles.forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error!
- boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
+ // we write:
+ // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
+ // but we need to do this without ever loading everything into RAM as some
+ // boot files seem to be getting pretty big now.
+ require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
+ fileOffset+=2+bootFile.length+1;
+ var bf = require('Storage').read(bootFile);
+ require('Storage').write('.boot0',bf,fileOffset);
+ fileOffset+=bf.length;
+ require('Storage').write('.boot0',";\n",fileOffset);
+ fileOffset+=2;
});
-// update ble
-boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`;
-// write file
-require('Storage').write('.boot0',boot);
+require('Storage').write('.boot0',bootPost,fileOffset);
+
delete boot;
+delete bootPost;
+delete bootFiles;
+delete fileSize;
+delete fileOffset;
E.showMessage("Reloading...");
eval(require('Storage').read('.boot0'));
// .bootcde should be run automatically after if required, since
diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json
index 4cbfd9c59..11884576f 100644
--- a/apps/boot/metadata.json
+++ b/apps/boot/metadata.json
@@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
- "version": "0.42",
+ "version": "0.46",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
diff --git a/apps/bordle/README.md b/apps/bordle/README.md
new file mode 100644
index 000000000..f15f1e6fa
--- /dev/null
+++ b/apps/bordle/README.md
@@ -0,0 +1,17 @@
+# Bordle
+
+The Bangle version of a popular word guessing game. The goal is to guess a 5 letter word in 6 tries or less. After each guess, the letters in the guess are
+marked in colors: yellow for a letter that appears in the to-be-guessed word, but in a different location and green for a letter in the correct position.
+
+Only words contained in the internal dictionary are allowed as valid guesses. At app launch, a target word is picked from the dictionary at random.
+
+On startup, a grid of 6 lines with 5 (empty) letter boxes is displayed. Swiping left or right at any time switches between grid view and keyboard view.
+The keyboad was inspired by the 'Scribble' app (it is a simplified version using the layout library). The letter group "Z ..." contains the delete key and
+the enter key. Hitting enter after the 5th letter will add the guess to the grid view and color mark it.
+
+The (English language) dictionary was derived from the the Unix ispell word list by filtering out plurals and past particples (and some hand editing) from all 5 letter words.
+It is contained in the file 'wordlencr.txt' which contains one long string (no newline characters) of all the words concatenated. It would not be too difficult to swap it
+out for a different language version. The keyboard currently only supports the 26 characters of the latin alphabet (no accents or umlauts).
+
+
+
diff --git a/apps/bordle/app-icon.js b/apps/bordle/app-icon.js
new file mode 100644
index 000000000..64ccbc8a5
--- /dev/null
+++ b/apps/bordle/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AA/TADwoIFkYyOF0owIF04wGUSqvVBZQtZGJYJIFzomKF0onIF07EKF0owLF9wNEnwACE6oZILxovbMBov/F/4v/C54uWF/4vKBQQLLF/4YPFwYMLF7AZGF5Y5KF5xJIFwoMJD44vaBhwvcLQpgHF8gGRF6xYNBpQvTXBoNOF65QJBIgvjBywvUV5YOOF64OIB54v/cQwAKB5ov/F84wKADYuIF+AwkFIwwnE45hmExCSlEpTEiERr3KADw+PF0ownUSoseA=="))
diff --git a/apps/bordle/app.png b/apps/bordle/app.png
new file mode 100644
index 000000000..633a83e4e
Binary files /dev/null and b/apps/bordle/app.png differ
diff --git a/apps/bordle/bordle.app.js b/apps/bordle/bordle.app.js
new file mode 100644
index 000000000..b1d771877
--- /dev/null
+++ b/apps/bordle/bordle.app.js
@@ -0,0 +1,159 @@
+var Layout = require("Layout");
+
+var gameState = 0;
+var keyState = 0;
+var keyStateIdx = 0;
+
+function buttonPushed(b) {
+ if (keyState==0) {
+ keyState++;
+ keyStateIdx = b;
+ if (b<6) {
+ for (i=1; i<=5; ++i) {
+ var c = String.fromCharCode(i+64+(b-1)*5);
+ layout["bt"+i.toString()].label = c;
+ layout["bt"+i.toString()].bgCol = wordle.keyColors[c]||g.theme.bg;
+ }
+ layout.bt6.label = "<";
+ }
+ else {
+ layout.bt1.label = "Z";
+ layout.bt1.bgCol = wordle.keyColors.Z||g.theme.bg;
+ layout.bt2.label = "";
+ layout.bt4.label = "";
+ layout.bt3.label = layout.bt5.label = " ";
+ layout.bt6.label = "<";
+ }
+ }
+ else { // actual button pushed
+ inp = layout.input.label;
+ if (b!=6) {
+ if ((keyStateIdx<=5 || b<=1) && inp.length<5) inp += String.fromCharCode(b+(keyStateIdx-1)*5+64);
+ else if (layout.input.label.length>0 && b==2) inp = inp.slice(0,-1);
+ layout.input.label = inp;
+ }
+ layout = getKeyLayout(inp);
+ keyState = 0;
+ if (inp.length==5 && keyStateIdx==6 && b==4) {
+ rc = wordle.addGuess(inp);
+ layout.input.label = "";
+ layout.update();
+ gameState = 0;
+ if (rc>0) return;
+ g.clear();
+ wordle.render();
+ return;
+ }
+ }
+ layout.update();
+ g.clear();
+ layout.render();
+}
+
+function getKeyLayout(text) {
+ return new Layout( {
+ type: "v", c: [
+ {type:"txt", font:"6x8:2", id:"input", label:text, pad: 3},
+ {type: "h", c: [
+ {type:"btn", font:"6x8:2", id:"bt1", label:"ABCDE", cb: l=>buttonPushed(1), pad:4, filly:1, fillx:1 },
+ {type:"btn", font:"6x8:2", id:"bt2", label:"FGHIJ", cb: l=>buttonPushed(2), pad:4, filly:1, fillx:1 },
+ ]},
+ {type: "h", c: [
+ {type:"btn", font:"6x8:2", id:"bt3", label:"KLMNO", cb: l=>buttonPushed(3), pad:4, filly:1, fillx:1 },
+ {type:"btn", font:"6x8:2", id:"bt4", label:"PQRST", cb: l=>buttonPushed(4), pad:4, filly:1, fillx:1 },
+ ]},
+ {type: "h", c: [
+ {type:"btn", font:"6x8:2", id:"bt5", label:"UVWXY", cb: l=>buttonPushed(5), pad:4, filly:1, fillx:1 },
+ {type:"btn", font:"6x8:2", id:"bt6", label:"Z ...", cb: l=>buttonPushed(6), pad:4, filly:1, fillx:1 },
+ ]}
+ ]});
+}
+
+class Wordle {
+ constructor(word) {
+ this.word = word;
+ this.guesses = [];
+ this.guessColors = [];
+ this.keyColors = [];
+ this.nGuesses = -1;
+ if (word == "rnd") {
+ this.words = require("Storage").read("wordlencr.txt");
+ i = Math.floor(Math.floor(this.words.length/5)*Math.random())*5;
+ this.word = this.words.slice(i, i+5).toUpperCase();
+ }
+ console.log(this.word);
+ }
+ render(clear) {
+ h = g.getHeight();
+ bh = Math.floor(h/6);
+ bbh = Math.floor(0.85*bh);
+ w = g.getWidth();
+ bw = Math.floor(w/5);
+ bbw = Math.floor(0.85*bw);
+ if (clear) g.clear();
+ g.setFont("Vector", Math.floor(bbh*0.95)).setFontAlign(0,0);
+ g.setColor(g.theme.fg);
+ for (i=0; i<6; ++i) {
+ for (j=0; j<5; ++j) {
+ if (i<=this.nGuesses) {
+ g.setColor(this.guessColors[i][j]).fillRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
+ g.setColor(g.theme.fg).drawString(this.guesses[i][j], 2+j*bw+bw/2, 2+i*bh+bh/2);
+ }
+ g.setColor(g.theme.fg).drawRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
+ }
+ }
+ }
+ addGuess(w) {
+ if ((this.words.indexOf(w.toLowerCase())%5)!=0) {
+ E.showAlert(w+"\nis not a word", "Invalid word").then(function() {
+ layout = getKeyLayout("");
+ wordle.render(true);
+ });
+ return 3;
+ }
+ this.guesses.push(w);
+ this.nGuesses++;
+ this.guessColors.push([]);
+ correct = 0;
+ var sol = this.word;
+ for (i=0; i 11) { ay++; _am -= 12; }
+ while ((m + _am) < 0) { ay--; _am += 12; }
+ n = new Date(_d.getTime());
+ n.setMonth(m + _am);
+ n.setFullYear(y + ay);
+ return n;
+ };
+ monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset;
+ state = "calendar";
+ var start = Date().getTime();
+ const months = ['Jan.', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec.'];
+ const monthclr = ['#0f0', '#f0f', '#00f', '#ff0', '#0ff', '#fff'];
+ if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
+ if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
+ if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
+ d = addMonths(Date(), monthOffset);
+ tdy = Date().getDate() + "." + Date().getMonth();
+ newmonth = false;
+ c_y = 0;
+ g.reset();
+ g.setBgColor(0);
+ g.clear();
+ var prevmonth = addMonths(d, -1);
+ const today = prevmonth.getDate();
+ var rD = new Date(prevmonth.getTime());
+ rD.setDate(rD.getDate() - (today - 1));
+ const dow = (s.FIRSTDAY + rD.getDay()) % 7;
+ rD.setDate(rD.getDate() - dow);
+ var rDate = rD.getDate();
+ bottomrightY = c_y - 3;
+ clrsun = s.REDSUN ? '#f00' : '#fff';
+ clrsat = s.REDSUN ? '#f00' : '#fff';
+ var fg = [clrsun, '#fff', '#fff', '#fff', '#fff', '#fff', clrsat];
+ for (var y = 1; y <= 11; y++) {
+ bottomrightY += CELL_H;
+ bottomrightX = -2;
+ for (var x = 1; x <= 7; x++) {
+ bottomrightX += CELL2_W;
+ rMonth = rD.getMonth();
+ rDate = rD.getDate();
+ if (tdy == rDate + "." + rMonth) {
+ caldrawToday(rDate);
+ } else if (rDate == 1) {
+ caldrawFirst(rDate);
+ } else {
+ caldrawNormal(rDate, fg[rD.getDay()]);
+ }
+ if (newmonth && x == 7) {
+ caldrawMonth(rDate, monthclr[rMonth % 6], months[rMonth], rD);
+ }
+ rD.setDate(rDate + 1);
+ }
+ }
+ delete addMonths;
+ if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start));
+}
+function caldrawMonth(rDate, c, m, rD) {
+ g.setColor(c);
+ g.setFont("Vector", 18);
+ g.setFontAlign(-1, 1, 1);
+ drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : "";
+ g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1);
+ newmonth = false;
+}
+function caldrawToday(rDate) {
+ g.setFont("Vector", 16);
+ g.setFontAlign(1, 1);
+ g.setColor('#0f0');
+ g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
+ g.setColor('#000');
+ g.drawString(rDate, bottomrightX, bottomrightY);
+}
+function caldrawFirst(rDate) {
+ g.flip();
+ g.setFont("Vector", 16);
+ g.setFontAlign(1, 1);
+ bottomrightY += 3;
+ newmonth = true;
+ g.setColor('#0ff');
+ g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
+ g.setColor('#000');
+ g.drawString(rDate, bottomrightX, bottomrightY);
+}
+function caldrawNormal(rDate, c) {
+ g.setFont("Vector", 16);
+ g.setFontAlign(1, 1);
+ g.setColor(c);
+ g.drawString(rDate, bottomrightX, bottomrightY);//100
+}
function drawMinutes() {
if (DEBUG) console.log("|-->minutes");
var d = new Date();
@@ -52,15 +154,17 @@ function drawSeconds() {
if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000);
}
-function drawCalendar() {
+function drawWatch() {
if (DEBUG) console.log("CALENDAR");
+ monthOffset = 0;
+ state = "watch";
var d = new Date();
g.reset();
g.setBgColor(0);
g.clear();
drawMinutes();
if (!dimSeconds) drawSeconds();
- const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6
+ const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6
const today = d.getDate();
var rD = new Date(d.getTime());
rD.setDate(rD.getDate() - dow);
@@ -91,7 +195,7 @@ function drawCalendar() {
var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1);
if (DEBUG) console.log("Next Day:" + (nextday / 3600));
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
- dayInterval = setTimeout(drawCalendar, nextday * 1000);
+ dayInterval = setTimeout(drawWatch, nextday * 1000);
}
function BTevent() {
@@ -102,18 +206,105 @@ function BTevent() {
setTimeout(function () { Bangle.buzz(interval); }, interval * 3);
}
}
+function action(a) {
+ g.reset();
+ if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
+ if (DEBUG) console.log("action:" + a);
+ switch (a) {
+ case "[ignore]":
+ break;
+ case "[calend.]":
+ drawFullCalendar();
+ break;
+ case "[AI:music]":
+ l = require("Storage").list(RegExp("music.*app.js"));
+ if (l.length > 0) {
+ load(l[0]);
+ } else E.showAlert("Music app not found", "Not found").then(drawWatch);
+ break;
+ case "[AI:messg]":
+ l = require("Storage").list(RegExp("message.*app.js"));
+ if (l.length > 0) {
+ load(l[0]);
+ } else E.showAlert("Message app not found", "Not found").then(drawWatch);
+ break;
+ default:
+ l = require("Storage").list(RegExp(a + ".app.js"));
+ if (l.length > 0) {
+ load(l[0]);
+ } else E.showAlert(a + ": App not found", "Not found").then(drawWatch);
+ break;
+ }
+}
+function input(dir) {
+ Bangle.buzz(100, 1);
+ if (DEBUG) console.log("swipe:" + dir);
+ switch (dir) {
+ case "r":
+ if (state == "calendar") {
+ drawWatch();
+ } else {
+ action(s.DRAGRIGHT);
+ }
+ break;
+ case "l":
+ if (state == "calendar") {
+ drawWatch();
+ } else {
+ action(s.DRAGLEFT);
+ }
+ break;
+ case "d":
+ if (state == "calendar") {
+ monthOffset--;
+ drawFullCalendar(monthOffset);
+ } else {
+ action(s.DRAGDOWN);
+ }
+ break;
+ case "u":
+ if (state == "calendar") {
+ monthOffset++;
+ drawFullCalendar(monthOffset);
+ } else {
+ action(s.DRAGUP);
+ }
+ break;
+ default:
+ if (state == "calendar") {
+ drawWatch();
+ }
+ break;
+
+ }
+}
+
+let drag;
+Bangle.on("drag", e => {
+ if (!drag) {
+ drag = { x: e.x, y: e.y };
+ } else if (!e.b) {
+ const dx = e.x - drag.x, dy = e.y - drag.y;
+ var dir = "t";
+ if (Math.abs(dx) > Math.abs(dy) + 20) {
+ dir = (dx > 0) ? "r" : "l";
+ } else if (Math.abs(dy) > Math.abs(dx) + 20) {
+ dir = (dy > 0) ? "d" : "u";
+ }
+ drag = null;
+ input(dir);
+ }
+});
//register events
Bangle.on('lock', locked => {
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
dimSeconds = locked; //dim seconds if lock=on
- drawCalendar();
+ drawWatch();
});
NRF.on('connect', BTevent);
NRF.on('disconnect', BTevent);
-
dimSeconds = Bangle.isLocked();
-drawCalendar();
-
+drawWatch();
Bangle.setUI("clock");
diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json
index ccc84a980..dde32f746 100644
--- a/apps/clockcal/metadata.json
+++ b/apps/clockcal/metadata.json
@@ -1,7 +1,7 @@
{
"id": "clockcal",
"name": "Clock & Calendar",
- "version": "0.01",
+ "version": "0.3",
"description": "Clock with Calendar",
"readme":"README.md",
"icon": "app.png",
diff --git a/apps/clockcal/screenshot3.png b/apps/clockcal/screenshot3.png
new file mode 100644
index 000000000..ab34f4306
Binary files /dev/null and b/apps/clockcal/screenshot3.png differ
diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js
index cc2a78181..abedad99b 100644
--- a/apps/clockcal/settings.js
+++ b/apps/clockcal/settings.js
@@ -1,16 +1,22 @@
(function (back) {
var FILE = "clockcal.json";
-
- settings = Object.assign({
+ defaults={
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
REDSUN: true, // Use red color for sunday?
REDSAT: true, // Use red color for saturday?
- }, require('Storage').readJSON(FILE, true) || {});
-
+ DRAGDOWN: "[AI:messg]",
+ DRAGRIGHT: "[AI:music]",
+ DRAGLEFT: "[ignore]",
+ DRAGUP: "[calend.]"
+ };
+ settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {});
+ actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"];
+ require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js","")));
+
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
@@ -67,26 +73,55 @@
writeSettings();
}
},
+ 'Drag Up ': {
+ min:0, max:actions.length-1,
+ value: actions.indexOf(settings.DRAGUP),
+ format: v => actions[v],
+ onchange: v => {
+ settings.DRAGUP = actions[v];
+ writeSettings();
+ }
+ },
+ 'Drag Right': {
+ min:0, max:actions.length-1,
+ value: actions.indexOf(settings.DRAGRIGHT),
+ format: v => actions[v],
+ onchange: v => {
+ settings.DRAGRIGHT = actions[v];
+ writeSettings();
+ }
+ },
+ 'Drag Down': {
+ min:0, max:actions.length-1,
+ value: actions.indexOf(settings.DRAGDOWN),
+ format: v => actions[v],
+ onchange: v => {
+ settings.DRGDOWN = actions[v];
+ writeSettings();
+ }
+ },
+ 'Drag Left': {
+ min:0, max:actions.length-1,
+ value: actions.indexOf(settings.DRAGLEFT),
+ format: v => actions[v],
+ onchange: v => {
+ settings.DRAGLEFT = actions[v];
+ writeSettings();
+ }
+ },
'Load deafauls?': {
value: 0,
min: 0, max: 1,
format: v => ["No", "Yes"][v],
onchange: v => {
if (v == 1) {
- settings = {
- CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
- BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect.
- MODE24: true, //24h mode vs 12h mode
- FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
- REDSUN: true, // Use red color for sunday?
- REDSAT: true, // Use red color for saturday?
- };
+ settings = defaults;
writeSettings();
- load()
+ load();
}
}
},
- }
+ };
// Show the menu
E.showMenu(menu);
-})
+});
diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog
index b7e18abe3..4f48bdd14 100644
--- a/apps/crowclk/ChangeLog
+++ b/apps/crowclk/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
+0.03: Fix the clock for dark mode.
diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js
index d06369fa8..eee1653cb 100644
--- a/apps/crowclk/crow_clock.js
+++ b/apps/crowclk/crow_clock.js
@@ -76,7 +76,7 @@ function draw_clock(){
// g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y);
// g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius);
- g.setColor(g.theme.fg);
+ g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg);
let ticks = [0, 90, 180, 270];
ticks.forEach((item)=>{
let agl = item+180;
@@ -92,13 +92,13 @@ function draw_clock(){
let minute_agl = minute_angle(date);
g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); //
g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); //
- g.setColor(g.theme.fg);
+ g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg);
g.fillCircle(clock_center.x, clock_center.y, 6);
- g.setColor(g.theme.bg);
+ g.setColor(g.theme.dark ? g.theme.fg : g.theme.bg);
g.fillCircle(clock_center.x, clock_center.y, 3);
// draw minute ticks. Takes long time to draw!
- g.setColor(g.theme.fg);
+ g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg);
for (var i=0; i<60; i++){
let agl = i*6+180;
g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180});
diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json
index 752e30fb0..6985cf11a 100644
--- a/apps/crowclk/metadata.json
+++ b/apps/crowclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "crowclk",
"name": "Crow Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
"icon": "crow_clock.png",
"screenshots": [{"url":"screenshot_crow.png"}],
diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog
index 8f23fa9f3..a98be5c0f 100644
--- a/apps/cscsensor/ChangeLog
+++ b/apps/cscsensor/ChangeLog
@@ -5,3 +5,4 @@
0.05: Add cadence sensor support
0.06: Now read wheel rev as well as cadence sensor
Improve connection code
+0.07: Make Bangle.js 2 compatible
diff --git a/apps/cscsensor/README.md b/apps/cscsensor/README.md
index 9740fd9cf..3828e8e3e 100644
--- a/apps/cscsensor/README.md
+++ b/apps/cscsensor/README.md
@@ -11,9 +11,9 @@ Currently the app displays the following data:
- total distance traveled
- an icon with the battery status of the remote sensor
-Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app.
-If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor.
-Button 2 switches between the display for cycling speed and cadence.
+Button 1 (swipe up on Bangle.js 2) resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app.
+If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 (swipe down on Bangle.js 2) will attempt to reconnect to the sensor.
+Button 2 (tap on Bangle.js 2) switches between the display for cycling speed and cadence.
Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app.
diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js
index e2af0db16..4ebe7d57e 100644
--- a/apps/cscsensor/cscsensor.app.js
+++ b/apps/cscsensor/cscsensor.app.js
@@ -7,6 +7,11 @@ const SETTINGS_FILE = 'cscsensor.json';
const storage = require('Storage');
const W = g.getWidth();
const H = g.getHeight();
+const yStart = 48;
+const rowHeight = (H-yStart)/6;
+const yCol1 = W/2.7586;
+const fontSizeLabel = W/12.632;
+const fontSizeValue = W/9.2308;
class CSCSensor {
constructor() {
@@ -22,7 +27,6 @@ class CSCSensor {
this.speed = 0;
this.maxSpeed = 0;
this.lastSpeed = 0;
- this.qUpdateScreen = true;
this.lastRevsStart = -1;
this.qMetric = !require("locale").speed(1).toString().endsWith("mph");
this.speedUnit = this.qMetric ? "km/h" : "mph";
@@ -49,6 +53,7 @@ class CSCSensor {
toggleDisplayCadence() {
this.showCadence = !this.showCadence;
this.screenInit = true;
+ g.setBgColor(0, 0, 0);
}
setBatteryLevel(level) {
@@ -63,14 +68,16 @@ class CSCSensor {
}
drawBatteryIcon() {
- g.setColor(1, 1, 1).drawRect(10, 55, 20, 75).fillRect(14, 53, 16, 55).setColor(0).fillRect(11, 56, 19, 74);
+ g.setColor(1, 1, 1).drawRect(10*W/240, yStart+0.029167*H, 20*W/240, yStart+0.1125*H)
+ .fillRect(14*W/240, yStart+0.020833*H, 16*W/240, yStart+0.029167*H)
+ .setColor(0).fillRect(11*W/240, yStart+0.033333*H, 19*W/240, yStart+0.10833*H);
if (this.batteryLevel!=-1) {
if (this.batteryLevel<25) g.setColor(1, 0, 0);
else if (this.batteryLevel<50) g.setColor(1, 0.5, 0);
else g.setColor(0, 1, 0);
- g.fillRect(11, 74-18*this.batteryLevel/100, 19, 74);
+ g.fillRect(11*W/240, (yStart+0.10833*H)-18*this.batteryLevel/100, 19*W/240, yStart+0.10833*H);
}
- else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66);
+ else g.setFontVector(W/17.143).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16*W/240, yStart+0.075*H);
}
updateScreenRevs() {
@@ -88,36 +95,36 @@ class CSCSensor {
for (var i=0; i<6; ++i) {
if ((i&1)==0) g.setColor(0, 0, 0);
else g.setColor(0x30cd);
- g.fillRect(0, 48+i*32, 86, 48+(i+1)*32);
+ g.fillRect(0, yStart+i*rowHeight, yCol1-1, yStart+(i+1)*rowHeight);
if ((i&1)==1) g.setColor(0);
else g.setColor(0x30cd);
- g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
- g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239);
- g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
+ g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight);
+ g.setColor(0.5, 0.5, 0.5).drawRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight).drawLine(0, H-1, W-1, H-1);
+ g.moveTo(0, yStart+0.13333*H).lineTo(30*W/240, yStart+0.13333*H).lineTo(30*W/240, yStart).lineTo(yCol1, yStart).lineTo(yCol1, H-1).lineTo(0, H-1).lineTo(0, yStart+0.13333*H);
}
- g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0);
- g.drawString("Time:", 87, 66);
- g.drawString("Speed:", 87, 98);
- g.drawString("Ave spd:", 87, 130);
- g.drawString("Max spd:", 87, 162);
- g.drawString("Trip:", 87, 194);
- g.drawString("Total:", 87, 226);
+ g.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0);
+ g.drawString("Time:", yCol1, yStart+rowHeight/2+0*rowHeight);
+ g.drawString("Speed:", yCol1, yStart+rowHeight/2+1*rowHeight);
+ g.drawString("Avg spd:", yCol1, yStart+rowHeight/2+2*rowHeight);
+ g.drawString("Max spd:", yCol1, yStart+rowHeight/2+3*rowHeight);
+ g.drawString("Trip:", yCol1, yStart+rowHeight/2+4*rowHeight);
+ g.drawString("Total:", yCol1, yStart+rowHeight/2+5*rowHeight);
this.drawBatteryIcon();
this.screenInit = false;
}
- g.setFontAlign(-1, 0, 0).setFontVector(26);
- g.setColor(0x30cd).fillRect(88, 49, 238, 79);
- g.setColor(0xffff).drawString(dmins+":"+dsecs, 92, 66);
- g.setColor(0).fillRect(88, 81, 238, 111);
- g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, 92, 98);
- g.setColor(0x30cd).fillRect(88, 113, 238, 143);
- g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, 92, 130);
- g.setColor(0).fillRect(88, 145, 238, 175);
- g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, 92, 162);
- g.setColor(0x30cd).fillRect(88, 177, 238, 207);
- g.setColor(0xffff).drawString(ddist + " " + this.distUnit, 92, 194);
- g.setColor(0).fillRect(88, 209, 238, 238);
- g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226);
+ g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue);
+ g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*0, 238, 47+1*rowHeight);
+ g.setColor(0xffff).drawString(dmins+":"+dsecs, yCol1+5, 50+rowHeight/2+0*rowHeight);
+ g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight);
+ g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, yCol1+5, 50+rowHeight/2+1*rowHeight);
+ g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*2, 238, 47+3*rowHeight);
+ g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+2*rowHeight);
+ g.setColor(0).fillRect(yCol1+1, 49+rowHeight*3, 238, 47+4*rowHeight);
+ g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+3*rowHeight);
+ g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*4, 238, 47+5*rowHeight);
+ g.setColor(0xffff).drawString(ddist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+4*rowHeight);
+ g.setColor(0).fillRect(yCol1+1, 49+rowHeight*5, 238, 47+6*rowHeight);
+ g.setColor(0xffff).drawString(tdist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+5*rowHeight);
}
updateScreenCadence() {
@@ -125,21 +132,21 @@ class CSCSensor {
for (var i=0; i<2; ++i) {
if ((i&1)==0) g.setColor(0, 0, 0);
else g.setColor(0x30cd);
- g.fillRect(0, 48+i*32, 86, 48+(i+1)*32);
+ g.fillRect(0, yStart+i*rowHeight, yCol1-1, yStart+(i+1)*rowHeight);
if ((i&1)==1) g.setColor(0);
else g.setColor(0x30cd);
- g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
- g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239);
- g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
+ g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight);
+ g.setColor(0.5, 0.5, 0.5).drawRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight).drawLine(0, H-1, W-1, H-1);
+ g.moveTo(0, yStart+0.13333*H).lineTo(30*W/240, yStart+0.13333*H).lineTo(30*W/240, yStart).lineTo(yCol1, yStart).lineTo(yCol1, H-1).lineTo(0, H-1).lineTo(0, yStart+0.13333*H);
}
- g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0);
- g.drawString("Cadence:", 87, 98);
+ g.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0);
+ g.drawString("Cadence:", yCol1, yStart+rowHeight/2+1*rowHeight);
this.drawBatteryIcon();
this.screenInit = false;
}
- g.setFontAlign(-1, 0, 0).setFontVector(26);
- g.setColor(0).fillRect(88, 81, 238, 111);
- g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98);
+ g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue);
+ g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight);
+ g.setColor(0xffff).drawString(Math.round(this.cadence), yCol1+5, 50+rowHeight/2+1*rowHeight);
}
updateScreen() {
@@ -163,45 +170,45 @@ class CSCSensor {
}
this.lastCrankRevs = crankRevs;
this.lastCrankTime = crankTime;
- }
- // wheel revolution
- var wheelRevs = event.target.value.getUint32(1, true);
- var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
- if (dRevs>0) {
- qChanged = true;
- this.totaldist += dRevs*this.wheelCirc/63360.0;
- if ((this.totaldist-this.settings.totaldist)>0.1) {
- this.settings.totaldist = this.totaldist;
- storage.writeJSON(SETTINGS_FILE, this.settings);
+ } else {
+ // wheel revolution
+ var wheelRevs = event.target.value.getUint32(1, true);
+ var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
+ if (dRevs>0) {
+ qChanged = true;
+ this.totaldist += dRevs*this.wheelCirc/63360.0;
+ if ((this.totaldist-this.settings.totaldist)>0.1) {
+ this.settings.totaldist = this.totaldist;
+ storage.writeJSON(SETTINGS_FILE, this.settings);
+ }
}
- }
- this.lastRevs = wheelRevs;
- if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs;
- var wheelTime = event.target.value.getUint16(5, true);
- var dT = (wheelTime-this.lastTime)/1024;
- var dBT = (Date.now()-this.lastBangleTime)/1000;
- this.lastBangleTime = Date.now();
- if (dT<0) dT+=64;
- if (Math.abs(dT-dBT)>3) dT = dBT;
- this.lastTime = wheelTime;
- this.speed = this.lastSpeed;
- if (dRevs>0 && dT>0) {
- this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
- this.speedFailed = 0;
- this.movingTime += dT;
- }
- else {
- this.speedFailed++;
- qChanged = false;
- if (this.speedFailed>3) {
- this.speed = 0;
- qChanged = (this.lastSpeed>0);
+ this.lastRevs = wheelRevs;
+ if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs;
+ var wheelTime = event.target.value.getUint16(5, true);
+ var dT = (wheelTime-this.lastTime)/1024;
+ var dBT = (Date.now()-this.lastBangleTime)/1000;
+ this.lastBangleTime = Date.now();
+ if (dT<0) dT+=64;
+ if (Math.abs(dT-dBT)>3) dT = dBT;
+ this.lastTime = wheelTime;
+ this.speed = this.lastSpeed;
+ if (dRevs>0 && dT>0) {
+ this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
+ this.speedFailed = 0;
+ this.movingTime += dT;
+ } else if (!this.showCadence) {
+ this.speedFailed++;
+ qChanged = false;
+ if (this.speedFailed>3) {
+ this.speed = 0;
+ qChanged = (this.lastSpeed>0);
+ }
}
+ this.lastSpeed = this.speed;
+ if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
}
- this.lastSpeed = this.speed;
- if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed;
}
- if (qChanged && this.qUpdateScreen) this.updateScreen();
+ if (qChanged) this.updateScreen();
}
}
@@ -253,9 +260,9 @@ E.on('kill',()=>{
});
NRF.on('disconnect', connection_setup); // restart if disconnected
Bangle.setUI("updown", d=>{
- if (d<0) { mySensor.reset(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); }
- if (d==0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
- if (d>0) { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); }
+ if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
+ else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
+ else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
});
Bangle.loadWidgets();
diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json
index af338c59e..4006789ef 100644
--- a/apps/cscsensor/metadata.json
+++ b/apps/cscsensor/metadata.json
@@ -2,11 +2,11 @@
"id": "cscsensor",
"name": "Cycling speed sensor",
"shortName": "CSCSensor",
- "version": "0.06",
+ "version": "0.07",
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
"icon": "icons8-cycling-48.png",
"tags": "outdoors,exercise,ble,bluetooth",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"cscsensor.app.js","url":"cscsensor.app.js"},
diff --git a/apps/cycling/ChangeLog b/apps/cycling/ChangeLog
new file mode 100644
index 000000000..ec66c5568
--- /dev/null
+++ b/apps/cycling/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial version
diff --git a/apps/cycling/README.md b/apps/cycling/README.md
new file mode 100644
index 000000000..7ba8ee224
--- /dev/null
+++ b/apps/cycling/README.md
@@ -0,0 +1,34 @@
+# Cycling
+> Displays data from a BLE Cycling Speed and Cadence sensor.
+
+*This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.*
+
+The following data are displayed:
+- curent speed
+- moving time
+- average speed
+- maximum speed
+- trip distance
+- total distance
+
+Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over.
+
+**Cadence / Crank features are currently not implemented**
+
+## Usage
+Open the app and connect to a CSC sensor.
+
+Upon first connection, close the app afain and enter the settings app to configure the wheel circumference. The total circumference is (cm + mm) - it is split up into two values for ease of configuration. Check the status screen inside the Cycling app while connected to see the address of the currently connected sensor (if you need to differentiate between multiple sensors).
+
+Inside the Cycling app, use button / tap screen to:
+- cycle through screens (if connected)
+- reconnect (if connection aborted)
+
+## TODO
+* Sensor battery status
+* Implement crank events / show cadence
+* Bangle.js 1 compatibility
+* Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike)
+
+## Development
+There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage.
diff --git a/apps/cycling/blecsc-emu.js b/apps/cycling/blecsc-emu.js
new file mode 100644
index 000000000..ca5058545
--- /dev/null
+++ b/apps/cycling/blecsc-emu.js
@@ -0,0 +1,111 @@
+// UUID of the Bluetooth CSC Service
+const SERVICE_UUID = "1816";
+// UUID of the CSC measurement characteristic
+const MEASUREMENT_UUID = "2a5b";
+
+// Wheel revolution present bit mask
+const FLAGS_WREV_BM = 0x01;
+// Crank revolution present bit mask
+const FLAGS_CREV_BM = 0x02;
+
+/**
+ * Fake BLECSC implementation for the emulator, where it's hard to test
+ * with actual hardware. Generates "random" wheel events (no crank).
+ *
+ * To upload as a module, paste the entire file in the console using this
+ * command: require("Storage").write("blecsc-emu",``);
+ */
+class BLECSCEmulator {
+ constructor() {
+ this.timeout = undefined;
+ this.interval = 500;
+ this.ccr = 0;
+ this.lwt = 0;
+ this.handlers = {
+ // value
+ // disconnect
+ // wheelEvent
+ // crankEvent
+ };
+ }
+
+ getDeviceAddress() {
+ return 'fa:ke:00:de:vi:ce';
+ }
+
+ /**
+ * Callback for the GATT characteristicvaluechanged event.
+ * Consumers must not call this method!
+ */
+ onValue(event) {
+ // Not interested in non-CSC characteristics
+ if (event.target.uuid != "0x" + MEASUREMENT_UUID) return;
+
+ // Notify the generic 'value' handler
+ if (this.handlers.value) this.handlers.value(event);
+
+ const flags = event.target.value.getUint8(0, true);
+ // Notify the 'wheelEvent' handler
+ if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({
+ cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions
+ lwet: event.target.value.getUint16(5, true), // last wheel event time
+ });
+
+ // Notify the 'crankEvent' handler
+ if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({
+ ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions
+ lcet: event.target.value.getUint16(9, true), // last crank event time
+ });
+ }
+
+ /**
+ * Register an event handler.
+ *
+ * @param {string} event value|disconnect
+ * @param {function} handler handler function that receives the event as its first argument
+ */
+ on(event, handler) {
+ this.handlers[event] = handler;
+ }
+
+ fakeEvent() {
+ this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20));
+ this.lwt = (this.lwt + this.interval) % 0x10000;
+ this.ccr++;
+
+ var buffer = new ArrayBuffer(8);
+ var view = new DataView(buffer);
+ view.setUint8(0, 0x01); // Wheel revolution data present bit
+ view.setUint32(1, this.ccr, true); // Cumulative crank revolutions
+ view.setUint16(5, this.lwt, true); // Last wheel event time
+
+ this.onValue({
+ target: {
+ uuid: "0x2a5b",
+ value: view,
+ },
+ });
+
+ this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval);
+ }
+
+ /**
+ * Find and connect to a device which exposes the CSC service.
+ *
+ * @return {Promise}
+ */
+ connect() {
+ this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval);
+ return Promise.resolve(true);
+ }
+
+ /**
+ * Disconnect the device.
+ */
+ disconnect() {
+ if (!this.timeout) return;
+ clearTimeout(this.timeout);
+ }
+}
+
+exports = BLECSCEmulator;
diff --git a/apps/cycling/blecsc.js b/apps/cycling/blecsc.js
new file mode 100644
index 000000000..7a47108e5
--- /dev/null
+++ b/apps/cycling/blecsc.js
@@ -0,0 +1,150 @@
+const SERVICE_UUID = "1816";
+// UUID of the CSC measurement characteristic
+const MEASUREMENT_UUID = "2a5b";
+
+// Wheel revolution present bit mask
+const FLAGS_WREV_BM = 0x01;
+// Crank revolution present bit mask
+const FLAGS_CREV_BM = 0x02;
+
+/**
+ * This class communicates with a Bluetooth CSC peripherial using the Espruino NRF library.
+ *
+ * ## Usage:
+ * 1. Register event handlers using the \`on(eventName, handlerFunction)\` method
+ * You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can
+ * have raw characteristic values passed through using the \`value\` event.
+ * 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method
+ * 3. To tear down the connection, call the \`disconnect()\` method
+ *
+ * ## Events
+ * - \`wheelEvent\` - the peripharial sends a notification containing wheel event data
+ * - \`crankEvent\` - the peripharial sends a notification containing crank event data
+ * - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event)
+ * - \`disconnect\` - the peripherial ends the connection or the connection is lost
+ *
+ * Each event can only have one handler. Any call to \`on()\` will
+ * replace a previously registered handler for the same event.
+ */
+class BLECSC {
+ constructor() {
+ this.device = undefined;
+ this.ccInterval = undefined;
+ this.gatt = undefined;
+ this.handlers = {
+ // wheelEvent
+ // crankEvent
+ // value
+ // disconnect
+ };
+ }
+
+ getDeviceAddress() {
+ if (!this.device || !this.device.id)
+ return '00:00:00:00:00:00';
+ return this.device.id.split(" ")[0];
+ }
+
+ checkConnection() {
+ if (!this.device)
+ console.log("no device");
+ // else
+ // console.log("rssi: " + this.device.rssi);
+ }
+
+ /**
+ * Callback for the GATT characteristicvaluechanged event.
+ * Consumers must not call this method!
+ */
+ onValue(event) {
+ // Not interested in non-CSC characteristics
+ if (event.target.uuid != "0x" + MEASUREMENT_UUID) return;
+
+ // Notify the generic 'value' handler
+ if (this.handlers.value) this.handlers.value(event);
+
+ const flags = event.target.value.getUint8(0, true);
+ // Notify the 'wheelEvent' handler
+ if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({
+ cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions
+ lwet: event.target.value.getUint16(5, true), // last wheel event time
+ });
+
+ // Notify the 'crankEvent' handler
+ if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({
+ ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions
+ lcet: event.target.value.getUint16(9, true), // last crank event time
+ });
+ }
+
+ /**
+ * Callback for the NRF disconnect event.
+ * Consumers must not call this method!
+ */
+ onDisconnect(event) {
+ console.log("disconnected");
+ if (this.ccInterval)
+ clearInterval(this.ccInterval);
+
+ if (!this.handlers.disconnect) return;
+ this.handlers.disconnect(event);
+ }
+
+ /**
+ * Register an event handler.
+ *
+ * @param {string} event wheelEvent|crankEvent|value|disconnect
+ * @param {function} handler function that will receive the event as its first argument
+ */
+ on(event, handler) {
+ this.handlers[event] = handler;
+ }
+
+ /**
+ * Find and connect to a device which exposes the CSC service.
+ *
+ * @return {Promise}
+ */
+ connect() {
+ // Register handler for the disconnect event to be passed throug
+ NRF.on('disconnect', this.onDisconnect.bind(this));
+
+ // Find a device, then get the CSC Service and subscribe to
+ // notifications on the CSC Measurement characteristic.
+ // NRF.setLowPowerConnection(true);
+ return NRF.requestDevice({
+ timeout: 5000,
+ filters: [{ services: [SERVICE_UUID] }],
+ }).then(device => {
+ this.device = device;
+ this.device.on('gattserverdisconnected', this.onDisconnect.bind(this));
+ this.ccInterval = setInterval(this.checkConnection.bind(this), 2000);
+ return device.gatt.connect();
+ }).then(gatt => {
+ this.gatt = gatt;
+ return gatt.getPrimaryService(SERVICE_UUID);
+ }).then(service => {
+ return service.getCharacteristic(MEASUREMENT_UUID);
+ }).then(characteristic => {
+ characteristic.on('characteristicvaluechanged', this.onValue.bind(this));
+ return characteristic.startNotifications();
+ });
+ }
+
+ /**
+ * Disconnect the device.
+ */
+ disconnect() {
+ if (this.ccInterval)
+ clearInterval(this.ccInterval);
+
+ if (!this.gatt) return;
+ try {
+ this.gatt.disconnect();
+ } catch {
+ //
+ }
+ }
+}
+
+exports = BLECSC;
diff --git a/apps/cycling/cycling.app.js b/apps/cycling/cycling.app.js
new file mode 100644
index 000000000..268284a29
--- /dev/null
+++ b/apps/cycling/cycling.app.js
@@ -0,0 +1,453 @@
+const Layout = require('Layout');
+const storage = require('Storage');
+
+const SETTINGS_FILE = 'cycling.json';
+const SETTINGS_DEFAULT = {
+ sensors: {},
+ metric: true,
+};
+
+const RECONNECT_TIMEOUT = 4000;
+const MAX_CONN_ATTEMPTS = 2;
+
+class CSCSensor {
+ constructor(blecsc, display) {
+ // Dependency injection
+ this.blecsc = blecsc;
+ this.display = display;
+
+ // Load settings
+ this.settings = storage.readJSON(SETTINGS_FILE, true) || SETTINGS_DEFAULT;
+ this.wheelCirc = undefined;
+
+ // CSC runtime variables
+ this.movingTime = 0; // unit: s
+ this.lastBangleTime = Date.now(); // unit: ms
+ this.lwet = 0; // last wheel event time (unit: s/1024)
+ this.cwr = -1; // cumulative wheel revolutions
+ this.cwrTrip = 0; // wheel revolutions since trip start
+ this.speed = 0; // unit: m/s
+ this.maxSpeed = 0; // unit: m/s
+ this.speedFailed = 0;
+
+ // Other runtime variables
+ this.connected = false;
+ this.failedAttempts = 0;
+ this.failed = false;
+
+ // Layout configuration
+ this.layout = 0;
+ this.display.useMetricUnits(true);
+ this.deviceAddress = undefined;
+ this.display.useMetricUnits((this.settings.metric));
+ }
+
+ onDisconnect(event) {
+ console.log("disconnected ", event);
+
+ this.connected = false;
+ this.wheelCirc = undefined;
+
+ this.setLayout(0);
+ this.display.setDeviceAddress("unknown");
+
+ if (this.failedAttempts >= MAX_CONN_ATTEMPTS) {
+ this.failed = true;
+ this.display.setStatus("Connection failed after " + MAX_CONN_ATTEMPTS + " attempts.");
+ } else {
+ this.display.setStatus("Disconnected");
+ setTimeout(this.connect.bind(this), RECONNECT_TIMEOUT);
+ }
+ }
+
+ loadCircumference() {
+ if (!this.deviceAddress) return;
+
+ // Add sensor to settings if not present
+ if (!this.settings.sensors[this.deviceAddress]) {
+ this.settings.sensors[this.deviceAddress] = {
+ cm: 223,
+ mm: 0,
+ };
+ storage.writeJSON(SETTINGS_FILE, this.settings);
+ }
+
+ const high = this.settings.sensors[this.deviceAddress].cm || 223;
+ const low = this.settings.sensors[this.deviceAddress].mm || 0;
+ this.wheelCirc = (10*high + low) / 1000;
+ }
+
+ connect() {
+ this.connected = false;
+ this.setLayout(0);
+ this.display.setStatus("Connecting");
+ console.log("Trying to connect to BLE CSC");
+
+ // Hook up events
+ this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this));
+ this.blecsc.on('disconnect', this.onDisconnect.bind(this));
+
+ // Scan for BLE device and connect
+ this.blecsc.connect()
+ .then(function() {
+ this.failedAttempts = 0;
+ this.failed = false;
+ this.connected = true;
+ this.deviceAddress = this.blecsc.getDeviceAddress();
+ console.log("Connected to " + this.deviceAddress);
+
+ this.display.setDeviceAddress(this.deviceAddress);
+ this.display.setStatus("Connected");
+
+ this.loadCircumference();
+
+ // Switch to speed screen in 2s
+ setTimeout(function() {
+ this.setLayout(1);
+ this.updateScreen();
+ }.bind(this), 2000);
+ }.bind(this))
+ .catch(function(e) {
+ this.failedAttempts++;
+ this.onDisconnect(e);
+ }.bind(this));
+ }
+
+ disconnect() {
+ this.blecsc.disconnect();
+ this.reset();
+ this.setLayout(0);
+ this.display.setStatus("Disconnected");
+ }
+
+ setLayout(num) {
+ this.layout = num;
+ if (this.layout == 0) {
+ this.display.updateLayout("status");
+ } else if (this.layout == 1) {
+ this.display.updateLayout("speed");
+ } else if (this.layout == 2) {
+ this.display.updateLayout("distance");
+ }
+ }
+
+ reset() {
+ this.connected = false;
+ this.failed = false;
+ this.failedAttempts = 0;
+ this.wheelCirc = undefined;
+ }
+
+ interact(d) {
+ // Only interested in tap / center button
+ if (d) return;
+
+ // Reconnect in failed state
+ if (this.failed) {
+ this.reset();
+ this.connect();
+ } else if (this.connected) {
+ this.setLayout((this.layout + 1) % 3);
+ }
+ }
+
+ updateScreen() {
+ var tripDist = this.cwrTrip * this.wheelCirc;
+ var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0;
+
+ this.display.setTotalDistance(this.cwr * this.wheelCirc);
+ this.display.setTripDistance(tripDist);
+ this.display.setSpeed(this.speed);
+ this.display.setAvg(avgSpeed);
+ this.display.setMax(this.maxSpeed);
+ this.display.setTime(Math.floor(this.movingTime));
+ }
+
+ onWheelEvent(event) {
+ // Calculate number of revolutions since last wheel event
+ var dRevs = (this.cwr > 0 ? event.cwr - this.cwr : 0);
+ this.cwr = event.cwr;
+
+ // Increment the trip revolutions counter
+ this.cwrTrip += dRevs;
+
+ // Calculate time delta since last wheel event
+ var dT = (event.lwet - this.lwet)/1024;
+ var now = Date.now();
+ var dBT = (now-this.lastBangleTime)/1000;
+ this.lastBangleTime = now;
+ if (dT<0) dT+=64; // wheel event time wraps every 64s
+ if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this
+ this.lwet = event.lwet;
+
+ // Recalculate current speed
+ if (dRevs>0 && dT>0) {
+ this.speed = dRevs * this.wheelCirc / dT;
+ this.speedFailed = 0;
+ this.movingTime += dT;
+ } else {
+ this.speedFailed++;
+ if (this.speedFailed>3) {
+ this.speed = 0;
+ }
+ }
+
+ // Update max speed
+ if (this.speed>this.maxSpeed
+ && (this.movingTime>3 || this.speed<20)
+ && this.speed<50
+ ) this.maxSpeed = this.speed;
+
+ this.updateScreen();
+ }
+}
+
+class CSCDisplay {
+ constructor() {
+ this.metric = true;
+ this.fontLabel = "6x8";
+ this.fontSmall = "15%";
+ this.fontMed = "18%";
+ this.fontLarge = "32%";
+ this.currentLayout = "status";
+ this.layouts = {};
+ this.layouts.speed = new Layout({
+ type: "v",
+ c: [
+ {
+ type: "h",
+ id: "speed_g",
+ fillx: 1,
+ filly: 1,
+ pad: 4,
+ bgCol: "#fff",
+ c: [
+ {type: undefined, width: 32, halign: -1},
+ {type: "txt", id: "speed", label: "00.0", font: this.fontLarge, bgCol: "#fff", col: "#000", width: 122},
+ {type: "txt", id: "speed_u", label: " km/h", font: this.fontLabel, col: "#000", width: 22, r: 90},
+ ]
+ },
+ {
+ type: "h",
+ id: "time_g",
+ fillx: 1,
+ pad: 4,
+ bgCol: "#000",
+ height: 36,
+ c: [
+ {type: undefined, width: 32, halign: -1},
+ {type: "txt", id: "time", label: "00:00", font: this.fontMed, bgCol: "#000", col: "#fff", width: 122},
+ {type: "txt", id: "time_u", label: "mins", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90},
+ ]
+ },
+ {
+ type: "h",
+ id: "stats_g",
+ fillx: 1,
+ bgCol: "#fff",
+ height: 36,
+ c: [
+ {
+ type: "v",
+ pad: 4,
+ bgCol: "#fff",
+ c: [
+ {type: "txt", id: "max_l", label: "MAX", font: this.fontLabel, col: "#000"},
+ {type: "txt", id: "max", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69},
+ ],
+ },
+ {
+ type: "v",
+ pad: 4,
+ bgCol: "#fff",
+ c: [
+ {type: "txt", id: "avg_l", label: "AVG", font: this.fontLabel, col: "#000"},
+ {type: "txt", id: "avg", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69},
+ ],
+ },
+ {type: "txt", id: "stats_u", label: " km/h", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
+ ]
+ },
+ ],
+ });
+ this.layouts.distance = new Layout({
+ type: "v",
+ bgCol: "#fff",
+ c: [
+ {
+ type: "h",
+ id: "tripd_g",
+ fillx: 1,
+ pad: 4,
+ bgCol: "#fff",
+ height: 32,
+ c: [
+ {type: "txt", id: "tripd_l", label: "TRP", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36},
+ {type: "txt", id: "tripd", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118},
+ {type: "txt", id: "tripd_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
+ ]
+ },
+ {
+ type: "h",
+ id: "totald_g",
+ fillx: 1,
+ pad: 4,
+ bgCol: "#fff",
+ height: 32,
+ c: [
+ {type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36},
+ {type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118},
+ {type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
+ ]
+ },
+ ],
+ });
+ this.layouts.status = new Layout({
+ type: "v",
+ c: [
+ {
+ type: "h",
+ id: "status_g",
+ fillx: 1,
+ bgCol: "#fff",
+ height: 100,
+ c: [
+ {type: "txt", id: "status", label: "Bangle Cycling", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 176, wrap: 1},
+ ]
+ },
+ {
+ type: "h",
+ id: "addr_g",
+ fillx: 1,
+ pad: 4,
+ bgCol: "#fff",
+ height: 32,
+ c: [
+ { type: "txt", id: "addr_l", label: "ADDR", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 },
+ { type: "txt", id: "addr", label: "unknown", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 140 },
+ ]
+ },
+ ],
+ });
+ }
+
+ updateLayout(layout) {
+ this.currentLayout = layout;
+
+ g.clear();
+ this.layouts[layout].update();
+ this.layouts[layout].render();
+ Bangle.drawWidgets();
+ }
+
+ renderIfLayoutActive(layout, node) {
+ if (layout != this.currentLayout) return;
+ this.layouts[layout].render(node);
+ }
+
+ useMetricUnits(metric) {
+ this.metric = metric;
+
+ // console.log("using " + (metric ? "metric" : "imperial") + " units");
+
+ var speedUnit = metric ? "km/h" : "mph";
+ this.layouts.speed.speed_u.label = speedUnit;
+ this.layouts.speed.stats_u.label = speedUnit;
+
+ var distanceUnit = metric ? "km" : "mi";
+ this.layouts.distance.tripd_u.label = distanceUnit;
+ this.layouts.distance.totald_u.label = distanceUnit;
+
+ this.updateLayout(this.currentLayout);
+ }
+
+ convertDistance(meters) {
+ if (this.metric) return meters / 1000;
+ return meters / 1609.344;
+ }
+
+ convertSpeed(mps) {
+ if (this.metric) return mps * 3.6;
+ return mps * 2.23694;
+ }
+
+ setSpeed(speed) {
+ this.layouts.speed.speed.label = this.convertSpeed(speed).toFixed(1);
+ this.renderIfLayoutActive("speed", this.layouts.speed.speed_g);
+ }
+
+ setAvg(speed) {
+ this.layouts.speed.avg.label = this.convertSpeed(speed).toFixed(1);
+ this.renderIfLayoutActive("speed", this.layouts.speed.stats_g);
+ }
+
+ setMax(speed) {
+ this.layouts.speed.max.label = this.convertSpeed(speed).toFixed(1);
+ this.renderIfLayoutActive("speed", this.layouts.speed.stats_g);
+ }
+
+ setTime(seconds) {
+ var time = '';
+ var hours = Math.floor(seconds/3600);
+ if (hours) {
+ time += hours + ":";
+ this.layouts.speed.time_u.label = " hrs";
+ } else {
+ this.layouts.speed.time_u.label = "mins";
+ }
+
+ time += String(Math.floor((seconds%3600)/60)).padStart(2, '0') + ":";
+ time += String(seconds % 60).padStart(2, '0');
+
+ this.layouts.speed.time.label = time;
+ this.renderIfLayoutActive("speed", this.layouts.speed.time_g);
+ }
+
+ setTripDistance(distance) {
+ this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1);
+ this.renderIfLayoutActive("distance", this.layouts.distance.tripd_g);
+ }
+
+ setTotalDistance(distance) {
+ distance = this.convertDistance(distance);
+ if (distance >= 1000) {
+ this.layouts.distance.totald.label = String(Math.round(distance));
+ } else {
+ this.layouts.distance.totald.label = distance.toFixed(1);
+ }
+ this.renderIfLayoutActive("distance", this.layouts.distance.totald_g);
+ }
+
+ setDeviceAddress(address) {
+ this.layouts.status.addr.label = address;
+ this.renderIfLayoutActive("status", this.layouts.status.addr_g);
+ }
+
+ setStatus(status) {
+ this.layouts.status.status.label = status;
+ this.renderIfLayoutActive("status", this.layouts.status.status_g);
+ }
+}
+
+var BLECSC;
+if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") {
+ // Emulator
+ BLECSC = require("blecsc-emu");
+} else {
+ // Actual hardware
+ BLECSC = require("blecsc");
+}
+var blecsc = new BLECSC();
+var display = new CSCDisplay();
+var sensor = new CSCSensor(blecsc, display);
+
+E.on('kill',()=>{
+ sensor.disconnect();
+});
+
+Bangle.setUI("updown", d => {
+ sensor.interact(d);
+});
+
+Bangle.loadWidgets();
+sensor.connect();
diff --git a/apps/cycling/cycling.icon.js b/apps/cycling/cycling.icon.js
new file mode 100644
index 000000000..12c597956
--- /dev/null
+++ b/apps/cycling/cycling.icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))
diff --git a/apps/cycling/icons8-cycling-48.png b/apps/cycling/icons8-cycling-48.png
new file mode 100644
index 000000000..0bc83859f
Binary files /dev/null and b/apps/cycling/icons8-cycling-48.png differ
diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json
new file mode 100644
index 000000000..cb4260bb2
--- /dev/null
+++ b/apps/cycling/metadata.json
@@ -0,0 +1,17 @@
+{
+ "id": "cycling",
+ "name": "Bangle Cycling",
+ "shortName": "Cycling",
+ "version": "0.01",
+ "description": "Display live values from a BLE CSC sensor",
+ "icon": "icons8-cycling-48.png",
+ "tags": "outdoors,exercise,ble,bluetooth",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ {"name":"cycling.app.js","url":"cycling.app.js"},
+ {"name":"cycling.settings.js","url":"settings.js"},
+ {"name":"blecsc","url":"blecsc.js"},
+ {"name":"cycling.img","url":"cycling.icon.js","evaluate": true}
+ ]
+}
diff --git a/apps/cycling/settings.js b/apps/cycling/settings.js
new file mode 100644
index 000000000..76303379d
--- /dev/null
+++ b/apps/cycling/settings.js
@@ -0,0 +1,57 @@
+// This file should contain exactly one function, which shows the app's settings
+/**
+ * @param {function} back Use back() to return to settings menu
+ */
+(function(back) {
+ const storage = require('Storage')
+ const SETTINGS_FILE = 'cycling.json'
+
+ // Set default values and merge with stored values
+ let settings = Object.assign({
+ metric: true,
+ sensors: {},
+ }, (storage.readJSON(SETTINGS_FILE, true) || {}));
+
+ const menu = {
+ '': { 'title': 'Cycling' },
+ '< Back': back,
+ 'Units': {
+ value: settings.metric,
+ format: v => v ? 'metric' : 'imperial',
+ onchange: (metric) => {
+ settings.metric = metric;
+ storage.writeJSON(SETTINGS_FILE, settings);
+ },
+ },
+ }
+
+ const sensorMenus = {};
+ for (var addr of Object.keys(settings.sensors)) {
+ // Define sub menu
+ sensorMenus[addr] = {
+ '': { title: addr },
+ '< Back': () => E.showMenu(menu),
+ 'cm': {
+ value: settings.sensors[addr].cm,
+ min: 80, max: 240, step: 1,
+ onchange: (v) => {
+ settings.sensors[addr].cm = v;
+ storage.writeJSON(SETTINGS_FILE, settings);
+ },
+ },
+ '+ mm': {
+ value: settings.sensors[addr].mm,
+ min: 0, max: 9, step: 1,
+ onchange: (v) => {
+ settings.sensors[addr].mm = v;
+ storage.writeJSON(SETTINGS_FILE, settings);
+ },
+ },
+ };
+
+ // Add entry to main menu
+ menu[addr] = () => E.showMenu(sensorMenus[addr]);
+ }
+
+ E.showMenu(menu);
+})
diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog
index 6c4a25b26..77d82eff9 100644
--- a/apps/doztime/ChangeLog
+++ b/apps/doztime/ChangeLog
@@ -2,3 +2,6 @@
0.02: added emulator capability and display of widgets
0.03: bug of advancing time fixed; doztime now correct within ca. 1 second
0.04: changed time colour from slightly off white to pure white
+0.05: extraneous comments and code removed
+ display improved
+ now supports Adjust Clock widget, if installed
diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js
index b77e5201a..8a315118f 100644
--- a/apps/doztime/app-bangle2.js
+++ b/apps/doztime/app-bangle2.js
@@ -1,23 +1,23 @@
// Positioning values for graphics buffers
const g_height = 80; // total graphics height
-const g_x_off = 0; // position from left was 16, then 8 here
-const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240
+const g_x_off = 0; // position from left
+const g_y_off = (180 - g_height)/2; // vertical center for graphics region
const g_width = 240 - 2 * g_x_off; // total graphics width
-const g_height_d = 28; // height of date region was 32
+const g_height_d = 28; // height of date region
const g_y_off_d = 0; // y position of date region within graphics region
-const spacing = 0; // space between date and time in graphics region
+const spacing = 6; // space between date and time in graphics region
const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region
-const g_height_t = 44; // height of time region was 48
+const g_height_t = 44; // height of time region
// Other vars
const A1 = [30,30,30,30,31,31,31,31,31,31,30,30];
const B1 = [30,30,30,30,30,31,31,31,31,31,30,30];
const B2 = [30,30,30,30,31,31,31,31,31,30,30,30];
const timeColour = "#ffffff";
-const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff
-const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20
-const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20
-const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30
+const dateColours = ["#ff0000","#ff8000","#ffff00","#00ff00","#0080ff","#ff00ff","#ffffff"];
+const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line
+const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line
+const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line
const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30
const baseYear = 11584;
const baseDate = Date(2020,11,21); // month values run from 0 to 11
@@ -42,28 +42,25 @@ var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true});
g.clear(); // start with blank screen
g.flip = function()
{
- g.setBgColor(0,0,0);
+ g.setBgColor(0,0,0);
g.setColor(dateColour);
- g.drawImage(
- {
- width:g_width,
- height:g_height_d,
- buffer:g_d.buffer
- }, g_x_off, g_y_off + g_y_off_d);
- g.setColor(timeColour2);
- g.drawImage(
- {
- width:g_width,
- height:g_height_t,
- buffer:g_t.buffer
- }, g_x_off, g_y_off + g_y_off_t);
+ g.drawImage(
+ {
+ width:g_width,
+ height:g_height_d,
+ buffer:g_d.buffer
+ }, g_x_off, g_y_off + g_y_off_d);
+ g.setColor(timeColour2);
+ g.drawImage(
+ {
+ width:g_width,
+ height:g_height_t,
+ buffer:g_t.buffer
+ }, g_x_off, g_y_off + g_y_off_t);
};
-setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1
-setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2
-//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true});
-//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true});
-//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true});
+setWatch(function(){ modeTime(); }, BTN, {repeat:true} );
+setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" });
Bangle.on('touch', function(button, xy) { //from Gordon Williams
if (button==1) toggleTimeDigits();
@@ -71,10 +68,10 @@ Bangle.on('touch', function(button, xy) { //from Gordon Williams
});
function buildSequence(targ){
- for(let i=0;i n > dt)-1;
- let year = baseYear+parseInt(index/12);
- let month = index % 12;
- let day = parseInt((dt-sequence[index])/86400000);
- let colour = dateColours[day % 6];
- if(day==30){ colour=dateColours[6]; }
- return({"year":year,"month":month,"day":day,"colour":colour});
+ let index = sequence.findIndex(n => n > dt)-1;
+ let year = baseYear+parseInt(index/12);
+ let month = index % 12;
+ let day = parseInt((dt-sequence[index])/86400000);
+ let colour = dateColours[day % 6];
+ if(day==30){ colour=dateColours[6]; }
+ return({"year":year,"month":month,"day":day,"colour":colour});
}
function toggleTimeDigits(){
- addTimeDigit = !addTimeDigit;
- modeTime();
+ addTimeDigit = !addTimeDigit;
+ modeTime();
}
function toggleDateFormat(){
- dateFormat = !dateFormat;
- modeTime();
+ dateFormat = !dateFormat;
+ modeTime();
}
function formatDate(res,dateFormat){
- let yyyy = res.year.toString(12);
- calenDef = calen10;
- if(!dateFormat){ //ordinal format
- let mm = ("0"+(res.month+1).toString(12)).substr(-2);
- let dd = ("0"+(res.day+1).toString(12)).substr(-2);
- if(res.day==30){
- calenDef = calen7;
- let m = ((res.month+1).toString(12)).substr(-2);
- return(yyyy+"-"+"S"+m); // ordinal format
- }
- return(yyyy+"-"+mm+"-"+dd);
- }
- let m = res.month.toString(12); // cardinal format
- let w = parseInt(res.day/6);
- let d = res.day%6;
- //return(yyyy+"-"+res.month+"-"+w+"-"+d);
- return(yyyy+"-"+m+"-"+w+"-"+d);
+ let yyyy = res.year.toString(12);
+ calenDef = calen10;
+ if(!dateFormat){ //ordinal format
+ let mm = ("0"+(res.month+1).toString(12)).substr(-2);
+ let dd = ("0"+(res.day+1).toString(12)).substr(-2);
+ if(res.day==30){
+ calenDef = calen7;
+ let m = ((res.month+1).toString(12)).substr(-2);
+ return(yyyy+"-"+"S"+m); // ordinal format
+ }
+ return(yyyy+"-"+mm+"-"+dd);
+ }
+ let m = res.month.toString(12); // cardinal format
+ let w = parseInt(res.day/6);
+ let d = res.day%6;
+ //return(yyyy+"-"+res.month+"-"+w+"-"+d);
+ return(yyyy+"-"+m+"-"+w+"-"+d);
}
function writeDozTime(text,def){
- let pts = def.pts;
- let x=def.pt0[0];
- let y=def.pt0[1];
- g_t.clear();
+ let pts = def.pts;
+ let x=def.pt0[0];
+ let y=def.pt0[1];
+ g_t.clear();
g_t.setFont("Vector",def.size);
- for(let i in text){
- if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new
- else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new
- else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
- x = x+def.step[0];
- y = y+def.step[1];
- }
+ for(let i in text){
+ if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); }
+ else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); }
+ else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
+ x = x+def.step[0];
+ y = y+def.step[1];
+ }
}
function writeDozDate(text,def,colour){
-
- dateColour = colour;
- let pts = def.pts;
- let x=def.pt0[0];
- let y=def.pt0[1];
- g_d.clear();
- g_d.setFont("Vector",def.size);
- for(let i in text){
- if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new
- else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new
- else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
- x = x+def.step[0];
- y = y+def.step[1];
- }
+
+ dateColour = colour;
+ let pts = def.pts;
+ let x=def.pt0[0];
+ let y=def.pt0[1];
+ g_d.clear();
+ g_d.setFont("Vector",def.size);
+ for(let i in text){
+ if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); }
+ else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); }
+ else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
+ x = x+def.step[0];
+ y = y+def.step[1];
+ }
}
+Bangle.loadWidgets();
+//for malaire's Adjust Clock widget, if used
+function adjustedNow() {
+ return WIDGETS.adjust ? new Date(WIDGETS.adjust.now()) : new Date();
+}
+Bangle.drawWidgets();
+
// Functions for time mode
function drawTime()
{
- let dt = new Date();
- let date = "";
- let timeDef;
- let x = 0;
- dt.setDate(dt.getDate());
- if(addTimeDigit){
- x =
- 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds();
- let msg = "00000"+Math.floor(x).toString(12);
- let time = msg.substr(-5,3)+"."+msg.substr(-2);
- let wait = 347*(1-(x%1));
- timeDef = time6;
- } else {
- x =
- 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds();
- let msg = "0000"+Math.floor(x).toString(12);
- let time = msg.substr(-4,3)+"."+msg.substr(-1);
- let wait = 4167*(1-(x%1));
- timeDef = time5;
- }
- if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day
- date = formatDate(res,dateFormat);
- if(dt x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day
+ date = formatDate(res,dateFormat);
+ if(dt2200)) {
- } else {
- // We have a GPS time. Set time
- setTime(g.time.getTime()/1000);
- }
- });
- Bangle.setGPSPower(1,"time");
- setTimeout(fixTime, 10*60*1000); // every 10 minutes
-}
-// Start time fixing with GPS on next 10 minute interval
-setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000);
diff --git a/apps/doztime/metadata.json b/apps/doztime/metadata.json
index d206cb0c3..6933487ab 100644
--- a/apps/doztime/metadata.json
+++ b/apps/doztime/metadata.json
@@ -2,7 +2,7 @@
"id": "doztime",
"name": "Dozenal Time",
"shortName": "Dozenal Time",
- "version": "0.04",
+ "version": "0.05",
"description": "A dozenal Holocene calendar and dozenal diurnal clock",
"icon": "app.png",
"type": "clock",
diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog
index 811784b39..da07af798 100644
--- a/apps/dtlaunch/ChangeLog
+++ b/apps/dtlaunch/ChangeLog
@@ -8,3 +8,4 @@
0.08: Optimize line wrapping for Bangle 2
0.09: fix the trasparent widget bar if there are no widgets for Bangle 2
0.10: added "one click exit" setting for Bangle 2
+0.11: Fix bangle.js 1 white icons not displaying
diff --git a/apps/dtlaunch/app-b1.js b/apps/dtlaunch/app-b1.js
index ec0569127..ed9cc778e 100644
--- a/apps/dtlaunch/app-b1.js
+++ b/apps/dtlaunch/app-b1.js
@@ -48,6 +48,7 @@ function draw_icon(p,n,selected) {
var x = (n%3)*80;
var y = n>2?130:40;
(selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89);
+ g.setColor(g.theme.fg);
g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25});
g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1);
var txt = apps[p*6+n].name.split(" ");
diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json
index 7a4094e54..b3f94442f 100644
--- a/apps/dtlaunch/metadata.json
+++ b/apps/dtlaunch/metadata.json
@@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
- "version": "0.10",
+ "version": "0.11",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",
diff --git a/apps/game1024/ChangeLog b/apps/game1024/ChangeLog
new file mode 100644
index 000000000..8759fb428
--- /dev/null
+++ b/apps/game1024/ChangeLog
@@ -0,0 +1,6 @@
+0.01: Initial version
+0.02: Temporary intermediate version
+0.03: Basic colors
+0.04: Bug fix score reset after Game Over, new icon
+0.05: Chevron marker on the randomly added square
+0.06: Fixed issue 1609 added a message popup state handler to control unwanted screen redraw
\ No newline at end of file
diff --git a/apps/game1024/README.md b/apps/game1024/README.md
new file mode 100644
index 000000000..500453145
--- /dev/null
+++ b/apps/game1024/README.md
@@ -0,0 +1,36 @@
+
+# Play the game of 1024
+
+Move the tiles by swiping to the lefthand, righthand or up- and downward side of the watch.
+
+When two tiles with the same number are squashed together they will add up as exponentials:
+
+**1 + 1 = 2** or **A + A = D** which is a representation of **2^1 + 2^1 = 2^1 = 4**
+
+**2 + 2 = 3** or **B + B = C** which is a representation of **2^2 + 2^2 = 2^3 = 8**
+
+**3 + 3 = 4** or **C + C = D** which is a representation of **2^3 + 2^3 = 2^4 = 16**
+
+After each move a new tile will be added on a random empty square. The value can be 1 or 2, and will be marked with a chevron.
+
+So you can continue till you reach **1024** which equals **2^(10)**. So when you reach tile **10** you have won.
+
+The score is maintained by adding the outcome of the sum of all pairs of squashed tiles (4+16+4+8 etc.)
+
+Use the side **BTN** to exit the game, score and tile positions will be saved.
+
+## Buttons on the screen
+
+ - Button **U**: Undo the last move. There are currently a maximum of 4 undo levels. The level is indicated with a small number in the lower righthand corner of the Undo button
+ - Button **\***: Change the text on the tile to number, capitals or Roman numbers
+ - Button **R**: Reset the game. The Higscore will be remembered. You will be prompted first.
+
+### Credits
+
+Game 1024 is based on Saming's 2048 and Misho M. Petkovic 1024game.org and conceptually similar to Threes by Asher Vollmer.
+
+In Dark theme with numbers:
+
+
+In Light theme with characters:
+
\ No newline at end of file
diff --git a/apps/game1024/app-icon.js b/apps/game1024/app-icon.js
new file mode 100644
index 000000000..8e8b56d9f
--- /dev/null
+++ b/apps/game1024/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBkQAWkAyVgQXx5gAMCQOqAAeiC/4X/AAXdC6HP7gECn///oXH///+QXEn4XC4f/mf/AwQXEmczmQXD74QD7/8AQZHLFIPfC4QzC4ZICC5XPngXD/4CB5oXNIYQXG+YXSCYQXKkQXWU4oXbL5mjC5M/R5evC5PfniwBa5Gvd4gXE5/z7s/DQIXGl6PJ5v//5eCC46/F4YXCAgMzAoYXFkYXFABTvMC/4X0ACkCC/4XJu4AMCQOIAAeCC+0///zC6dz/8z/83C6V/CgN/+4XSn4DCF6ZcGC6Hyv53V+Z3WCgR3OkQAWA="))
\ No newline at end of file
diff --git a/apps/game1024/app.js b/apps/game1024/app.js
new file mode 100644
index 000000000..133630634
--- /dev/null
+++ b/apps/game1024/app.js
@@ -0,0 +1,699 @@
+const debugMode = 'off'; // valid values are: off, test, production, development
+const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)};
+const rows = 4, cols = 4;
+const borderWidth = 6;
+const sqWidth = (Math.floor(Bangle.appRect.w - 48) / rows) - borderWidth;
+const cellColors = [{bg:'#00FFFF', fg: '#000000'},
+ {bg:'#FF00FF', fg: '#000000'}, {bg:'#808000', fg: '#FFFFFF'}, {bg:'#0000FF', fg: '#FFFFFF'}, {bg:'#008000', fg: '#FFFFFF'},
+ {bg:'#800000', fg: '#FFFFFF'}, {bg:'#00FF00', fg: '#000000'}, {bg:'#000080', fg: '#FFFFFF'}, {bg:'#FFFF00', fg: '#000000'},
+ {bg:'#800080', fg: '#FFFFFF'}, {bg:'#FF0000', fg: '#FFFFFF'}];
+const cellFonts = ["12x20", "12x20", "Vector:14"];
+const cellChars = [
+ [0,1,2,3,4,5,6,7,8,9,10],
+ ['0','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
+ ['0','I', 'II', 'III', 'IV', 'V', 'VI', 'VII','VIII', 'IX', 'X']
+];
+// const numInitialCells = 2;
+const maxUndoLevels = 4;
+const noExceptions = true;
+let charIndex = 0; // plain numbers on the grid
+const themeBg = g.theme.bg;
+
+
+const scores = {
+ currentScore: 0,
+ highScore: 0,
+ lastScores: [0],
+ add: function(val) {
+ this.currentScore = this.currentScore + Math.pow(2, val);
+ debug(() => console.log("new score=",this.currentScore));
+ },
+ addToUndo: function () {
+ this.lastScores.push(this.currentScore);
+ if (this.lastScores.length > maxUndoLevels) this.lastScores.shift();
+ },
+ undo: function () {
+ this.currentScore = this.lastScores.pop();
+ debug(() => console.log("undo score =", this.currentScore, "rest:", this.lastScores));
+ },
+ reset: function () {
+ this.currentScore = 0;
+ this.lastScores = [0];
+ },
+ draw: function () {
+ g.setColor(btnAtribs.fg);
+ let ulCorner = {x: Bangle.appRect.x + 6, y: Bangle.appRect.y2 -22 };
+ let lrCorner = {x: Bangle.appRect.x2, y: Bangle.appRect.y2 - 1};
+ g.fillRect(ulCorner.x, ulCorner.y, lrCorner.x, lrCorner.y)
+ .setFont12x20(1)
+ .setFontAlign(0,0,0);
+ let scrX = Math.floor((ulCorner.x + lrCorner.x)/3);
+ let scrY = Math.floor((ulCorner.y + lrCorner.y)/2) + 1;
+ g.setColor('#000000')
+ .drawString(this.currentScore, scrX+1, scrY+1)
+ .setColor(btnAtribs.bg)
+ .drawString(this.currentScore, scrX, scrY);
+ scrX = Math.floor(4*(ulCorner.x + lrCorner.x)/5);
+ g.setFont("6x8:1x2")
+ .drawString(this.highScore, btnAtribs.x + Math.floor(btnAtribs.w/2), scrY);
+ },
+ hsContents: function () {
+ return {"highScore": this.highScore, "lastScore": this.currentScore};
+ },
+ check: function () {
+ this.highScore = (this.currentScore > this.highScore) ? this.currentScore : this.highScore;
+ debug(() => console.log('highScore =', this.highScore));
+ }
+};
+
+// snapshot interval is the number of moves after wich a snapshot is wriiten to file
+const snInterval = 1;
+
+const snReadOnInit = true;
+// a snapshot contains a json file dump of the last positions of the tiles on the board, including the scores
+const snapshot = {
+ interval: snInterval,
+ snFileName: 'game1024.json',
+ counter: 0,
+ updCounter: function() {
+ this.counter = ++this.counter > this.interval ? 0 : this.counter;
+ },
+ dump: {gridsize: rows * cols, expVals: [], score: 0, highScore: 0, charIndex: charIndex},
+ write: function() {
+ require("Storage").writeJSON(this.snFileName, this.dump);
+ },
+ read: function () {
+ let sn = require("Storage").readJSON(this.snFileName, noExceptions);
+ if ((typeof sn == "undefined") || (sn.gridsize !== rows * cols)) {
+ require("Storage").writeJSON(this.snFileName, this.dump);
+ return false;
+ } else {
+ if ((typeof sn !== "undefined") && (sn.gridsize == rows * cols)){
+ this.dump = sn;
+ return true;
+ }
+ }
+ },
+ setDump: function () {
+ this.dump.expVals = [];
+ allSquares.forEach(sq => {
+ this.dump.expVals.push(sq.expVal);
+ });
+ this.dump.score = scores.currentScore;
+ this.dump.highScore = scores.highScore;
+ this.dump.charIndex = charIndex;
+ },
+ make: function () {
+ this.updCounter();
+ if (this.counter == this.interval) {
+ this.setDump();
+ this.write();
+ debug(() => console.log("snapped the state of the game:", this.dump));
+ }
+ },
+ recover: function () {
+ if (this.read()) {
+ this.dump.expVals.forEach((val, idx) => {
+ allSquares[idx].setExpVal(val);
+ });
+ scores.currentScore = this.dump.score ? this.dump.score : 0;
+ scores.highScore = this.dump.highScore ? this.dump.highScore : 0 ;
+ charIndex = this.dump.charIndex ? this.dump.charIndex : 0 ;
+ }
+ },
+ reset: function () {
+ this.dump.gridsize = rows * cols;
+ this.dump.expVals = [];
+ for (let i = 0; i< this.dump.gridsize; i++) {
+ this.dump.expVals[i] = 0;
+ }
+ this.dump.score = 0;
+ this.dump.highScore = scores.highScore;
+ this.dump.charIndex = charIndex;
+ this.write();
+ debug(() => console.log("reset D U M P E D!", this.dump));
+ }
+};
+const btnAtribs = {x: 134, w: 42, h: 42, fg:'#C0C0C0', bg:'#800000'};
+const buttons = {
+ all: [],
+ draw: function () {
+ this.all.forEach(btn => {
+ btn.draw();
+ });
+ },
+ add: function(btn) {
+ this.all.push(btn);
+ },
+ isPopUpActive: false,
+ activatePopUp: function() {
+ this.isPopUpActive = true;
+ },
+ deActivatePopUp: function() {
+ this.isPopUpActive = false;
+ }
+};
+/**
+ * to the right = -1
+ all tiles move to the left, begin with the outer righthand side tiles
+ moving 0 to max 3 places to the right
+
+ find first tile beginning with bottom row, righthand side
+ */
+
+const mover = {
+ direction: {
+ up: {name: 'up', step: 1, innerBegin: 0, innerEnd: rows-1, outerBegin: 0, outerEnd: cols-1, iter: rows -1,
+ sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m < rows -1 ? (m+1)*(cols) + n : -1;}
+ },
+ down: {name: 'down', step:-1, innerBegin: rows-1, innerEnd: 0, outerBegin: cols-1, outerEnd: 0, iter: rows -1,
+ sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m > 0 ? (m-1)*(cols) + n : -1;}
+ },
+ left: {name: 'left', step: 1, innerBegin: 0, innerEnd: cols-1, outerBegin: 0, outerEnd: rows-1, iter: cols -1,
+ sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m < cols -1 ? n*(rows) + m +1 : -1;}
+ },
+ right: {name: 'right', step:-1, innerBegin: cols-1, innerEnd: 0, outerBegin: rows-1, outerEnd: 0, iter: cols -1,
+ sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m > 0 ? n*(rows) + m -1: -1;}
+ }
+ },
+ anyLeft: function() {
+ let canContinue = false;
+ [this.direction.up,this.direction.left].forEach (dir => {
+ const step = dir.step;
+ // outer loop for all colums/rows
+ for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) {
+ // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up
+ for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) {
+ const idx = dir.sqIndex(m,n);
+ const nextIdx = dir.sqNextIndex(m,n);
+ if (allSquares[idx].expVal == 0) {
+ canContinue = true; // there is an empty cell found
+ break;
+ }
+ if (nextIdx >= 0) {
+ if (allSquares[idx].expVal == allSquares[nextIdx].expVal) {
+ canContinue = true; // equal adjacent cells > 0 found
+ break;
+ }
+ if (allSquares[nextIdx].expVal == 0) {
+ canContinue = true; // there is an empty cell found
+ break;
+ }
+ }
+ if (canContinue) break;
+ }
+ if (canContinue) break;
+ }
+ });
+ return canContinue;
+ },
+ nonEmptyCells: function (dir) {
+ debug(() => console.log("Move: ", dir.name));
+ const step = dir.step;
+ // outer loop for all colums/rows
+ for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) {
+ // let rowStr = '| ';
+
+ // Move a number of iteration with the squares to move them all to one side
+ for (let iter = 0; iter < dir.iter; iter++) {
+
+ // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up
+ for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) {
+ // get the array of squares index for current cell
+ const idx = dir.sqIndex(m,n);
+ const nextIdx = dir.sqNextIndex(m,n);
+
+ if (allSquares[idx].expVal == 0 && nextIdx >= 0) {
+ allSquares[idx].setExpVal(allSquares[nextIdx].expVal);
+ allSquares[nextIdx].setExpVal(0);
+ }
+ }
+ }
+ }
+ },
+ // add up the conjacent squares with identical values en set next square to empty in the process
+ mergeEqlCells: function(dir) {
+ const step = dir.step;
+ // outer loop for all colums/rows
+ for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) {
+ // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up
+ for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) {
+ const idx = dir.sqIndex(m,n);
+ const nextIdx = dir.sqNextIndex(m,n);
+
+ if ((allSquares[idx].expVal > 0) && nextIdx >= 0) {
+ if (allSquares[idx].expVal == allSquares[nextIdx].expVal) {
+ let expVal = allSquares[idx].expVal;
+ allSquares[idx].setExpVal(++expVal);
+ allSquares[idx].addToScore();
+ allSquares[nextIdx].setExpVal(0);
+ }
+ }
+ }
+ }
+ }
+};
+// Minimum number of pixels to interpret it as drag gesture
+const dragThreshold = 10;
+
+// Maximum number of pixels to interpret a click from a drag event series
+const clickThreshold = 3;
+
+let allSquares = [];
+
+class Button {
+ constructor(name, x0, y0, width, height, text, bg, fg, cb, enabled) {
+ this.x0 = x0;
+ this.y0 = y0;
+ this.x1 = x0 + width;
+ this.y1 = y0 + height;
+ this.name = name;
+ this.cb = cb;
+ this.text = text;
+ this.bg = bg;
+ this.fg = fg;
+ this.font = "6x8:3";
+ this.enabled = enabled;
+ }
+ disable() {
+ this.enabled = false;
+ }
+ enable() {
+ this.enabled = true;
+ }
+ draw() {
+ g.setColor(this.bg)
+ .fillRect(this.x0, this.y0, this.x1, this.y1)
+ .setFont(this.font)
+ .setFontAlign(0,0,0);
+ let strX = Math.floor((this.x0+this.x1)/2);
+ let strY = Math.floor((this.y0+this.y1)/2);
+ g.setColor("#000000")
+ .drawString(this.text, strX+2, strY+2)
+ .setColor(this.fg)
+ .drawString(this.text, strX, strY);
+ // buttons.push(this);
+ }
+ onClick() {if (typeof this.cb === 'function' && this.enabled) {
+ this.cb(this);
+ }
+ }
+}
+
+class Cell {
+ constructor(x0, y0, width, idx, cb) {
+ this.x0 = x0;
+ this.y0 = y0;
+ this.x1 = x0 + width;
+ this.y1 = y0 + width;
+ this.expVal = 0;
+ this.previousExpVals=[];
+ this.idx = idx;
+ this.cb = cb;
+ this.isRndm = false;
+ this.ax = x0;
+ this.ay = Math.floor(0.2*width+y0);
+ this.bx = Math.floor(0.3*width+x0);
+ this.by = Math.floor(0.5*width+y0);
+ this.cx = x0;
+ this.cy = Math.floor(0.8*width+y0);
+ }
+ getColor(i) {
+ return cellColors[i >= cellColors.length ? cellColors.length -1 : i];
+ }
+ drawBg() {
+ debug(()=>console.log("Drawbg!!"));
+ if (this.isRndm == true) {
+ debug(()=>console.log('Random: (ax)', this.ax));
+ g.setColor(this.getColor(this.expVal).bg)
+ .fillRect(this.x0, this.y0, this.x1, this.y1)
+ .setColor(themeBg)
+ .fillPoly([this.cx,this.cy,this.bx,this.by,this.ax,this.ay]);
+ } else {
+ g.setColor(this.getColor(this.expVal).bg)
+ .fillRect(this.x0, this.y0, this.x1, this.y1);
+ }
+ }
+ drawNumber() {
+ if (this.expVal !== 0) {
+ g.setFont(cellFonts[charIndex])
+ .setFontAlign(0,0,0);
+ let char = cellChars[charIndex][this.expVal];
+ let strX = Math.floor((this.x0 + this.x1)/2);
+ let strY = Math.floor((this.y0 + this.y1)/2);
+ g.setColor(this.getColor(this.expVal).fg)
+ .drawString(char, strX, strY);
+ }
+ }
+ setExpVal(val) {
+ this.expVal = val;
+ }
+ getIdx() {return this.idx;}
+ pushToUndo() {
+ // remember this new step
+ this.previousExpVals.push(this.expVal);
+ // keep the undo list not longer than max undo levels
+ if (this.previousExpVals.length > maxUndoLevels) this.previousExpVals.shift();
+ }
+ popFromUndo() {
+ // take one step back
+ if (this.previousExpVals.length > 0) {
+ this.expVal = this.previousExpVals.pop();
+ }
+ }
+ removeUndo() {
+ this.previousExpVals=[0];
+ }
+ addToScore() {if (typeof this.cb === 'function') {
+ this.cb(this.expVal);
+ }
+ }
+ setRndmFalse() {
+ this.isRndm = false;
+ }
+ setRndmTrue() {
+ this.isRndm = true;
+ }
+ drawRndmIndicator(){
+ if (this.isRndm == true) {
+ debug(()=>console.log('Random: (ax)', this.ax));
+ g.setColor(this.getColor(0).bg)
+ .fillPoly(this.ax,this.ay,this.bx,this.by,this.cx,this.cy);
+ }
+ }
+}
+
+function undoGame() {
+ g.clear();
+ if (scores.lastScores.length > 0) {
+ allSquares.forEach(sq => {
+ sq.popFromUndo();
+ sq.drawBg();
+ sq.drawNumber();
+ });
+ scores.undo();
+ scores.draw();
+ buttons.draw();
+ updUndoLvlIndex();
+ snapshot.make();
+ }
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+}
+function addToUndo() {
+ allSquares.forEach(sq => {
+ sq.pushToUndo();
+ });
+ scores.addToUndo();
+}
+function addToScore (val) {
+ scores.add(val);
+ if (val == 10) messageYouWin();
+}
+function createGrid () {
+ let cn =0;
+ for (let r = 0; r < rows; r++) {
+ for (let c = 0; c < cols; c++) {
+ let x0 = borderWidth + c*(borderWidth + sqWidth) - (rows/2)*(2*borderWidth + sqWidth) + middle.x + Math.floor(sqWidth/3);
+ let y0 = borderWidth + r*(borderWidth + sqWidth) - (cols/2)*(2*borderWidth + sqWidth) + middle.y + Math.floor(sqWidth/3);
+ let cell = new Cell(x0, y0, sqWidth, c + r*cols, addToScore);
+ allSquares.push(cell);
+ }
+ }
+}
+function messageGameOver () {
+ const c = (g.theme.dark) ? {"fg": "#FFFFFF", "bg": "#808080"} : {"fg": "#FF0000", "bg": "#000000"};
+ g.setColor(c.bg)
+ .setFont12x20(2).setFontAlign(0,0,0)
+ .drawString("G A M E", middle.x+13, middle.y-24)
+ .drawString("O V E R !", middle.x+13, middle.y+24);
+ g.setColor(c.fg)
+ .drawString("G A M E", middle.x+12, middle.y-25)
+ .drawString("O V E R !", middle.x+12, middle.y+25);
+}
+function messageYouWin () {
+ g.setColor("#1a0d00")
+ .setFont12x20(2)
+ .setFontAlign(0,0,0)
+ .drawString("YOU HAVE", middle.x+18, middle.y-24)
+ .drawString("W O N ! !", middle.x+18, middle.y+24);
+ g.setColor("#FF0808")
+ .drawString("YOU HAVE", middle.x+17, middle.y-25)
+ .drawString("W O N ! !", middle.x+17, middle.y+25);
+ Bangle.buzz(200, 1);
+}
+function makeRandomNumber () {
+ return Math.ceil(2*Math.random());
+}
+function addRandomNumber() {
+ let emptySquaresIdxs = [];
+ allSquares.forEach(sq => {
+ if (sq.expVal == 0) emptySquaresIdxs.push(sq.getIdx());
+ });
+ if (emptySquaresIdxs.length > 0) {
+ let randomIdx = Math.floor( emptySquaresIdxs.length * Math.random() );
+ allSquares[emptySquaresIdxs[randomIdx]].setExpVal(makeRandomNumber());
+ allSquares[emptySquaresIdxs[randomIdx]].setRndmTrue();
+ }
+}
+function drawGrid() {
+ allSquares.forEach(sq => {
+ sq.drawBg();
+ // sq.drawRndmIndicator();
+ sq.drawNumber();
+ });
+}
+function initGame() {
+ g.clear();
+ // scores.read();
+ createGrid();
+ if (snReadOnInit) {
+ snapshot.recover();
+ debug(() => console.log("R E C O V E R E D !", snapshot.dump));
+ let sum = allSquares.reduce(function (tv, sq) {return (sq.expVal + tv) ;}, 0);
+ if (!sum) {
+ addRandomNumber();
+ }
+ } else {
+ addRandomNumber();
+ // addToUndo();
+ }
+ addRandomNumber();
+ drawGrid();
+ scores.draw();
+ buttons.draw();
+ // Clock mode allows short-press on button to exit
+ Bangle.setUI("clock");
+ // Load widgets
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+}
+function drawPopUp(message,cb) {
+ buttons.activatePopUp();
+ g.setColor('#FFFFFF');
+ let rDims = Bangle.appRect;
+ g.fillPoly([rDims.x+10, rDims.y+20,
+ rDims.x+20, rDims.y+10,
+ rDims.x2-30, rDims.y+10,
+ rDims.x2-20, rDims.y+20,
+ rDims.x2-20, rDims.y2-40,
+ rDims.x2-30, rDims.y2-30,
+ rDims.x+20, rDims.y2-30,
+ rDims.x+10, rDims.y2-40
+ ]);
+ buttons.all.forEach(btn => {btn.disable();});
+ const btnYes = new Button('yes', rDims.x+16, rDims.y2-80, 54, btnAtribs.h, 'YES', btnAtribs.fg, btnAtribs.bg, cb, true);
+ const btnNo = new Button('no', rDims.x2-80, rDims.y2-80, 54, btnAtribs.h, 'NO', btnAtribs.fg, btnAtribs.bg, cb, true);
+ btnYes.draw();
+ btnNo.draw();
+ g.setColor('#000000');
+ g.setFont12x20(1);
+ g.setFontAlign(-1,-1,0);
+ g.drawString(message, rDims.x+20, rDims.y+20);
+ buttons.add(btnYes);
+ buttons.add(btnNo);
+
+}
+function handlePopUpClicks(btn) {
+ const name = btn.name;
+ buttons.all.pop(); // remove the no button
+ buttons.all.pop(); // remove the yes button
+ buttons.all.forEach(b => {b.enable();}); // enable the remaining buttons again
+ debug(() => console.log("Button name =", name));
+ buttons.deActivatePopUp();
+ switch (name) {
+ case 'yes':
+ resetGame();
+ break;
+ default:
+ g.clear();
+ drawGrid();
+ scores.draw();
+ buttons.draw();
+ updUndoLvlIndex();
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+ }
+}
+function resetGame() {
+ g.clear();
+ scores.reset();
+ allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();sq.setRndmFalse();});
+ addRandomNumber();
+ addRandomNumber();
+ drawGrid();
+ scores.draw();
+ buttons.draw();
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+}
+
+/**
+ * Function that can be used in test or development environment, or production.
+ * Depends on global constant debugMode
+ * @param {function} func function to call like console.log()
+ */
+ const debug = (func) => {
+ switch (debugMode) {
+ case "development":
+ if (typeof func === 'function') {
+ func();
+ }
+ break;
+ case "off":
+ default: break;
+ }
+};
+
+// Handle a "click" event (only needed for menu button)
+function handleclick(e) {
+ buttons.all.forEach(btn => {
+ if ((e.x >= btn.x0) && (e.x <= btn.x1) && (e.y >= btn.y0) && (e.y <= btn.y1)) {
+ btn.onClick();
+ debug(() => console.log(btn.name));
+ }
+ });
+}
+
+// Handle a drag event (moving the stones around)
+function handledrag(e) {
+ // Stop moving things around when the popup message is active
+ // Bangleapps issue #1609
+ if (!(buttons.isPopUpActive)) {
+ runGame((Math.abs(e.dx) > Math.abs(e.dy) ?
+ (e.dx > 0 ? mover.direction.right : mover.direction.left ) :
+ (e.dy > 0 ? mover.direction.down : mover.direction.up )));
+ }
+}
+// Evaluate "drag" events from the UI and call handlers for drags or clicks
+// The UI sends a drag as a series of events indicating partial movements
+// of the finger.
+// This class combines such parts to a long drag from start to end
+// If the drag is short, it is interpreted as click,
+// otherwise as drag.
+// The approprate method is called with the data of the drag.
+class Dragger {
+
+ constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) {
+ this.clickHandler = clickHandler;
+ this.dragHandler = dragHandler;
+ this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold);
+ this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold);
+ this.dx = 0;
+ this.dy = 0;
+ this.enabled = true;
+ }
+
+ // Enable or disable the Dragger
+ setEnabled(b) {
+ this.enabled = b;
+ }
+
+ // Handle a raw drag event from the UI
+ handleRawDrag(e) {
+ if (!this.enabled)
+ return;
+ this.dx += e.dx; // Always accumulate
+ this.dy += e.dy;
+ if (e.b === 0) { // Drag event ended: Evaluate full drag
+ if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold)
+ this.clickHandler({
+ x: e.x - this.dx,
+ y: e.y - this.dy
+ }); // take x and y from the drag start
+ else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold)
+ this.dragHandler({
+ x: e.x - this.dx,
+ y: e.y - this.dy,
+ dx: this.dx,
+ dy: this.dy
+ });
+ this.dx = 0; // Clear the drag accumulator
+ this.dy = 0;
+ }
+ }
+
+ // Attach the drag evaluator to the UI
+ attach() {
+ Bangle.on("drag", e => this.handleRawDrag(e));
+ }
+}
+
+// Dragger is needed for interaction during the game
+var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold);
+
+// Disable dragger as board is not yet initialized
+dragger.setEnabled(false);
+
+// Nevertheless attach it so that it is ready once the game starts
+dragger.attach();
+
+function runGame(dir){
+ addToUndo();
+ updUndoLvlIndex();
+ mover.nonEmptyCells(dir);
+ mover.mergeEqlCells(dir);
+ mover.nonEmptyCells(dir);
+ allSquares.forEach(sq => {sq.setRndmFalse();});
+ addRandomNumber();
+ drawGrid();
+ scores.check();
+ scores.draw();
+ // scores.write();
+ snapshot.make();
+ dragger.setEnabled(true);
+ if (!(mover.anyLeft())) {
+ debug(() => console.log("G A M E O V E R !!"));
+ snapshot.reset();
+ messageGameOver();
+ }
+}
+
+function updUndoLvlIndex() {
+ let x = 170;
+ let y = 60;
+ g.setColor(btnAtribs.fg)
+ .fillRect(x-6,y-6, 176, 67);
+ if (scores.lastScores.length > 0) {
+ g.setColor("#000000")
+ .setFont("4x6:2")
+ .drawString(scores.lastScores.length, x, y);
+ }
+}
+function incrCharIndex() {
+ charIndex++;
+ if (charIndex >= cellChars.length) charIndex = 0;
+ drawGrid();
+}
+buttons.add(new Button('undo', btnAtribs.x, 25, btnAtribs.w, btnAtribs.h, 'U', btnAtribs.fg, btnAtribs.bg, undoGame, true));
+buttons.add(new Button('chars', btnAtribs.x, 71, btnAtribs.w, 31, '*', btnAtribs.fg, btnAtribs.bg, function(){incrCharIndex();}, true));
+buttons.add(new Button('restart', btnAtribs.x, 106, btnAtribs.w, btnAtribs.h, 'R', btnAtribs.fg, btnAtribs.bg, function(){drawPopUp('Do you want\nto restart?',handlePopUpClicks);}, true));
+
+initGame();
+
+dragger.setEnabled(true);
+
+E.on('kill',function() {
+ this.write();
+ debug(() => console.log("1024 game got killed!"));
+});
\ No newline at end of file
diff --git a/apps/game1024/game1024.app.info b/apps/game1024/game1024.app.info
new file mode 100644
index 000000000..b1c9d84ce
--- /dev/null
+++ b/apps/game1024/game1024.app.info
@@ -0,0 +1,6 @@
+require("Storage").write("timer.info",{
+ "id":"game1024",
+ "name":"1024 Game",
+ "src":"game1024.app.js",
+ "icon":"game1024.img"
+});
\ No newline at end of file
diff --git a/apps/game1024/game1024.json b/apps/game1024/game1024.json
new file mode 100644
index 000000000..3749649ee
--- /dev/null
+++ b/apps/game1024/game1024.json
@@ -0,0 +1 @@
+{"gridsize": 16, "expVals": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], "score": 0, "highScore": 0, "charIndex": 1}
\ No newline at end of file
diff --git a/apps/game1024/game1024.png b/apps/game1024/game1024.png
new file mode 100644
index 000000000..c0f7eaf21
Binary files /dev/null and b/apps/game1024/game1024.png differ
diff --git a/apps/game1024/game1024_sc_dump_dark.png b/apps/game1024/game1024_sc_dump_dark.png
new file mode 100644
index 000000000..87577ecfa
Binary files /dev/null and b/apps/game1024/game1024_sc_dump_dark.png differ
diff --git a/apps/game1024/game1024_sc_dump_light.png b/apps/game1024/game1024_sc_dump_light.png
new file mode 100644
index 000000000..06ada65ac
Binary files /dev/null and b/apps/game1024/game1024_sc_dump_light.png differ
diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json
new file mode 100644
index 000000000..73d7607f3
--- /dev/null
+++ b/apps/game1024/metadata.json
@@ -0,0 +1,17 @@
+{ "id": "game1024",
+ "name": "1024 Game",
+ "shortName" : "1024 Game",
+ "version": "0.06",
+ "icon": "game1024.png",
+ "screenshots": [ {"url":"screenshot.png" } ],
+ "readme":"README.md",
+ "description": "Swipe the squares up, down, to the left or right, join the numbers and get to the 10 (2^1024), J or X tile!",
+ "type": "app",
+ "tags": "game,puzzle",
+ "allow_emulator": true,
+ "supports" : ["BANGLEJS2"],
+ "storage": [
+ {"name":"game1024.app.js","url":"app.js"},
+ {"name":"game1024.img","url":"app-icon.js","evaluate":true}
+ ]
+ }
diff --git a/apps/game1024/screenshot.png b/apps/game1024/screenshot.png
new file mode 100644
index 000000000..8be52f8cb
Binary files /dev/null and b/apps/game1024/screenshot.png differ
diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js
index a701ef3a9..0cecad73b 100644
--- a/apps/gbdebug/app-icon.js
+++ b/apps/gbdebug/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))
+require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))
\ No newline at end of file
diff --git a/apps/gsat/ChangeLog b/apps/gsat/ChangeLog
new file mode 100644
index 000000000..48156d0d4
--- /dev/null
+++ b/apps/gsat/ChangeLog
@@ -0,0 +1 @@
+0.01: Added Source Code
diff --git a/apps/gsat/README.md b/apps/gsat/README.md
new file mode 100644
index 000000000..faf986947
--- /dev/null
+++ b/apps/gsat/README.md
@@ -0,0 +1,3 @@
+# Geek Squad Appointment Timer
+
+An app dedicated to setting a 20 minute timer for Geek Squad Appointments.
diff --git a/apps/gsat/app-icon.js b/apps/gsat/app-icon.js
new file mode 100644
index 000000000..06f93e2ef
--- /dev/null
+++ b/apps/gsat/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))
diff --git a/apps/gsat/app.js b/apps/gsat/app.js
new file mode 100644
index 000000000..3a7d443fe
--- /dev/null
+++ b/apps/gsat/app.js
@@ -0,0 +1,38 @@
+// Clear screen
+g.clear();
+
+const secsinmin = 60;
+const quickfixperiod = 900;
+var seconds = 1200;
+
+function countSecs() {
+ if (seconds != 0) {seconds -=1;}
+ console.log(seconds);
+}
+function drawTime() {
+ g.clear();
+ g.setFontAlign(0,0);
+ g.setFont('Vector', 12);
+ g.drawString('Geek Squad Appointment Timer', 125, 20);
+ if (seconds == 0) {
+ g.setFont('Vector', 35);
+ g.drawString('Appointment', 125, 100);
+ g.drawString('finished!', 125, 150);
+ Bangle.buzz();
+ return;
+ }
+ min = seconds / secsinmin;
+ if (seconds < quickfixperiod) {
+ g.setFont('Vector', 20);
+ g.drawString('Quick Fix', 125, 50);
+ g.drawString('Period Passed!', 125, 75);
+ }
+ g.setFont('Vector', 50);
+ g.drawString(Math.ceil(min), 125, 125);
+ g.setFont('Vector', 25);
+ g.drawString('minutes', 125, 165);
+ g.drawString('remaining', 125, 195);
+}
+drawTime();
+setInterval(countSecs, 1000);
+setInterval(drawTime, 60000);
diff --git a/apps/gsat/app.png b/apps/gsat/app.png
new file mode 100644
index 000000000..cf057046b
Binary files /dev/null and b/apps/gsat/app.png differ
diff --git a/apps/gsat/metadata.json b/apps/gsat/metadata.json
new file mode 100644
index 000000000..878d213e4
--- /dev/null
+++ b/apps/gsat/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "gsat",
+ "name": "Geek Squad Appointment Timer",
+ "shortName": "gsat",
+ "version": "0.01",
+ "description": "Starts a 20 minute timer for appointments at Geek Squad.",
+ "icon": "app.png",
+ "tags": "tool",
+ "readme": "README.md",
+ "supports": ["BANGLEJS"],
+ "screenshots": [{"url":"screenshot.png"}],
+ "storage": [
+ {"name":"gsat.app.js","url":"app.js"},
+ {"name":"gsat.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/gsat/screenshot.png b/apps/gsat/screenshot.png
new file mode 100644
index 000000000..032319bf6
Binary files /dev/null and b/apps/gsat/screenshot.png differ
diff --git a/apps/hidjoystick/ChangeLog b/apps/hidjoystick/ChangeLog
index 5560f00bc..625daf4bb 100644
--- a/apps/hidjoystick/ChangeLog
+++ b/apps/hidjoystick/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Make Bangle.js 2 compatible
diff --git a/apps/hidjoystick/app.js b/apps/hidjoystick/app.js
index 134814cee..69f56463d 100644
--- a/apps/hidjoystick/app.js
+++ b/apps/hidjoystick/app.js
@@ -1,7 +1,86 @@
-var storage = require('Storage');
+const storage = require('Storage');
+const Layout = require("Layout");
const settings = storage.readJSON('setting.json',1) || { HID: false };
+const BANGLEJS2 = process.env.HWVERSION == 2;
+const sidebarWidth=18;
+const buttonWidth = (Bangle.appRect.w-sidebarWidth)/2;
+const buttonHeight = (Bangle.appRect.h-16)/2*0.85; // subtract text row and add a safety margin
var sendInProgress = false; // Only send one message at a time, do not flood
+var touchBtn2 = 0;
+var touchBtn3 = 0;
+var touchBtn4 = 0;
+var touchBtn5 = 0;
+
+function renderBtnArrows(l) {
+ const d = g.getWidth() - l.width;
+
+ function c(a) {
+ return {
+ width: 8,
+ height: a.length,
+ bpp: 1,
+ buffer: (new Uint8Array(a)).buffer
+ };
+ }
+
+ g.drawImage(c([0,8,12,14,255,14,12,8]),d,g.getHeight()/2);
+ if (!BANGLEJS2) {
+ g.drawImage(c([16,56,124,254,16,16,16,16]),d,40);
+ g.drawImage(c([16,16,16,16,254,124,56,16]),d,194);
+ }
+}
+
+const layoutChilden = [];
+if (BANGLEJS2) { // add virtual buttons in display
+ layoutChilden.push({type:"h", c:[
+ {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN2", id:"touchBtn2" },
+ {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN3", id:"touchBtn3" },
+ ]});
+}
+layoutChilden.push({type:"h", c:[
+ {type:"txt", font:"6x8:2", label:"Joystick" },
+]});
+if (BANGLEJS2) { // add virtual buttons in display
+ layoutChilden.push({type:"h", c:[
+ {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN4", id:"touchBtn4" },
+ {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN5", id:"touchBtn5" },
+ ]});
+}
+
+const layout = new Layout(
+ {type:"h", c:[
+ {type:"v", width:Bangle.appRect.w-sidebarWidth, c: layoutChilden},
+ {type:"custom", width:18, height: Bangle.appRect.h, render:renderBtnArrows }
+ ]}
+);
+
+function isInBox(box, x, y) {
+ return x >= box.x && x < box.x+box.w && y >= box.y && y < box.y+box.h;
+}
+
+if (BANGLEJS2) {
+ Bangle.on('drag', function(event) {
+ if (event.b == 0) { // release
+ touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0;
+ } else if (isInBox(layout.touchBtn2, event.x, event.y)) {
+ touchBtn2 = 1;
+ touchBtn3 = touchBtn4 = touchBtn5 = 0;
+ } else if (isInBox(layout.touchBtn3, event.x, event.y)) {
+ touchBtn3 = 1;
+ touchBtn2 = touchBtn4 = touchBtn5 = 0;
+ } else if (isInBox(layout.touchBtn4, event.x, event.y)) {
+ touchBtn4 = 1;
+ touchBtn2 = touchBtn3 = touchBtn5 = 0;
+ } else if (isInBox(layout.touchBtn5, event.x, event.y)) {
+ touchBtn5 = 1;
+ touchBtn2 = touchBtn3 = touchBtn4 = 0;
+ } else {
+ // outside any buttons, release all
+ touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0;
+ }
+ });
+}
const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
try {
@@ -20,31 +99,17 @@ const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
function drawApp() {
g.clear();
- g.setFont("6x8",2);
- g.setFontAlign(0,0);
- g.drawString("Joystick", 120, 120);
- const d = g.getWidth() - 18;
-
- function c(a) {
- return {
- width: 8,
- height: a.length,
- bpp: 1,
- buffer: (new Uint8Array(a)).buffer
- };
- }
-
- g.drawImage(c([16,56,124,254,16,16,16,16]),d,40);
- g.drawImage(c([16,16,16,16,254,124,56,16]),d,194);
- g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+ layout.render();
}
function update() {
- const btn1 = BTN1.read();
- const btn2 = BTN2.read();
- const btn3 = BTN3.read();
- const btn4 = BTN4.read();
- const btn5 = BTN5.read();
+ const btn1 = BTN1 ? BTN1.read() : 0;
+ const btn2 = !BANGLEJS2 ? BTN2.read() : touchBtn2;
+ const btn3 = !BANGLEJS2 ? BTN3.read() : touchBtn3;
+ const btn4 = !BANGLEJS2 ? BTN4.read() : touchBtn4;
+ const btn5 = !BANGLEJS2 ? BTN5.read() : touchBtn5;
const acc = Bangle.getAccel();
var x = acc.x*-127;
var y = acc.y*-127;
diff --git a/apps/hidjoystick/metadata.json b/apps/hidjoystick/metadata.json
index e2b78a97b..c13ae2efa 100644
--- a/apps/hidjoystick/metadata.json
+++ b/apps/hidjoystick/metadata.json
@@ -2,11 +2,11 @@
"id": "hidjoystick",
"name": "Bluetooth Joystick",
"shortName": "Joystick",
- "version": "0.01",
- "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.",
+ "version": "0.02",
+ "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5. On Bangle.js 2 buttons 2-5 are emulated with the touchscreen.",
"icon": "app.png",
"tags": "bluetooth",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"hidjoystick.app.js","url":"app.js"},
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog
index e559adfb6..f05a9dc13 100644
--- a/apps/hrm/ChangeLog
+++ b/apps/hrm/ChangeLog
@@ -6,3 +6,4 @@
0.06: Add widgets
0.07: Update scaling for new firmware
0.08: Don't force backlight on/watch unlocked on Bangle 2
+0.09: Grey out BPM until confidence is over 50%
diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js
index 305f0e1bc..703b60c01 100644
--- a/apps/hrm/heartrate.js
+++ b/apps/hrm/heartrate.js
@@ -35,9 +35,9 @@ function onHRM(h) {
g.clearRect(0,24,g.getWidth(),80);
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
var str = hrmInfo.bpm;
- g.setFontVector(40).drawString(str,px,45);
+ g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45);
px += g.stringWidth(str)/2;
- g.setFont("6x8");
+ g.setFont("6x8").setColor(g.theme.fg);
g.drawString("BPM",px+15,45);
}
Bangle.on('HRM', onHRM);
@@ -101,4 +101,3 @@ function readHRM() {
lastHrmPt = [hrmOffset, y];
}
}
-
diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json
index 3e94c163c..10821d094 100644
--- a/apps/hrm/metadata.json
+++ b/apps/hrm/metadata.json
@@ -1,7 +1,7 @@
{
"id": "hrm",
"name": "Heart Rate Monitor",
- "version": "0.08",
+ "version": "0.09",
"description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png",
"tags": "health",
diff --git a/apps/info/README.md b/apps/info/README.md
index 007a9794e..32920cb75 100644
--- a/apps/info/README.md
+++ b/apps/info/README.md
@@ -7,7 +7,7 @@ screen. Very useful if combined with pattern launcher ;)


-
+
## Contributors
diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog
index 7d8fecb1e..9d43f6575 100644
--- a/apps/lcars/ChangeLog
+++ b/apps/lcars/ChangeLog
@@ -15,4 +15,5 @@
0.15: Using wpedom to count steps.
0.16: Improved stability. Wind can now be shown.
0.17: Settings for mph/kph and other minor improvements.
-0.18: Fullscreen mode can now be enabled or disabled in the settings.
\ No newline at end of file
+0.18: Fullscreen mode can now be enabled or disabled in the settings.
+0.19: Alarms can not go bigger than 100.
diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js
index 7d5da2d8e..433d33427 100644
--- a/apps/lcars/lcars.app.js
+++ b/apps/lcars/lcars.app.js
@@ -626,7 +626,7 @@ Bangle.on('charging',function(charging) {
function increaseAlarm(){
- if(isAlarmEnabled()){
+ if(isAlarmEnabled() && getAlarmMinutes() < 95){
settings.alarm += 5;
} else {
settings.alarm = getCurrentTimeInMinutes() + 5;
diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json
index e6ca10f79..1335b8e1d 100644
--- a/apps/lcars/metadata.json
+++ b/apps/lcars/metadata.json
@@ -3,7 +3,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
- "version":"0.18",
+ "version":"0.19",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog
index 7a7ecd027..4210ccf03 100644
--- a/apps/lightswitch/ChangeLog
+++ b/apps/lightswitch/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
0.02: Add the option to enable touching the widget only on clock and settings.
+0.03: Settings page now uses built-in min/max/wrap (fix #1607)
diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json
index 902b1536b..9ac388eda 100644
--- a/apps/lightswitch/metadata.json
+++ b/apps/lightswitch/metadata.json
@@ -2,7 +2,7 @@
"id": "lightswitch",
"name": "Light Switch Widget",
"shortName": "Light Switch",
- "version": "0.02",
+ "version": "0.03",
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
"icon": "images/app.png",
"screenshots": [
diff --git a/apps/lightswitch/settings.js b/apps/lightswitch/settings.js
index aac159148..bebb16d15 100644
--- a/apps/lightswitch/settings.js
+++ b/apps/lightswitch/settings.js
@@ -44,9 +44,11 @@
// return entry for string value
return {
value: entry.value.indexOf(settings[key]),
+ min : 0,
+ max : entry.value.length-1,
+ wrap : true,
format: v => entry.title ? entry.title[v] : entry.value[v],
onchange: function(v) {
- this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v;
writeSetting(key, entry.value[v], entry.drawWidgets);
if (entry.exec) entry.exec(entry.value[v]);
}
@@ -57,8 +59,10 @@
value: settings[key] * entry.factor,
step: entry.step,
format: v => v > 0 ? v + entry.unit : "off",
+ min : entry.min,
+ max : entry.max,
+ wrap : true,
onchange: function(v) {
- this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v;
writeSetting(key, v / entry.factor, entry.drawWidgets);
},
};
@@ -133,16 +137,16 @@
title: "Light Switch"
},
"< Back": () => back(),
- "-- Widget --------": 0,
+ "-- Widget": 0,
"Bulb col": getEntry("colors"),
"Image": getEntry("image"),
- "-- Control -------": 0,
+ "-- Control": 0,
"Touch": getEntry("touchOn"),
"Drag Delay": getEntry("dragDelay"),
"Min Value": getEntry("minValue"),
- "-- Unlock --------": 0,
+ "-- Unlock": 0,
"TapSide": getEntry("unlockSide"),
- "-- Flash ---------": 0,
+ "-- Flash": 0,
"TapSide ": getEntry("tapSide"),
"Tap": getEntry("tapOn"),
"Timeout": getEntry("tOut"),
diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog
index 939ac3b5d..040e62671 100644
--- a/apps/mmind/ChangeLog
+++ b/apps/mmind/ChangeLog
@@ -1 +1,2 @@
0.01: First release
+0.02: Make sure to reset turns
diff --git a/apps/mmind/metadata.json b/apps/mmind/metadata.json
index c2ed474b6..ea970ee23 100644
--- a/apps/mmind/metadata.json
+++ b/apps/mmind/metadata.json
@@ -3,7 +3,7 @@
"name": "Classic Mind Game",
"shortName":"Master Mind",
"icon": "mmind.png",
- "version":"0.01",
+ "version":"0.02",
"description": "This is the classic game for masterminds",
"screenshots": [{"url":"screenshot_mmind.png"}],
"type": "app",
diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js
index e7def025d..10d315285 100644
--- a/apps/mmind/mmind.app.js
+++ b/apps/mmind/mmind.app.js
@@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) {
break;
case 4:
//new game
+ turn = 0;
play = [-1,-1,-1,-1];
game = [];
endgame=false;
@@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) {
game = [];
get_secret();
draw();
-//Bangle.loadWidgets();
-//Bangle.drawWidgets();
-
-
-
-
-
diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog
index 5560f00bc..8ccf678de 100644
--- a/apps/powermanager/ChangeLog
+++ b/apps/powermanager/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Allow forcing monotonic battery voltage/percentage
diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md
index f2cfcdf3e..434ec814e 100644
--- a/apps/powermanager/README.md
+++ b/apps/powermanager/README.md
@@ -1,6 +1,10 @@
# Power manager
-Manages settings for charging. You can set a warning threshold to be able to disconnect the charger at a given percentage. Also allows to set the battery calibration offset.
+Manages settings for charging.
+Features:
+* Warning threshold to be able to disconnect the charger at a given percentage
+* Set the battery calibration offset.
+* Force monotonic battery percentage or voltage
## Internals
diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js
index ff4ba8932..077e24413 100644
--- a/apps/powermanager/boot.js
+++ b/apps/powermanager/boot.js
@@ -26,4 +26,26 @@
Bangle.on("charging",handleCharging);
handleCharging(Bangle.isCharging());
}
+
+ if (settings.forceMonoPercentage){
+ var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4;
+ var op = E.getBattery;
+ E.getBattery = function() {
+ var current = Math.round((op()+op()+op()+op())/4);
+ if (Bangle.isCharging() && current > p) p = current;
+ if (!Bangle.isCharging() && current < p) p = current;
+ return p;
+ };
+ }
+
+ if (settings.forceMonoVoltage){
+ var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/4;
+ var ov = NRF.getBattery;
+ NRF.getBattery = function() {
+ var current = (ov()+ov()+ov()+ov())/4;
+ if (Bangle.isCharging() && current > v) v = current;
+ if (!Bangle.isCharging() && current < v) v = current;
+ return v;
+ };
+ }
})();
diff --git a/apps/powermanager/default.json b/apps/powermanager/default.json
index a6d8412b2..6c929dc38 100644
--- a/apps/powermanager/default.json
+++ b/apps/powermanager/default.json
@@ -1,4 +1,6 @@
{
"warnEnabled": false,
- "warn": 96
+ "warn": 96,
+ "forceMonoVoltage": false,
+ "forceMonoPercentage": false
}
diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json
index 3ad31ba1e..2bb531099 100644
--- a/apps/powermanager/metadata.json
+++ b/apps/powermanager/metadata.json
@@ -2,7 +2,7 @@
"id": "powermanager",
"name": "Power Manager",
"shortName": "Power Manager",
- "version": "0.01",
+ "version": "0.02",
"description": "Allow configuration of warnings and thresholds for battery charging and display.",
"icon": "app.png",
"type": "bootloader",
diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js
index c8aa057fa..8af873e5f 100644
--- a/apps/powermanager/settings.js
+++ b/apps/powermanager/settings.js
@@ -24,6 +24,20 @@
'title': 'Power Manager'
},
'< Back': back,
+ 'Monotonic percentage': {
+ value: !!settings.forceMonoPercentage,
+ format: v => settings.forceMonoPercentage ? "On" : "Off",
+ onchange: v => {
+ writeSettings("forceMonoPercentage", v);
+ }
+ },
+ 'Monotonic voltage': {
+ value: !!settings.forceMonoVoltage,
+ format: v => settings.forceMonoVoltage ? "On" : "Off",
+ onchange: v => {
+ writeSettings("forceMonoVoltage", v);
+ }
+ },
'Charge warning': function() {
E.showMenu(submenu_chargewarn);
},
diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog
index b9c26b4e3..b80dfef94 100644
--- a/apps/rebble/ChangeLog
+++ b/apps/rebble/ChangeLog
@@ -2,3 +2,4 @@
0.02: Fix typo to Purple
0.03: Added dependancy on Pedometer Widget
0.04: Fixed icon and png to 48x48 pixels
+0.05: added charging icon
\ No newline at end of file
diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json
index 212a7b5b3..b26fb6a27 100644
--- a/apps/rebble/metadata.json
+++ b/apps/rebble/metadata.json
@@ -2,7 +2,7 @@
"id": "rebble",
"name": "Rebble Clock",
"shortName": "Rebble",
- "version": "0.04",
+ "version": "0.05",
"description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion",
"readme": "README.md",
"icon": "rebble.png",
diff --git a/apps/rebble/rebble.app.js b/apps/rebble/rebble.app.js
index d186ea8ec..7c7d57939 100644
--- a/apps/rebble/rebble.app.js
+++ b/apps/rebble/rebble.app.js
@@ -204,6 +204,14 @@ function drawBattery(x,y,wi,hi) {
g.setColor(g.theme.fg);
g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact
g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level
+
+ if( Bangle.isCharging() )
+ {
+ g.setBgColor(settings.bg);
+ image = ()=> { return require("heatshrink").decompress(atob("j8OwMB/4AD94DC44DCwP//n/gH//EOgE/+AdBh/gAYMH4EAvkDAYP/+/AFAX+FgfzGAnAA=="));}
+ g.drawImage(image(),x+3,y+4);
+ }
+
}
function getSteps() {
@@ -270,3 +278,14 @@ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
loadSettings();
loadLocation();
draw(); // queues the next draw for a minutes time
+Bangle.on('charging', function(charging) {
+ //redraw the sidebar ( with the battery )
+ switch(sideBar) {
+ case 0:
+ drawSideBar1();
+ break;
+ case 1:
+ drawSideBar2();
+ break;
+ }
+});
\ No newline at end of file
diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog
index 963944144..e9877808c 100644
--- a/apps/recorder/ChangeLog
+++ b/apps/recorder/ChangeLog
@@ -16,3 +16,5 @@
0.10: Fix broken recorder settings (when launched from settings app)
0.11: Fix KML and GPX export when there is no GPS data
0.12: Fix 'Back' label positioning on track/graph display, make translateable
+0.13: Fix for when widget is used before app
+0.14: Remove unneeded variable assignment
\ No newline at end of file
diff --git a/apps/recorder/app-settings.json b/apps/recorder/app-settings.json
index 4a3117a17..7410af213 100644
--- a/apps/recorder/app-settings.json
+++ b/apps/recorder/app-settings.json
@@ -1,6 +1,6 @@
{
"recording":false,
- "file":"record.log0.csv",
+ "file":"recorder.log0.csv",
"period":10,
"record" : ["gps"]
}
diff --git a/apps/recorder/app.js b/apps/recorder/app.js
index d900c12c1..99252e0e2 100644
--- a/apps/recorder/app.js
+++ b/apps/recorder/app.js
@@ -31,7 +31,12 @@ function updateSettings() {
}
function getTrackNumber(filename) {
- return parseInt(filename.match(/^recorder\.log(.*)\.csv$/)[1]||0);
+ var trackNum = 0;
+ var matches = filename.match(/^recorder\.log(.*)\.csv$/);
+ if (matches) {
+ trackNum = parseInt(matches[1]||0);
+ }
+ return trackNum;
}
function showMainMenu() {
@@ -214,7 +219,7 @@ function viewTrack(filename, info) {
f.erase();
viewTracks();
} else
- viewTrack(n, info);
+ viewTrack(filename, info);
});
};
menu['< Back'] = () => { viewTracks(); };
diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json
index 09873dada..d715af38d 100644
--- a/apps/recorder/metadata.json
+++ b/apps/recorder/metadata.json
@@ -2,7 +2,7 @@
"id": "recorder",
"name": "Recorder",
"shortName": "Recorder",
- "version": "0.12",
+ "version": "0.14",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png",
"tags": "tool,outdoors,gps,widget",
@@ -15,5 +15,5 @@
{"name":"recorder.wid.js","url":"widget.js"},
{"name":"recorder.settings.js","url":"settings.js"}
],
- "data": [{"name":"recorder.json"},{"wildcard":"recorder.log?.csv","storageFile":true}]
+ "data": [{"name":"recorder.json","url":"app-settings.json"},{"wildcard":"recorder.log?.csv","storageFile":true}]
}
diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js
index e10c99c0c..4a105754b 100644
--- a/apps/recorder/widget.js
+++ b/apps/recorder/widget.js
@@ -11,7 +11,7 @@
settings.recording = false;
return settings;
}
-
+
function updateSettings(settings) {
require("Storage").writeJSON("recorder.json", settings);
if (WIDGETS["recorder"]) WIDGETS["recorder"].reload();
@@ -233,7 +233,9 @@
Bangle.drawWidgets(); // relayout all widgets
},setRecording:function(isOn) {
var settings = loadSettings();
- if (isOn && !settings.recording && require("Storage").list(settings.file).length){
+ if (isOn && !settings.recording && !settings.file) {
+ settings.file = "recorder.log0.csv";
+ } else if (isOn && !settings.recording && require("Storage").list(settings.file).length){
var logfiles=require("Storage").list(/recorder.log.*/);
var maxNumber=0;
for (var c of logfiles){
@@ -246,16 +248,17 @@
}
var buttons={Yes:"yes",No:"no"};
if (newFileName) buttons["New"] = "new";
- var prompt = E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{
- if (selection=="no") return false; // just cancel
- if (selection=="yes") require("Storage").open(settings.file,"r").erase();
- if (selection=="new"){
+ return E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{
+ if (selection==="no") return false; // just cancel
+ if (selection==="yes") {
+ require("Storage").open(settings.file,"r").erase();
+ }
+ if (selection==="new"){
settings.file = newFileName;
updateSettings(settings);
}
return WIDGETS["recorder"].setRecording(1);
});
- return prompt;
}
settings.recording = isOn;
updateSettings(settings);
diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog
index 0a697ecb9..46fdb7e7e 100644
--- a/apps/run/ChangeLog
+++ b/apps/run/ChangeLog
@@ -6,4 +6,7 @@
0.05: exstats updated so update 'distance' label is updated, option for 'speed'
0.06: Add option to record a run using the recorder app automatically
0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
-0.08: Added support for notifications from exstats. Support all stats from exstats
\ No newline at end of file
+0.08: Added support for notifications from exstats. Support all stats from exstats
+0.09: Fix broken start/stop if recording not enabled (fix #1561)
+0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578)
+0.11: Notifications fixes
\ No newline at end of file
diff --git a/apps/run/app.js b/apps/run/app.js
index 45daf878e..fb8158e58 100644
--- a/apps/run/app.js
+++ b/apps/run/app.js
@@ -59,13 +59,16 @@ function onStartStop() {
layout.render();
})
);
- } else {
+ } else if (!settings.record && WIDGETS["recorder"]) {
prepPromises.push(
WIDGETS["recorder"].setRecording(false)
);
}
}
+ if (!prepPromises.length) // fix for Promise.all bug in 2v12
+ prepPromises.push(Promise.resolve());
+
Promise.all(prepPromises)
.then(() => {
if (running) {
@@ -121,7 +124,7 @@ function configureNotification(stat) {
}
Object.keys(settings.notify).forEach((statType) => {
- if (settings.notify[statType].increment > 0) {
+ if (settings.notify[statType].increment > 0 && exs.stats[statType]) {
configureNotification(exs.stats[statType]);
}
});
diff --git a/apps/run/metadata.json b/apps/run/metadata.json
index 8f139c2d5..09e5a3bed 100644
--- a/apps/run/metadata.json
+++ b/apps/run/metadata.json
@@ -1,6 +1,6 @@
{ "id": "run",
"name": "Run",
- "version":"0.08",
+ "version":"0.11",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",
diff --git a/apps/run/settings.js b/apps/run/settings.js
index 29a2f43cc..6a7d169c4 100644
--- a/apps/run/settings.js
+++ b/apps/run/settings.js
@@ -42,6 +42,11 @@
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
format: v => statsList[v].name,
onchange: v => {
+ for (var i=1;i<=6;i++)
+ if (settings["B"+i]==statsIDs[v]) {
+ settings["B"+i]="";
+ boxMenu["Box "+i].value=0;
+ }
settings[boxID] = statsIDs[v];
saveSettings();
},
@@ -60,7 +65,7 @@
'': { 'title': 'Run' },
'< Back': back,
};
- if (WIDGETS["recorder"])
+ if (global.WIDGETS&&WIDGETS["recorder"])
menu[/*LANG*/"Record Run"] = {
value : !!settings.record,
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
@@ -85,9 +90,9 @@
[[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]],
];
notificationsMenu[/*LANG*/"Dist Pattern"] = {
- value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
- min: 0, max: vibPatterns.length,
- format: v => vibPatterns[v]||"Off",
+ value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
+ min: 0, max: vibTimes.length,
+ format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.dist.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
@@ -95,9 +100,9 @@
}
}
notificationsMenu[/*LANG*/"Step Pattern"] = {
- value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
- min: 0, max: vibPatterns.length,
- format: v => vibPatterns[v]||"Off",
+ value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
+ min: 0, max: vibTimes.length,
+ format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.step.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
@@ -105,9 +110,9 @@
}
}
notificationsMenu[/*LANG*/"Time Pattern"] = {
- value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
- min: 0, max: vibPatterns.length,
- format: v => vibPatterns[v]||"Off",
+ value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
+ min: 0, max: vibTimes.length,
+ format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.time.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
diff --git a/apps/s7clk/icon.js b/apps/s7clk/icon.js
index d5d9aaf68..dbb4fc6d3 100644
--- a/apps/s7clk/icon.js
+++ b/apps/s7clk/icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUygP/AC5BlH4MAn/gAwN/4EP/AFBsEMhkBwEAjEDgYJBgEGgHA4EYDwOAmEwBIIYyj/wgf+AoMH/kA/4eBJXwYLVxgAjh//AC3w"))
+require("heatshrink").decompress(atob("mEqgInkn/gg/8Ao/AjEYgYF/AoZT/Kb4AiA="))
diff --git a/apps/s7clk/icon.png b/apps/s7clk/icon.png
index cb08aec5e..cfb1c0349 100644
Binary files a/apps/s7clk/icon.png and b/apps/s7clk/icon.png differ
diff --git a/apps/seiko-5actus/ChangeLog b/apps/seiko-5actus/ChangeLog
new file mode 100644
index 000000000..978e5d6ea
--- /dev/null
+++ b/apps/seiko-5actus/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Initial Release
+0.02: Shrink hand images to save memory
diff --git a/apps/seiko-5actus/README.md b/apps/seiko-5actus/README.md
new file mode 100644
index 000000000..4f09bf3c6
--- /dev/null
+++ b/apps/seiko-5actus/README.md
@@ -0,0 +1,16 @@
+# Seiko 5actus
+
+
+
+This is built on the knowledge of what I gained through designing the rolex watch face and improves on it by getting more done right at the start.
+
+This watch is modeled after one I personally own and love, I have spent quite a bit of time designing this in a pixel art editor to try and make it as clean as possible and am quite happy with how it came out.
+
+This watch face works in both the light and dark themes but I personally think it looks a lot cleaner in the dark them.
+
+This watch whilst technically designed in a way that would work with the BangleJs has been only listed to work with the BangleJs2, if someones wants to test it on a first gen and let me know if it works then i'll allow it to be installed on both devices but I assume with how the images have been designed it would look strange on a first gen watch.
+
+Special thanks to:
+* rozek (for his updated widget draw code for utilization with background images)
+* Gordon Williams (Bangle.js, watchapps for reference code and documentation)
+* The community (for helping drive such a wonderful project)
diff --git a/apps/seiko-5actus/app-icon.js b/apps/seiko-5actus/app-icon.js
new file mode 100644
index 000000000..796f24122
--- /dev/null
+++ b/apps/seiko-5actus/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEBpMB+fziAjRCQQXBHyoXEgIRLgMwC5EAj8gC5MC+QXJn4XKBgJHJMhkfJAYXEh/xC5cDBofzJocvIxKiCHpBGNExMCIxi2KeRIJFgMiiYBCkQ1Jh67EAAMSCgICBiQjFn8xDYX/AgQANn4qEgf/JIcDkcxiUSiMRY4cv+ZaFj6bDgZGBkMRDIIXD/7CHn5TDFYIADFIcRSxgAvXQwAQgRyDACcje4wAESQ4RDmMQgSGBj8zAAnyTgauH/65Cj7EBkMicAPyBYIABCgcRkYWCmYvCewMhmUiiMyF4gUBDoXzn7/Dj4RBF4IXB+QrDj40DmJgBiEyBYMDL4qcEgUikYqDCgSGKAAUSn8hbh8BgICBl/yFggwEbhMC/4sHAAcTIhIsBGYLcJGAQOHgLAEGBM/Jg0vRgsQZAMQBAQUBif/AwLJDfoQ1DkDOBkIOCgQDBFAKCDaIRPGSwqGBgM/dwUDeQiZEEAowCgXxb5jGGgQ+GaAjaHeIbmISYToHRYTPKYQIODE4cfEA4AEPghtEgQJCI5Mv+AXHJAiIIBggXFj/xDBMCcAYXGga7LcYIXIUpY1GC4SiJVRAXCiAWJA"))
diff --git a/apps/seiko-5actus/app.js b/apps/seiko-5actus/app.js
new file mode 100644
index 000000000..078b6e5c2
--- /dev/null
+++ b/apps/seiko-5actus/app.js
@@ -0,0 +1,181 @@
+var imgBg = {
+ width : 176, height : 176, bpp : 2,
+ transparent : 1,
+ buffer : require("heatshrink").decompress(atob("qoASv/8//1C6YASrorD6opjuorHBAIriQYwriV9FXFZldFjrSEFY6FjQcorPSYqtZFZaxaVoiDkZRArLv4rV/orT/5rGABtf/4rSq//+79UFaptIK5puHFZQUBFda5IABZuBCw4rKTAPVq4rUNw4rK/4rBroqRQAIrVQSd1N4QrQNZIAOFdbzBQ4IrOCQIIDWKQYFFZhqBHwaeBACBwBFazZQThQrJ/7CHABaSEFaTaWOIYrONJArTToorIdpDdRDQLJOCBDhRIxBoPABdXAwwrRVKQ+GZR7aZDYRHNM4IraOZyTQZbQrKaIwAKr7LOHRNdFaJzOr56RuppZACBgRAEtW1Wl1WqzWm1Nqy2qC5hyTrWq1IrFzWqyoXLv7mFQRlqqtp0tVy2V0uptNVA4JWK/72Fr4yFFY9VLINWyqJBFZtfFYwGGQZIrS////peF/6xM1KDDQQIrMKwJQFA4SEKQYLbGAoIrKv5PGGY4rGEYIAHyrZKQQoIDQhoARKwR6GBJIAXJpKECMIoAXUpaELBYQAICZR4CPYo3Kq4IBr7RITIzZFq7mOGwXXBYIjB//3/9drt/+5AGZ4RKHuoNEFZVdupQBuorBupjCJIyNIrqELr4mBr99vorEK476PBxAYC79//orCr4QBu5XIXAwiIcwxXCKYJlBFYSvIQRI7HTiLjBFYzZHAAzdBDA4AKDY6CMQgYQOABT8CEQjdJFTAAKuo8MADokkAH4A/AH4A/ABn1FldXFlfVTX4A/AH4A/AH4APr//ABVVrorcv4rL6tXFbgqL//1Qbv/1QAE1AED14re/wrK1Yr/Ff4rEcY3VFf4r/Ff4r/EaoAH/4rK14r/FZYALFb1/FZbTWAA9fFZYNBFjoA/AH4A/AH4A/vots+pu/AH4A/AH4A/ADdX6ousr4GFrohgABP/FbN/+o7O/6NZv5HOB4IrZ///LBlXB5wrO/qCNB5pzOOhgNBK7RICDhQNCX5P96td/91u4FBvpJLAoQPDrplEQRNdFYPVu93qvVO5JYJurZDSJV1FYP9FYP16oXBfJRKIbJpQB7vVv/3AwJvCbpTZVVIP9/9d6/9AALdTbJgAVEJDZMACoiCLAjZNAAqSKbpjZNSoo7PQg4zCIx9/FaBQBJ4rZRHqJQBFYzZRLCN/HooYRCQIRQYQ1dDCFVQR4A/ADFXCSK5RDJ40Iq6nPW6LcIq//fh39SrNf/6EN/47OFZp0NFbd/K5wPPI5obNM5F1FaYPNdYLcGfpA3IDQIrXABZ6FDSLcZTxAIBW4zcBFa4ZBFaLsNFZYZGFZBpIACCdBFZ7BFq7dSZJArOfQwAHHQYYBFaA+JABwhBIggrObirIJFZLuBbioXJFZI/JWJorBEJIJJS4KFRrqbKFZLvDupYSeZIrJbiwrBNwIrnTQYrReBIrNCpArKBRQAKIJQrLNhCvNJidXQSZYCPCiCTIQS6KEI4pVAAddFaF1FbAZHfioAVFYyUJWbRXHLrqxFFYhVeLI4rq6orCMAoAhFa4"))
+};
+
+/* Set hour hand image */
+
+var imgHour = {
+ width : 14, height : 114, bpp : 2,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AH4A/AB8P/4DB//wAz8D//8BIIKBn4DB54CBACPzAQP8EoImBD4PAJkQG/A34GIgbUBA"))
+};
+
+/* Set minute hand image */
+
+var imgMin = {
+ width : 4, height : 168, bpp : 2,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AH4AE/4A/AEI"))
+};
+
+/* Set second hand image */
+
+var imgSec = {
+ width : 6, height : 176, bpp : 2,
+ transparent : 1,
+ buffer : require("heatshrink").decompress(atob("qoA/ADFf6v9AU1c6vNFlICWvtXAXlVA="))
+};
+
+/* Sets the font for the date at an appropriate size of 14 */
+
+Graphics.prototype.setFontWorkSans = function(scale) {
+ // Actual height 12 (12 - 1)
+ this.setFontCustom(atob("AAAAAAADwAAAPAAAAAAAAAvAAC/0AH/gAL/QAD9AAAIAAAAACQAAL/9AC/r9APAA8A8ADwDwAfAH//wAH/8AAAAAADwAAAtAAAH//8Af//wAAAAAAAAAABwAUAfgHwDwA/APAP8A8DzwD/9PAD/Q8AAABQAAA0AHwDwA9AHwDw4PAPDw8A///gA/f8AAAAAAAAQAAAvAAAP8AALzwAD8PAAv//wB///AAAPAAAAUAAAAAAC/88AP/y8A8NDwDywPAPD18A8L/AAAGgAAAAAAC/8AA//9ALTx8A8ODwDw4PALz/8ALD/AAAAAAPAAAA8AAADwB/APC/8A9/gAD/AAAPgAAAAAAAAAAAAB8fwAf//wDw8PAPDw8A8PDwB///AC8vwAAAAAAAAAAD/DgAv/PQDwsPAPC08A8PDwB//8AB//QAAAAAAAAAAA8DwADwPAABAE"), 46, atob("BAYJBggICQgJCAkJBA=="), 14+(scale<<8)+(2<<16));
+ return this;
+};
+
+/* Sets the font for the day at an appropriate size of 12 */
+
+Graphics.prototype.setFontWorkSansSmall = function(scale) {
+ // Actual height 12 (11 - 0)
+ this.setFontCustom(atob("AAAAAAAAAAAAAAAAAABAP/zwGqjQAAAAP0AAEAAAP4AAGAAAAEoAAs/gL//QL88AAt/gL/+QK88AAYAAAUFAD+PAHPDgv//8fr70Hj7QCx+AAAAAC8AAL/AAPHBgL/PQB68AAPgAC9/APT7wEDjwAC/QAAAAAAuAC7/gL/DwPPywL9/gCwvAAD7wAABgAAAAP0AAEAAAACkAC//wP0C8sAAPYAAGPQA+D//0Af9ABQAADzAAB/AAv8AAB/AADyAABAAAACAAADgAADgAC//AAr5AADgAADQAAABAAAD9AAD0AAAAACwAACwAACwAACwAAAgAAABAAADwAADQAAAkAAv0Af9AL+AAvAAAAKgAD//ALgLgPADwPADwD//QA/9AAAAAAwAADwAAL//gL//gAAAAAgBQD4DwPAPwPA/wLnzwD/TwAUBQAAFADwPQLADwPDDwPvjwD+/AAQYAAAoAAD8AAv8AD08AP//gL//gAA8AAAAAL/PAPrDwPPDwPPDwPH/AAAoAAKgAC/+AHzrgPPDwPPDwH3/gBS+AKAAAPAAAPAbgPL/gP/QAPwAALAAAAAYAD+/ALvjwPLDwPLDwH//gBk+AAYAAD/PALTzwPDzwPDjwD//AAv8AAQBAA8DwA4DgAAAAAEBAAPD9AOD4AAAAABQAADwAAP4AANsAA8OAA4PABwHAAIUAAM8AAM8AAM8AAM8AAM8AAMoABgCAA4LAA8OAAdsAAP8AAHwAADgABgAADwAAPCzgPDzwLvBAD9AAAQAAABoAAv/wC0A8DD/OLPX3OMDjONHDHP/7Dfi5B4HQAP9AAACgAC/gB/8AP48AP48AB/8AAC/gAACgAAAAL//gP//wPDDwPDDwLv3gD+/AAAYAAGQAB/+AH0fQPADwPADwPADwDwPQBgNAAAAAL//gP//wPADwPADwHQDgD//AA/8AAAAAAAAAL//gP//wPDDwPDDwPDDwPADwAAAAAAAAL//gP//gPDAAPDAAPDAAPAAAAGQAB/+AD0fQPADwPCjwPDzwHz/QBy/gAAAAAAAAL//gL//gADAAADAAADAAL//gL//gAAAAAAAAL//gL//gAAAAAAeAAAvgAADwAADwL//gP//AAAAAAAAAL//gL//gAPAAA/0ADx/APAPwIABgAAAAL//gL//wAADwAADwAADwAACgAAAAL//gP6qQC/gAAH/QAAvwAv9AL9AAP//wGqqQAAAAL//gL6qQC+AAAP0AAB/AL//wL//gAAAAAGQAB/+AH0fQPADwPADwPADwD6vQB/9AAGQAAAAAL//gP//gPDwAPDwAH7gAD/AAAAAAAGQAB/+AH0fQPADwPAD9PAD/D6vbB/9PAGQAAAAAL//gP//gPDgAPD0ALr/AD/LwAUAgAUFAD+PALvDwPHDwPDjwLT7gDx/AAAAALAAAPAAAPAAAP//wPqqQPAAAPAAAAAAAP/+AGqvgAADwAADwAADwL//AL/4AAAAAKAAAL+AAAv9AAAvwAB/gAv8AL9AAKAAAKQAAL/QAAf/gAAPwAv/QL+AAL/gAAL/gAAvwC/+AP9AAEAAAEABgPgLwD9+AAfwAA/8AL0fgPADwOAAAL0AAB/AAAH/wA/qQL4AAPAAAFACgPAPwPA/wPHzwPvDwP4DwLQDwAAAAAAAAv///uqqvsAAPdAAAf4AAB/0AAC/wAAC4cAAKsAAPv///Kqqp"), 32, atob("BAMFCAgLCAMEBAcHAwYDBQgFBwcHBwcHBwcEBAcHBwcLCAgICQgHCQkEBwgHCgkJCAkICAcJCAwHBwgEBQQ="), 12+(scale<<8)+(2<<16));
+ return this;
+};
+
+/* Set variables to get screen width, height and center points */
+
+let W = g.getWidth();
+let H = g.getHeight();
+let cx = W/2;
+let cy = H/2;
+let Timeout;
+
+Bangle.loadWidgets();
+
+/* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */
+
+Bangle.drawWidgets = function () {
+ var w = g.getWidth(), h = g.getHeight();
+
+ var pos = {
+ tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left
+ tr:{x:w-1, y:0, r:1, c:0},
+ bl:{x:0, y:h-24, r:0, c:0},
+ br:{x:w-1, y:h-24, r:1, c:0}
+ };
+
+ if (global.WIDGETS) {
+ for (var wd of WIDGETS) {
+ var p = pos[wd.area];
+ if (!p) continue;
+
+ wd.x = p.x - p.r*wd.width;
+ wd.y = p.y;
+
+ p.x += wd.width*(1-2*p.r);
+ p.c++;
+ }
+
+ g.reset(); // also loads the current theme
+
+ try {
+ for (var wd of WIDGETS) {
+ g.setClipRect(wd.x,wd.y, wd.x+wd.width-1,23);
+ wd.draw(wd);
+ }
+ } catch (e) { print(e); }
+
+ g.reset(); // clears the clipping rectangle!
+ }
+ };
+
+/* Draws the clock hands and date */
+
+function drawHands() {
+ let d = new Date();
+
+ let hour = d.getHours() % 12;
+ let min = d.getMinutes();
+ let sec = d.getSeconds();
+
+ let twoPi = 2*Math.PI;
+ let Pi = Math.PI;
+
+ let hourAngle = (hour+(min/60))/12 * twoPi - Pi;
+ let minAngle = (min/60) * twoPi - Pi;
+ let secAngle = (sec/60) * twoPi - Pi;
+
+ g.setFontWorkSans();
+ g.setColor(g.theme.bg);
+ g.setFontAlign(0,0,0);
+ g.drawString(d.getDate(),162,90);
+ g.setFontWorkSansSmall();
+ let weekDay = d.toString().split(" ");
+ if (weekDay[0] == "Sat"){g.setColor(0,0,1);}
+ else if (weekDay[0] == "Sun"){g.setColor(1,0,0);}
+ else {g.setColor(g.theme.bg);}
+ g.drawString(weekDay[0].toUpperCase(), 137, 90);
+
+ handLayers = [
+ {x:cx,
+ y:cy,
+ image:imgHour,
+ rotate:hourAngle,
+ center:true
+ },
+ {x:cx,
+ y:cy,
+ image:imgMin,
+ rotate:minAngle,
+ center:true
+ },
+ {x:cx,
+ y:cy,
+ image:imgSec,
+ rotate:secAngle,
+ center:true
+ }];
+
+ g.setColor(g.theme.fg);
+ g.drawImages(handLayers);
+}
+
+function drawBackground() {
+ g.clear(1);
+ g.setBgColor(g.theme.bg);
+ g.setColor(g.theme.fg);
+ bgLayer = [
+ {x:cx,
+ y:cy,
+ image:imgBg,
+ center:true
+ }];
+ g.drawImages(bgLayer);
+ g.reset();
+}
+
+/* Refresh the display every second */
+
+function displayRefresh() {
+ g.clear(true);
+ drawBackground();
+ drawHands();
+ Bangle.drawWidgets();
+
+ let Pause = 1000 - (Date.now() % 1000);
+ Timeout = setTimeout(displayRefresh,Pause);
+}
+
+Bangle.on('lcdPower', (on) => {
+ if (on) {
+ if (Timeout != null) { clearTimeout(Timeout); Timeout = undefined;}
+ displayRefresh();
+ }
+});
+
+Bangle.setUI("clock");
+// load widgets after 'setUI' so they're aware there is a clock active
+Bangle.loadWidgets();
+displayRefresh();
diff --git a/apps/seiko-5actus/metadata.json b/apps/seiko-5actus/metadata.json
new file mode 100644
index 000000000..33de8213b
--- /dev/null
+++ b/apps/seiko-5actus/metadata.json
@@ -0,0 +1,17 @@
+{ "id": "seiko-5actus",
+ "name": "Seiko 5actus",
+ "shortName":"5actus",
+ "icon": "seiko-5actus.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "version":"0.02",
+ "description": "A watch designed after then Seiko 5actus from the 1970's",
+ "tags": "clock",
+ "type": "clock",
+ "supports":["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"seiko-5actus.app.js","url":"app.js"},
+ {"name":"seiko-5actus.img","url":"app-icon.js","evaluate":true}
+ ]
+ }
diff --git a/apps/seiko-5actus/screenshot.png b/apps/seiko-5actus/screenshot.png
new file mode 100644
index 000000000..fb8638999
Binary files /dev/null and b/apps/seiko-5actus/screenshot.png differ
diff --git a/apps/seiko-5actus/seiko-5actus.png b/apps/seiko-5actus/seiko-5actus.png
new file mode 100644
index 000000000..73f1b8164
Binary files /dev/null and b/apps/seiko-5actus/seiko-5actus.png differ
diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js
index 39f9b59db..e963f2c40 100644
--- a/apps/sleepphasealarm/app.js
+++ b/apps/sleepphasealarm/app.js
@@ -29,11 +29,11 @@ function calc_ess(val) {
if (nonmot) {
slsnds+=1;
if (slsnds >= sleepthresh) {
- return true; // awake
+ return true; // sleep
}
} else {
slsnds=0;
- return false; // sleep
+ return false; // awake
}
}
}
diff --git a/apps/slomoclock/app-icon.js b/apps/slomoclock/app-icon.js
index 22e264124..46f668745 100644
--- a/apps/slomoclock/app-icon.js
+++ b/apps/slomoclock/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("oFAwhC/ABOIABgfymYAKD+Z/9hGDL5c4wAf/XzjASTxqgQhAfPMB2IPxiACIBo+BDxqACIBg+CLxpANHwQPBABgvCIBT8CJ5owDD5iPOOAQfLBojiDCYQGFGIQfICIQfdBYJNMOI6SHD8jeNOIYzID8hfRD9LfEAoTdFBIifLAAIffBoQRBAJpxMD84JCD+S/GL56fID8ALBb6ZhID8qtJCZ4fgT4YDBABq/PD7RNEL6IRKD8WID5pfCD5kzNhKSFmYfMBwSeOGBoPDABgvCJ5wAON5pADABivPIAIAOd5xABABweOD4J+OD58IQBj8LD/6gUDyAfhXzgfiP/wA2"))
+require("heatshrink").decompress(atob("mEw4UA///7k8//GnldDZ9RosUqNABQsFqoACqALFg2qAAWQBaMVEYdUBYseC4e0BYsaBYekBYt6BYetBYouDAAIKEgPqC4erNgkFBQYABNgke2oiDrxIEvXUBYcXHgl7FIkB9oEDBYKYBTwILEi4LCoEBBYUQHQX7EYyRCBYJrF95ICBYNFBQdRBYcWEYwLDit7otUHQMVqIvDL4L5BgL8CI4YLDqILDO4gXGBQUEEZQ7CEYprEI4prFoLGBqkFoILFNZaPFF4ZHCR4hrFa5ILMfYJeDfYse2ovDrxGCAAMF1QAEMgIpD9QKD1Y1EgBfFBQg8BC4Y6EAAMaBYekBYseBYZGESIQuDfIYACgwXDyALRgojDNQhsCBYZqFABI="))
diff --git a/apps/smclock/ChangeLog b/apps/smclock/ChangeLog
index 0300d5ceb..2a3874d34 100644
--- a/apps/smclock/ChangeLog
+++ b/apps/smclock/ChangeLog
@@ -1,4 +1,6 @@
0.01: Initial version
0.02: Add battery level
-0.03: Fix battery display when full
+0.03: Fix battery display when full (incorporating code by Ronin0000)
0.04: Add support for settings
+0.05: Add ability to change background (3bit or 4bit)
+0.06: Replace battery text with image
diff --git a/apps/smclock/README.md b/apps/smclock/README.md
index 635292d0c..2fc239ab2 100644
--- a/apps/smclock/README.md
+++ b/apps/smclock/README.md
@@ -4,13 +4,16 @@ Just a simple watch face for the Banglejs2.
It shows battery level in the upper left corner, date information in the upper right, and time information in the bottom.
-
+
+
## Settings
-**Analog Clock:**
+**Analog Clock:** *Not yet implemented.*
-**Human Readable Date:** When the setting is on, the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off.
+**Background:** When the setting is set as "3bit", a background with more accurate colors is chosen for the watchface. Otherwise, it uses a background following the 16-bit Mac Color Palette.
+
+**Date Format:** When the setting is set as "Long", the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off.
**Show Week Info:** When the setting is on, the weekday and week number are shown in the upper right box. When the setting is off, the full year is shown instead. Default is off.
@@ -20,4 +23,4 @@ It shows battery level in the upper left corner, date information in the upper r
Monogram Watch Face can be selected as the default clock or it can be run manually from the launcher. Its settings can be accessed and changed via the relevant menu.
-Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert.
+*Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert.* - *Feature not implemented yet.*
diff --git a/apps/smclock/app.js b/apps/smclock/app.js
index 350c0dd07..41bc2b5e4 100644
--- a/apps/smclock/app.js
+++ b/apps/smclock/app.js
@@ -1,23 +1,23 @@
const SETTINGSFILE = "smclock.json";
-const background = {
- width: 176,
- height: 176,
- bpp: 3,
- transparent: 1,
- buffer: require("heatshrink").decompress(
- atob(
- "/4A/AH4ACUb8H9MkyVJAThB/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INP/AH4A/AAX8Yz4Afn5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INI="
- )
- ),
+const image3bit = {
+ width : 176, height : 176, bpp : 3,
+ transparent : 1,
+ buffer : require("heatshrink").decompress(atob("/4A/AH4AC23btoCct/pkmSpICcIP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5Bp/4A/AH4AC/kAAH0/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5BpA="))
+};
+const image4bit = {
+ width : 176, height : 176, bpp : 4,
+ transparent : 1,
+ buffer : require("heatshrink").decompress(atob("/4A/AH4Au1QAp1/2swApK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K+//AH4A/AF8AAH4AUK/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/AA=="))
};
const monthName = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
// dynamic variables
var batLevel = -1;
-var batColor = [0, 0, 0];
+var batColor = "";
// settings variables
+var backgroundImage;
var dateFormat;
var drawInterval;
var pollInterval;
@@ -31,6 +31,7 @@ function loadSettings() {
function def(value, def) {return value !== undefined ? value : def;}
var settings = require("Storage").readJSON(SETTINGSFILE, true) || {};
+ backgroundImage = def(settings.backgroundImage, "3bit");
dateFormat = def(settings.dateFormat, "Short");
drawInterval = def(settings.drawInterval, 10);
pollInterval = def(settings.pollInterval, 60);
@@ -67,23 +68,29 @@ function getBatteryColor(level) {
level = batLevel;
}
if (level > 80) {
- color = [0, 0, 1];
+ color = "#00f";
} else if (level > 60) {
- color = [0, 1, 1];
+ color = "#0ff";
} else if (level > 40) {
- color = [0, 1, 0];
+ color = "#0f0";
} else if (level > 20) {
- color = [1, 1, 0];
+ color = "#f40";
} else {
- color = [1, 0, 0];
+ color = "f00";
}
return color;
}
function draw() {
+ var background;
+ if (backgroundImage == "3bit") {
+ background = image3bit;
+ } else {
+ background = image4bit;
+ }
g.drawImage(background);
- const color = getBatteryColor(batLevel);
+ batColor = getBatteryColor(batLevel);
var bat = "";
const d = new Date();
const day = d.getDate();
@@ -95,32 +102,38 @@ function draw() {
const m = d.getMinutes();
const time = d02(h) + ":" + d02(m);
- if (E.getBattery() < 100) {
- bat = d02(E.getBattery()) + "%";
- } else {
- bat = E.getBattery() + "%";
- }
-
g.reset();
// draw battery info
- g.setColor(1, 1, 1);
+ var x = 12;
+ var y = 16;
+ if (Bangle.isCharging()) {
+ g.setColor("#ff0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
+ } else {
+ g.clearRect(x,y,x+14,y+24);
+ g.setColor("#000").fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2);
+ g.setColor(batColor).fillRect(x+4,y+20-(batLevel*16/100),x+10,y+20);
+ }
+ if (Bangle.isCharging()) {
+ g.setColor("#ff0");
+ } else {
+ g.setColor(batColor);
+ }
if (useVectorFont == true) {
g.setFont("Vector", 16);
- g.drawString("Bat:", 12, 22, false);
} else {
- g.setFont("4x6", 2);
- g.drawString("Bat:", 10, 22, false);
+ g.setFont("4x6", 3);
}
- g.setColor(color[0], color[1], color[2]);
if (batLevel < 100) {
- g.drawString(bat, 52, 22, false);
+ bat = d02(batLevel) + "%";
+ g.drawString(bat, 50, 22, false);
} else {
- g.drawString(bat, 46, 22, false);
+ bat = "100%";
+ g.drawString(bat, 40, 22, false);
}
// draw date info
- g.setColor(0, 0, 0);
+ g.setColor("#000");
if (useVectorFont == true) {
g.setFont("Vector", 20);
} else {
@@ -136,7 +149,7 @@ function draw() {
// draw week info
if (showWeekInfo == true) {
- date2 = weekday[d.getDay()] + " " + d02(week)
+ date2 = weekday[d.getDay()] + " " + d02(week);
if (useVectorFont == true) {
g.setFont("Vector", 18);
} else {
@@ -155,7 +168,7 @@ function draw() {
}
// draw time
- g.setColor(1, 1, 1);
+ g.setColor("#fff");
if (useVectorFont == true) {
g.setFont("Vector", 60);
g.drawString(time, 10, 108, false);
diff --git a/apps/smclock/metadata.json b/apps/smclock/metadata.json
index cc995d587..55668adcc 100644
--- a/apps/smclock/metadata.json
+++ b/apps/smclock/metadata.json
@@ -3,13 +3,13 @@
"name": "Monogram Watch Face",
"shortName": "MonoClock",
"icon": "app.png",
- "screenshots": [{ "url": "screenshot.png" }],
+ "screenshots": [{ "url": "screenshot0.png" }, {"url": "screenshot1.png" }],
"version": "0.04",
"description": "A simple watchface based on my stylised monogram.",
"type": "clock",
"tags": "clock",
"readme": "README.md",
- "supports": ["BANGLEJS", "BANGLEJS2"],
+ "supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{ "name": "smclock.app.js", "url": "app.js" },
diff --git a/apps/smclock/screenshot.png b/apps/smclock/screenshot.png
deleted file mode 100644
index c0e0bd0ee..000000000
Binary files a/apps/smclock/screenshot.png and /dev/null differ
diff --git a/apps/smclock/screenshot0.png b/apps/smclock/screenshot0.png
new file mode 100644
index 000000000..07eff8ddf
Binary files /dev/null and b/apps/smclock/screenshot0.png differ
diff --git a/apps/smclock/screenshot1.png b/apps/smclock/screenshot1.png
new file mode 100644
index 000000000..da25b2579
Binary files /dev/null and b/apps/smclock/screenshot1.png differ
diff --git a/apps/smclock/settings.js b/apps/smclock/settings.js
index a6c7d1b98..ee4a35a26 100644
--- a/apps/smclock/settings.js
+++ b/apps/smclock/settings.js
@@ -52,6 +52,7 @@
writeSettings();
},
},
+ "Background": stringInSettings("backgroundImage", ["3bit", "4bit"]),
Date: stringInSettings("dateFormat", ["Long", "Short"]),
"Draw Interval": {
value: settings.drawInterval,
diff --git a/apps/speedalt/ChangeLog b/apps/speedalt/ChangeLog
index 0550f9b86..78c14594b 100644
--- a/apps/speedalt/ChangeLog
+++ b/apps/speedalt/ChangeLog
@@ -9,3 +9,4 @@
0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight.
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
0.11: Now also runs on Bangle.js 2 with basic functionality
+0.12: Full functionality on Bangle.js 2: Bangle.js 1 buttons mapped to touch areas.
diff --git a/apps/speedalt/README.md b/apps/speedalt/README.md
index c21828aff..6f0d4efe5 100644
--- a/apps/speedalt/README.md
+++ b/apps/speedalt/README.md
@@ -2,23 +2,21 @@
You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint.
-*Note for **Bangle.js 2:** Currently only the BTN3 functionality is working with the Bangle.js 2 button.*
-
Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed.
The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information.
## Buttons and Controls
-BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint
+*(Mapping for **Bangle.js 2**: BTN2 = Touch upper right side; BTN3 = Touch lower right side; BTN4 = Touch left side)*
-***Bangle.js 2:** Currently only this button function is working*
+BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint
### [A]ltitude mode
BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum speed/alt values recorded.
-BTN1 : Long press > 2 secs resets the recorded maximum values.
+BTN1 : Long press > 2 secs resets the recorded maximum values. *(Bangle.js 2: Long press > 0.4 secs)*
### [D]istance mode
@@ -32,7 +30,7 @@ BTN1 : Select next waypoint.
BTN2 : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts.
-BTN3 : Long press exit and return to watch.
+BTN3 : Long press exit and return to watch. *(Bangle.js 2: Long press BTN > 2 secs)*
BTN4 : Left Display Tap : Swaps which figure is in the large display. You can have either speed or [A]ltitude/[D]istance on the large primary display.
diff --git a/apps/speedalt/app.js b/apps/speedalt/app.js
index f979762f1..79db932db 100644
--- a/apps/speedalt/app.js
+++ b/apps/speedalt/app.js
@@ -349,7 +349,7 @@ function drawSecondary(n,u) {
s = 30; // Font size
if (BANGLEJS2) s *= fontFactorB2;
buf.setFontVector(s);
- buf.drawString(u,xu - (BANGLEJS2*20),screenH_TwoThirds-25);
+ buf.drawString(u,xu - (BANGLEJS2*xu/5),screenH_TwoThirds-25);
}
function drawTime() {
@@ -391,7 +391,7 @@ function drawWP() { // from waypoints.json - see README.md
buf.setFontAlign(-1,1); //left, bottom
if (BANGLEJS2) s *= fontFactorB2;
buf.setFontVector(s);
- buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 20));
+ buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 15));
}
if ( cfg.modeA == 2 ) { // clock/large mode
@@ -421,7 +421,7 @@ function drawSats(sats) {
buf.drawString('A',screenW,140-(BANGLEJS2 * 40));
if ( showMax ) {
buf.setFontAlign(0,1); //centre, bottom
- buf.drawString('MAX',120,164);
+ buf.drawString('MAX',screenW_Half,screenH_TwoThirds + 4);
}
}
if ( cfg.modeA == 0 ) buf.drawString('D',screenW,140-(BANGLEJS2 * 40));
@@ -536,22 +536,18 @@ function onGPS(fix) {
}
-function setButtons(){
-if (!BANGLEJS2) { // Buttons for Bangle.js
- // Spd+Dist : Select next waypoint
- setWatch(function(e) {
- var dur = e.time - e.lastTime;
- if ( cfg.modeA == 1 ) {
- // Spd+Alt mode - Switch between fix and MAX
- if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
- else { max.spd = 0; max.alt = 0; } // Long press resets max values.
- }
- else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint
- onGPS(lf);
- }, BTN1, { edge:"falling",repeat:true});
- // Power saving on/off
- setWatch(function(e){
+function btn1press(longpress) {
+ if(emulator) console.log("Btn1, long="+longpress);
+ if ( cfg.modeA == 1 ) { // Spd+Alt mode - Switch between fix and MAX
+ if ( !longpress ) showMax = !showMax; // Short press toggle fix/max display
+ else { max.spd = 0; max.alt = 0; } // Long press resets max values.
+ }
+ else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint
+ onGPS(lf);
+ }
+function btn2press(){
+ if(emulator) console.log("Btn2");
pwrSav=!pwrSav;
if ( pwrSav ) {
LED1.reset();
@@ -564,52 +560,51 @@ if (!BANGLEJS2) { // Buttons for Bangle.js
Bangle.setLCDPower(1);
LED1.set();
}
- }, BTN2, {repeat:true,edge:"falling"});
-
- // Toggle between alt or dist
- setWatch(function(e){
- cfg.modeA = cfg.modeA+1;
- if ( cfg.modeA > 2 ) cfg.modeA = 0;
- savSettings();
- onGPS(lf);
- }, BTN3, {repeat:true,edge:"falling"});
-
- // Touch left screen to toggle display
- setWatch(function(e){
- cfg.primSpd = !cfg.primSpd;
- savSettings();
- onGPS(lf); // Update display
- }, BTN4, {repeat:true,edge:"falling"});
-
-} else { // Buttons for Bangle.js 2
- setWatch(function(e){ // Bangle.js BTN3
+ }
+function btn3press(){
+ if(emulator) console.log("Btn3");
cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 2 ) cfg.modeA = 0;
if(emulator)console.log("cfg.modeA="+cfg.modeA);
savSettings();
onGPS(lf);
- }, BTN1, {repeat:true,edge:"falling"});
-
-/* Bangle.on('tap', function(data) { // data - {dir, double, x, y, z}
+ }
+function btn4press(){
+ if(emulator) console.log("Btn4");
cfg.primSpd = !cfg.primSpd;
- if(emulator)console.log("!cfg.primSpd");
- }); */
+ savSettings();
+ onGPS(lf); // Update display
+ }
-/* Bangle.on('swipe', function(dir) {
- if (dir < 0) { // left: Bangle.js BTN3
- cfg.modeA = cfg.modeA+1;
- if ( cfg.modeA > 2 ) cfg.modeA = 0;
- if(emulator)console.log("cfg.modeA="+cfg.modeA);
- }
+
+function setButtons(){
+if (!BANGLEJS2) { // Buttons for Bangle.js 1
+ setWatch(function(e) {
+ btn1press(( e.time - e.lastTime) > 2); // > 2 sec. is long press
+ }, BTN1, { edge:"falling",repeat:true});
+
+ // Power saving on/off (red dot visible if off)
+ setWatch(btn2press, BTN2, {repeat:true,edge:"falling"});
+
+ // Toggle between alt or dist
+ setWatch(btn3press, BTN3, {repeat:true,edge:"falling"});
+
+ // Touch left screen to toggle display
+ setWatch(btn4press, BTN4, {repeat:true,edge:"falling"});
+
+} else { // Buttons for Bangle.js 2
+ setWatch(function(e) {
+ btn1press(( e.time - e.lastTime) > 0.4); // > 0.4 sec. is long press
+ }, BTN1, { edge:"falling",repeat:true});
+
+ Bangle.on('touch', function(btn_l_r, e) {
+ if(e.x < screenW_Half) btn4press();
else
- { // right: Bangle.js BTN4
- cfg.primSpd = !cfg.primSpd;
- if(emulator)console.log("!cfg.primSpd");
- }
+ if (e.y < screenH_Half)
+ btn2press();
+ else
+ btn3press();
});
-*/
- savSettings();
- onGPS(lf);
}
}
@@ -700,18 +695,6 @@ Bangle.on('lcdPower',function(on) {
else stopDraw();
});
-/*
-function onGPSraw(nmea) {
- var nofGP = 0, nofBD = 0, nofGL = 0;
- if (nmea.slice(3,6) == "GSV") {
- // console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
- if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
- if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
- if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
- SATinView = nofGP + nofBD + nofGL;
- } }
-if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
-*/
var gpssetup;
try {
diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json
index 617ac4b8e..e03d23c8b 100644
--- a/apps/speedalt/metadata.json
+++ b/apps/speedalt/metadata.json
@@ -2,7 +2,7 @@
"id": "speedalt",
"name": "GPS Adventure Sports",
"shortName": "GPS Adv Sport",
- "version": "0.11",
+ "version": "0.12",
"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",
diff --git a/apps/sunclock/README.md b/apps/sunclock/README.md
new file mode 100644
index 000000000..4076767d9
--- /dev/null
+++ b/apps/sunclock/README.md
@@ -0,0 +1,6 @@
+# Sun Clock
+Clock showing date/time, sunset/sunrise, H = current sun height/noon sun height, Az = sun azimuth
+
+
+
+Location set with mylocation app, time zone set with settings app.
diff --git a/apps/sunclock/app-icon.js b/apps/sunclock/app-icon.js
new file mode 100644
index 000000000..977aec98d
--- /dev/null
+++ b/apps/sunclock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("kEgwhC/AC8N6APo7oPJBQndBQYPEhoaFAogZIEokO93u8AuGAAYOCCAgOLCBQOFAAIeNEBAPPBw4wHB5wuIGAwPthGIxwIC8UowUuB4eIwAPBxEk91CAgIGGwAhBBYeCAwMoA4ZwEBIIOCAxAA/ABwA="))
diff --git a/apps/sunclock/app.js b/apps/sunclock/app.js
new file mode 100644
index 000000000..4609565a2
--- /dev/null
+++ b/apps/sunclock/app.js
@@ -0,0 +1,79 @@
+/* sclock.app.js for Bangle2
+Peter Bernschneider 30.12.2021
+Update current latitude and longitude in My Location app
+Update current Timezone in Settings app, menu item "System"
+Update for summer time by incrementing Timezone += 1 */
+setting = require("Storage").readJSON("setting.json",1);
+E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ
+SunCalc = require("suncalc.js");
+loc = require('locale');
+const LOCATION_FILE = "mylocation.json";
+const xyCenter = g.getWidth() / 2 + 3;
+const yposTime = 60;
+const yposDate = 100;
+const yposRS = 135;
+const yposPos = 160;
+var rise = "07:00";
+var set = "20:00";
+var pos = {altitude: 20, azimuth: 135};
+var noonpos = {altitude: 37, azimuth: 180};
+let idTimeout = null;
+
+function updatePos() {
+ coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":53.3,"lon":10.1,"location":"Pattensen"};
+ pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon);
+ times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon);
+ rise = times.sunrise.toString().split(" ")[4].substr(0,5);
+ set = times.sunset.toString().split(" ")[4].substr(0,5);
+ noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon);
+}
+
+function drawSimpleClock() {
+ var d = new Date(); // get date
+ var da = d.toString().split(" ");
+ g.clear();
+ Bangle.drawWidgets();
+ g.reset(); // default draw styles
+ g.setFontAlign(0, 0); // drawSting centered
+
+ var time = da[4].substr(0, 5); // draw time
+
+ g.setFont("Vector",60);
+ g.drawString(time, xyCenter, yposTime, true);
+
+ var date = [loc.dow(new Date(),1), loc.date(d,1)].join(" "); // draw day of week, date
+ g.setFont("Vector",24);
+ g.drawString(date, xyCenter, yposDate, true);
+
+ g.setFont("Vector",25);
+ g.drawString(`${rise} ${set}`, xyCenter, yposRS, true); // draw riseset
+ g.drawImage(require("Storage").read("sunrise.img"), xyCenter-16, yposRS-16);
+
+ g.setFont("Vector",21);
+ g.drawString(`H${pos.altitude}/${noonpos.altitude} Az${pos.azimuth}`, xyCenter, yposPos, true); // draw sun pos
+
+ let t = d.getSeconds()*1000 + d.getMilliseconds();
+ idTimeout = setTimeout(drawSimpleClock, 60000 - t); // time till next minute
+}
+
+// special function to handle display switch on
+Bangle.on('lcdPower', function(on){
+ if (on) {
+ drawSimpleClock();
+ } else {
+ if(idTimeout) {
+ clearTimeout(idTimeout);
+ }
+ }
+});
+
+g.clear(); // clean app screen
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+setInterval(updatePos, 60*5E3); // refesh every 5 mins
+
+updatePos();
+drawSimpleClock(); // draw now
+
+setWatch(Bangle.showLauncher, BTN1, { repeat: false, edge: "falling" }); // Show launcher when button pressed
diff --git a/apps/sunclock/app.png b/apps/sunclock/app.png
new file mode 100644
index 000000000..72c5b10d5
Binary files /dev/null and b/apps/sunclock/app.png differ
diff --git a/apps/sunclock/metadata.json b/apps/sunclock/metadata.json
new file mode 100644
index 000000000..a39343992
--- /dev/null
+++ b/apps/sunclock/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "sunclock",
+ "name": "Sun Clock",
+ "version": "0.01",
+ "description": "A clock with sunset/sunrise, sun height/azimuth",
+ "icon": "app.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"sunclock.app.js","url":"app.js"},
+ {"name":"sunclock.img","url":"app-icon.js","evaluate":true},
+ {"name":"suncalc.js","url":"suncalc.js"}
+ ]
+}
diff --git a/apps/sunclock/screenshot_sunclock.png b/apps/sunclock/screenshot_sunclock.png
new file mode 100644
index 000000000..a24af2116
Binary files /dev/null and b/apps/sunclock/screenshot_sunclock.png differ
diff --git a/apps/sunclock/suncalc.js b/apps/sunclock/suncalc.js
new file mode 100644
index 000000000..b1af0a0d9
--- /dev/null
+++ b/apps/sunclock/suncalc.js
@@ -0,0 +1,298 @@
+/* Module suncalc.js
+ (c) 2011-2015, Vladimir Agafonkin
+ SunCalc is a JavaScript library for calculating sun/moon position and light phases.
+ https://github.com/mourner/suncalc
+
+PB: Usage:
+E.setTimeZone(2); // 1 = MEZ, 2 = MESZ
+SunCalc = require("suncalc.js");
+pos = SunCalc.getPosition(Date.now(), 53.3, 10.1);
+times = SunCalc.getTimes(Date.now(), 53.3, 10.1);
+rise = times.sunrise; // Date object
+rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm
+*/
+var exports={};
+
+// shortcuts for easier to read formulas
+
+var PI = Math.PI,
+ sin = Math.sin,
+ cos = Math.cos,
+ tan = Math.tan,
+ asin = Math.asin,
+ atan = Math.atan2,
+ acos = Math.acos,
+ rad = PI / 180;
+
+// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
+
+// date/time constants and conversions
+
+var dayMs = 1000 * 60 * 60 * 24,
+ J1970 = 2440588,
+ J2000 = 2451545;
+
+function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
+function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021
+function toDays(date) { return toJulian(date) - J2000; }
+
+
+// general calculations for position
+
+var e = rad * 23.4397; // obliquity of the Earth
+
+function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
+function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
+
+function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
+function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
+
+function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
+
+function astroRefraction(h) {
+ if (h < 0) // the following formula works for positive altitudes only.
+ h = 0; // if h = -0.08901179 a div/0 would occur.
+
+ // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
+ return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
+}
+
+// general sun calculations
+
+function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
+
+function eclipticLongitude(M) {
+
+ var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
+ P = rad * 102.9372; // perihelion of the Earth
+
+ return M + C + P + PI;
+}
+
+function sunCoords(d) {
+
+ var M = solarMeanAnomaly(d),
+ L = eclipticLongitude(M);
+
+ return {
+ dec: declination(L, 0),
+ ra: rightAscension(L, 0)
+ };
+}
+
+// calculates sun position for a given date and latitude/longitude
+
+exports.getPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+
+ c = sunCoords(d),
+ H = siderealTime(d, lw) - c.ra;
+
+ return {
+ azimuth: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg
+ altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg
+ };
+};
+
+
+// sun times configuration (angle, morning name, evening name)
+
+var times = [
+ [-0.833, 'sunrise', 'sunset' ]
+];
+
+// calculations for sun times
+var J0 = 0.0009;
+
+function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
+
+function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
+function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
+
+function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
+function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
+
+// returns set time for the given sun altitude
+function getSetJ(h, lw, phi, dec, n, M, L) {
+
+ var w = hourAngle(h, phi, dec),
+ a = approxTransit(w, lw, n);
+ return solarTransitJ(a, M, L);
+}
+
+
+// calculates sun times for a given date, latitude/longitude, and, optionally,
+// the observer height (in meters) relative to the horizon
+
+exports.getTimes = function (date, lat, lng, height) {
+
+ height = height || 0;
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+
+ dh = observerAngle(height),
+
+ d = toDays(date),
+ n = julianCycle(d, lw),
+ ds = approxTransit(0, lw, n),
+
+ M = solarMeanAnomaly(ds),
+ L = eclipticLongitude(M),
+ dec = declination(L, 0),
+
+ Jnoon = solarTransitJ(ds, M, L),
+
+ i, len, time, h0, Jset, Jrise;
+
+
+ var result = {
+ solarNoon: fromJulian(Jnoon),
+ nadir: fromJulian(Jnoon - 0.5)
+ };
+
+ for (i = 0, len = times.length; i < len; i += 1) {
+ time = times[i];
+ h0 = (time[0] + dh) * rad;
+
+ Jset = getSetJ(h0, lw, phi, dec, n, M, L);
+ Jrise = Jnoon - (Jset - Jnoon);
+
+ result[time[1]] = fromJulian(Jrise);
+ result[time[2]] = fromJulian(Jset);
+ }
+
+ return result;
+};
+
+
+// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
+
+function moonCoords(d) { // geocentric ecliptic coordinates of the moon
+
+ var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
+ M = rad * (134.963 + 13.064993 * d), // mean anomaly
+ F = rad * (93.272 + 13.229350 * d), // mean distance
+
+ l = L + rad * 6.289 * sin(M), // longitude
+ b = rad * 5.128 * sin(F), // latitude
+ dt = 385001 - 20905 * cos(M); // distance to the moon in km
+
+ return {
+ ra: rightAscension(l, b),
+ dec: declination(l, b),
+ dist: dt
+ };
+}
+
+getMoonPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+
+ c = moonCoords(d),
+ H = siderealTime(d, lw) - c.ra,
+ h = altitude(H, phi, c.dec),
+ // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
+
+ h = h + astroRefraction(h); // altitude correction for refraction
+
+ return {
+ azimuth: azimuth(H, phi, c.dec),
+ altitude: h,
+ distance: c.dist,
+ parallacticAngle: pa
+ };
+};
+
+
+// calculations for illumination parameters of the moon,
+// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
+// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+
+getMoonIllumination = function (date) {
+
+ var d = toDays(date || new Date()),
+ s = sunCoords(d),
+ m = moonCoords(d),
+
+ sdist = 149598000, // distance from Earth to Sun in km
+
+ phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
+ inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
+ angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
+ cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
+
+ return {
+ fraction: (1 + cos(inc)) / 2,
+ phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
+ angle: angle
+ };
+};
+
+
+function hoursLater(date, h) {
+ return new Date(date.valueOf() + h * dayMs / 24);
+}
+
+// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
+
+getMoonTimes = function (date, lat, lng, inUTC) {
+ var t = new Date(date);
+ if (inUTC) t.setUTCHours(0, 0, 0, 0);
+ else t.setHours(0, 0, 0, 0);
+
+ var hc = 0.133 * rad,
+ h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
+ h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
+
+ // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
+ for (var i = 1; i <= 24; i += 2) {
+ h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
+ h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
+
+ a = (h0 + h2) / 2 - h1;
+ b = (h2 - h0) / 2;
+ xe = -b / (2 * a);
+ ye = (a * xe + b) * xe + h1;
+ d = b * b - 4 * a * h1;
+ roots = 0;
+
+ if (d >= 0) {
+ dx = Math.sqrt(d) / (Math.abs(a) * 2);
+ x1 = xe - dx;
+ x2 = xe + dx;
+ if (Math.abs(x1) <= 1) roots++;
+ if (Math.abs(x2) <= 1) roots++;
+ if (x1 < -1) x1 = x2;
+ }
+
+ if (roots === 1) {
+ if (h0 < 0) rise = i + x1;
+ else set = i + x1;
+
+ } else if (roots === 2) {
+ rise = i + (ye < 0 ? x2 : x1);
+ set = i + (ye < 0 ? x1 : x2);
+ }
+
+ if (rise && set) break;
+
+ h0 = h2;
+ }
+
+ var result = {};
+
+ if (rise) result.rise = hoursLater(t, rise);
+ if (set) result.set = hoursLater(t, set);
+
+ if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
+
+ return result;
+};
\ No newline at end of file
diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog
index 6515ab627..14159bc19 100644
--- a/apps/terminalclock/ChangeLog
+++ b/apps/terminalclock/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
-0.02: Rename "Activity" in "Motion" and display the true values for it
+0.02: Rename "Activity" in "Motion" and display the true values for it
+0.03: Add Banglejs 1 compatibility
diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js
index ab83a696f..d219b84d8 100644
--- a/apps/terminalclock/app.js
+++ b/apps/terminalclock/app.js
@@ -1,16 +1,28 @@
var locale = require("locale");
var fontColor = g.theme.dark ? "#0f0" : "#000";
-var paddingY = 2;
-var font6x8At4Size = 32;
-var font6x8At2Size = 18;
var heartRate = 0;
+// handling the differents versions of the Banglejs smartwatch
+if (process.env.HWVERSION == 1){
+ var paddingY = 3;
+ var font6x8At4Size = 48;
+ var font6x8At2Size = 27;
+ var font6x8FirstTextSize = 6;
+ var font6x8DefaultTextSize = 3;
+}
+else{
+ var paddingY = 2;
+ var font6x8At4Size = 32;
+ var font6x8At2Size = 18;
+ var font6x8FirstTextSize = 4;
+ var font6x8DefaultTextSize = 2;
+}
function setFontSize(pos){
if(pos == 1)
- g.setFont("6x8", 4);
+ g.setFont("6x8", font6x8FirstTextSize);
else
- g.setFont("6x8", 2);
+ g.setFont("6x8", font6x8DefaultTextSize);
}
function clearField(pos){
diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json
index de0244318..de369bf10 100644
--- a/apps/terminalclock/metadata.json
+++ b/apps/terminalclock/metadata.json
@@ -3,11 +3,12 @@
"name": "Terminal Clock",
"shortName":"Terminal Clock",
"description": "A terminal cli like clock displaying multiple sensor data",
- "version":"0.02",
+ "version":"0.03",
"icon": "app.png",
"type": "clock",
"tags": "clock",
- "supports": ["BANGLEJS2"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "allow_emulator": true,
"readme": "README.md",
"storage": [
{"name": "terminalclock.app.js","url": "app.js"},
diff --git a/apps/todolist/ChangeLog b/apps/todolist/ChangeLog
new file mode 100644
index 000000000..2e979ec12
--- /dev/null
+++ b/apps/todolist/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial release
\ No newline at end of file
diff --git a/apps/todolist/README.md b/apps/todolist/README.md
new file mode 100644
index 000000000..27c7cfb63
--- /dev/null
+++ b/apps/todolist/README.md
@@ -0,0 +1,40 @@
+Todo List
+========
+
+This is a simple Todo List application.
+
+
+
+The content is loaded from a JSON file.
+You can mark a task as completed.
+
+JSON file content example:
+```javascript
+[
+ {
+ name: "Pro",
+ children: [
+ {
+ name: "Read doc",
+ done: true,
+ children: [],
+ }
+ ],
+ },
+ {
+ name: "Pers",
+ children: [
+ {
+ name: "Grocery",
+ children: [
+ { name: "Milk", done: false, children: [] },
+ { name: "Eggs", done: false, children: [] },
+ { name: "Cheese", done: false, children: [] },
+ ],
+ },
+ { name: "Workout", done: false, children: [] },
+ { name: "Learn Rust", done: false, children: [] },
+ ],
+ },
+]
+```
\ No newline at end of file
diff --git a/apps/todolist/app-icon.js b/apps/todolist/app-icon.js
new file mode 100644
index 000000000..229852134
--- /dev/null
+++ b/apps/todolist/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwgmjiMRiAWTgIXUCoYZQB4IADC4YHECxkSkIECkQYLEwMSkQQBkcyCAMTmYKEiIuGif/AAIXBmciiUzC4MvBQPyC44LCC4YADBYpIFiM/BYZDBC5EhC4wKCBYKLFEYkxC5UxCwsSBYgXK/5GEmYuDC5oAKC/4XUmK5DC6PziMfC6cimTRB+bbDiSpCC5ItBaIXxbIg2CF5QqBB4IcCAAQvMCYMhdIi//X7P/X6sz+S/CkQADX8gXCif/GQIADMwS/LZ4a//BgkyJBK/ll/zmYADX54FBX9cyB4ZHEO5wPDa/7RJAAshC4xyCABacBC40SGBsxiIWEgEBW4gAKFwowCABwWGACgA=="))
\ No newline at end of file
diff --git a/apps/todolist/app.js b/apps/todolist/app.js
new file mode 100644
index 000000000..58cd3783c
--- /dev/null
+++ b/apps/todolist/app.js
@@ -0,0 +1,129 @@
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// Const
+let TODOLIST_FILE = "todolist.json";
+let MAX_DESCRIPTION_LEN = 14;
+
+// Clear todolist file
+// require("Storage").erase(TODOLIST_FILE);
+
+let DEFAULT_TODOLIST = [
+ {
+ name: "Pro",
+ children: [
+ {
+ name: "Read doc",
+ done: true,
+ children: [],
+ },
+ ],
+ },
+ {
+ name: "Pers",
+ children: [
+ {
+ name: "Grocery",
+ children: [
+ { name: "Milk", done: false, children: [] },
+ { name: "Eggs", done: false, children: [] },
+ { name: "Cheese", done: false, children: [] },
+ ],
+ },
+ { name: "Workout", done: false, children: [] },
+ { name: "Learn Rust", done: false, children: [] },
+ ],
+ },
+];
+
+// Load todolist
+let todolist =
+ require("Storage").readJSON(TODOLIST_FILE, true) || DEFAULT_TODOLIST;
+let menus = {};
+
+function writeData() {
+ require("Storage").writeJSON(TODOLIST_FILE, todolist);
+}
+
+function getChild(todolist, indexes) {
+ let childData = todolist;
+ for (let i = 0; i < indexes.length; i++) {
+ childData = childData[indexes[i]];
+ childData = childData.children;
+ }
+
+ return childData;
+}
+
+function getName(item) {
+ let title = item.name.substr(0, MAX_DESCRIPTION_LEN);
+ return title;
+}
+function getParentTitle(todolist, indexes) {
+ let parentIndexes = indexes.slice(0, indexes.length - 1);
+ let lastIndex = indexes[indexes.length - 1];
+ let item = getItem(todolist, parentIndexes, lastIndex);
+ return getName(item);
+}
+
+function getItem(todolist, parentIndexes, index) {
+ let childData = getChild(todolist, parentIndexes, index);
+ return childData[index];
+}
+
+function toggleableStatus(todolist, indexes, index) {
+ const reminder = getItem(todolist, indexes, index);
+ return {
+ value: !!reminder.done, // !! converts undefined to false
+ format: (val) => (val ? "[X]" : "[-]"),
+ onchange: (val) => {
+ reminder.done = val;
+ writeData();
+ },
+ };
+}
+
+function showSubMenu(key) {
+ const sub_menu = menus[key];
+ return E.showMenu(sub_menu);
+}
+
+function createListItem(todolist, indexes, index) {
+ let reminder = getItem(todolist, indexes, index);
+ if (reminder.children.length > 0) {
+ let childIndexes = [];
+ for (let i = 0; i < indexes.length; i++) {
+ childIndexes.push(indexes[i]);
+ }
+ childIndexes.push(index);
+ createMenus(todolist, childIndexes);
+ return () => showSubMenu(childIndexes);
+ } else {
+ return toggleableStatus(todolist, indexes, index);
+ }
+}
+
+function showMainMenu() {
+ const mainmenu = menus[""];
+ return E.showMenu(mainmenu);
+}
+
+function createMenus(todolist, indexes) {
+ const menuItem = {};
+ if (indexes.length == 0) {
+ menuItem[""] = { title: "todolist" };
+ } else {
+ menuItem[""] = { title: getParentTitle(todolist, indexes) };
+ menuItem["< Back"] = () =>
+ showSubMenu(indexes.slice(0, indexes.length - 1));
+ }
+ for (let i = 0; i < getChild(todolist, indexes).length; i++) {
+ const item = getItem(todolist, indexes, i);
+ const name = getName(item);
+ menuItem[name] = createListItem(todolist, indexes, i);
+ }
+ menus[indexes] = menuItem;
+}
+
+createMenus(todolist, []);
+showMainMenu();
diff --git a/apps/todolist/app.png b/apps/todolist/app.png
new file mode 100644
index 000000000..a93fc14ad
Binary files /dev/null and b/apps/todolist/app.png differ
diff --git a/apps/todolist/metadata.json b/apps/todolist/metadata.json
new file mode 100644
index 000000000..0833a86bd
--- /dev/null
+++ b/apps/todolist/metadata.json
@@ -0,0 +1,23 @@
+{
+ "id": "todolist",
+ "name": "TodoList",
+ "shortName": "TodoList",
+ "version": "0.01",
+ "type": "app",
+ "description": "Simple Todo List",
+ "icon": "app.png",
+ "allow_emulator": true,
+ "tags": "tool,todo",
+ "supports": ["BANGLEJS", "BANGLEJS2"],
+ "readme": "README.md",
+ "storage": [
+ { "name": "todolist.app.js", "url": "app.js" },
+ { "name": "todolist.img", "url": "app-icon.js", "evaluate": true }
+ ],
+ "data": [{ "name": "todolist.json" }],
+ "screenshots": [
+ { "url": "screenshot1.png" },
+ { "url": "screenshot2.png" },
+ { "url": "screenshot3.png" }
+ ]
+}
diff --git a/apps/todolist/screenshot1.png b/apps/todolist/screenshot1.png
new file mode 100644
index 000000000..523d60307
Binary files /dev/null and b/apps/todolist/screenshot1.png differ
diff --git a/apps/todolist/screenshot2.png b/apps/todolist/screenshot2.png
new file mode 100644
index 000000000..0337f9000
Binary files /dev/null and b/apps/todolist/screenshot2.png differ
diff --git a/apps/todolist/screenshot3.png b/apps/todolist/screenshot3.png
new file mode 100644
index 000000000..e5a4a85ac
Binary files /dev/null and b/apps/todolist/screenshot3.png differ
diff --git a/apps/touchtimer/ChangeLog b/apps/touchtimer/ChangeLog
index 01904c6ea..0969a3da4 100644
--- a/apps/touchtimer/ChangeLog
+++ b/apps/touchtimer/ChangeLog
@@ -1,2 +1,4 @@
0.01: Initial creation of the touch timer app
-0.02: Add settings menu
\ No newline at end of file
+0.02: Add settings menu
+0.03: Add ability to repeat last timer
+0.04: Add 5 second count down buzzer
diff --git a/apps/touchtimer/app.js b/apps/touchtimer/app.js
index ffa1af80a..c2f2fb5e9 100644
--- a/apps/touchtimer/app.js
+++ b/apps/touchtimer/app.js
@@ -126,6 +126,14 @@ var main = () => {
timerIntervalId = setInterval(() => {
timerCountDown.draw();
+ // Buzz lightly when there are less then 5 seconds left
+ if (settings.countDownBuzz) {
+ var remainingSeconds = timerCountDown.getAdjustedTime().seconds;
+ if (remainingSeconds <= 5 && remainingSeconds > 0) {
+ Bangle.buzz();
+ }
+ }
+
if (timerCountDown.isFinished()) {
buttonStartPause.value = "FINISHED!";
buttonStartPause.draw();
@@ -141,6 +149,13 @@ var main = () => {
if (buzzCount >= settings.buzzCount) {
clearInterval(buzzIntervalId);
buzzIntervalId = undefined;
+
+ buttonStartPause.value = "REPEAT";
+ buttonStartPause.draw();
+ buttonStartPause.value = "START";
+ timerCountDown = undefined;
+ timerEdit.draw();
+
return;
} else {
Bangle.buzz(settings.buzzDuration * 1000, 1);
diff --git a/apps/touchtimer/metadata.json b/apps/touchtimer/metadata.json
index 645a0ce18..0f2b9f491 100644
--- a/apps/touchtimer/metadata.json
+++ b/apps/touchtimer/metadata.json
@@ -2,7 +2,7 @@
"id": "touchtimer",
"name": "Touch Timer",
"shortName": "Touch Timer",
- "version": "0.02",
+ "version": "0.04",
"description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.",
"icon": "app.png",
"tags": "tools",
diff --git a/apps/touchtimer/settings.js b/apps/touchtimer/settings.js
index 885670f57..79424f250 100644
--- a/apps/touchtimer/settings.js
+++ b/apps/touchtimer/settings.js
@@ -31,6 +31,14 @@
writeSettings(settings);
},
},
+ "CountDown Buzz": {
+ value: !!settings.countDownBuzz,
+ format: value => value?"On":"Off",
+ onchange: (value) => {
+ settings.countDownBuzz = value;
+ writeSettings(settings);
+ },
+ },
"Pause Between": {
value: settings.pauseBetween,
min: 1,
diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog
index abbfcbb99..02831edde 100644
--- a/apps/vectorclock/ChangeLog
+++ b/apps/vectorclock/ChangeLog
@@ -5,3 +5,4 @@
0.05: "Chime the time" (buzz or beep) with up/down swipe added
0.06: Redraw widgets when time is updated
0.07: Fix problem with "Bangle.CLOCK": github.com/espruino/BangleApps/issues/1437
+0.08: Redraw widgets only once per minute
diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js
index 8d2961c4a..663a4c84f 100644
--- a/apps/vectorclock/app.js
+++ b/apps/vectorclock/app.js
@@ -81,7 +81,7 @@ function draw() {
executeCommands();
- Bangle.drawWidgets();
+ if (process.env.HWVERSION==2) Bangle.drawWidgets();
}
var timeout;
diff --git a/apps/vectorclock/metadata.json b/apps/vectorclock/metadata.json
index 0f558e3ee..541766fa2 100644
--- a/apps/vectorclock/metadata.json
+++ b/apps/vectorclock/metadata.json
@@ -1,7 +1,7 @@
{
"id": "vectorclock",
"name": "Vector Clock",
- "version": "0.07",
+ "version": "0.08",
"description": "A digital clock that uses the built-in vector font.",
"icon": "app.png",
"type": "clock",
diff --git a/apps/waveclk/ChangeLog b/apps/waveclk/ChangeLog
index 8c2a33143..f1fb77c59 100644
--- a/apps/waveclk/ChangeLog
+++ b/apps/waveclk/ChangeLog
@@ -1,2 +1,3 @@
0.01: New App!
0.02: Load widgets after setUI so widclk knows when to hide
+0.03: Show the day of the week
diff --git a/apps/waveclk/app.js b/apps/waveclk/app.js
index f1c67ce2f..18b28500b 100644
--- a/apps/waveclk/app.js
+++ b/apps/waveclk/app.js
@@ -41,6 +41,7 @@ function draw() {
var date = new Date();
var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date).toUpperCase();
+ var dowStr = require("locale").dow(date).toUpperCase();
// draw time
g.setFontAlign(0,0).setFont("ZCOOL");
g.drawString(timeStr,x,y);
@@ -48,6 +49,9 @@ function draw() {
y += 35;
g.setFontAlign(0,0,1).setFont("6x8");
g.drawString(dateStr,g.getWidth()-8,g.getHeight()/2);
+ // draw the day of the week
+ g.setFontAlign(0,0,3).setFont("6x8");
+ g.drawString(dowStr,8,g.getHeight()/2);
// queue draw in one minute
queueDraw();
}
diff --git a/apps/waveclk/metadata.json b/apps/waveclk/metadata.json
index a8d270da2..9ba2798ff 100644
--- a/apps/waveclk/metadata.json
+++ b/apps/waveclk/metadata.json
@@ -1,7 +1,7 @@
{
"id": "waveclk",
"name": "Wave Clock",
- "version": "0.02",
+ "version": "0.03",
"description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/)",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
diff --git a/apps/waveclk/screenshot.png b/apps/waveclk/screenshot.png
index 7f05ce688..161ef96ef 100644
Binary files a/apps/waveclk/screenshot.png and b/apps/waveclk/screenshot.png differ
diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog
new file mode 100644
index 000000000..1b584f7dd
--- /dev/null
+++ b/apps/waypointer/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New app!
+0.02: Make Bangle.js 2 compatible
diff --git a/apps/waypointer/README.md b/apps/waypointer/README.md
index e98fdbb7e..c0b4c5125 100644
--- a/apps/waypointer/README.md
+++ b/apps/waypointer/README.md
@@ -24,7 +24,7 @@ need to travel in to reach the selected waypoint. The blue text is
the name of the current waypoint. NONE means that there is no
waypoint set and so bearing and distance will remain at 0. To select
a waypoint, press BTN2 (middle) and wait for the blue text to turn
-white. Then use BTN1 and BTN3 to select a waypoint. The waypoint
+white. Then use BTN1 and BTN3 (swipe up/down on Bangle.js 2) to select a waypoint. The waypoint
choice is fixed by pressing BTN2 again. In the screen shot below a
waypoint giving the location of Stone Henge has been selected.
diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js
index d3aab7c50..615fbbc36 100644
--- a/apps/waypointer/app.js
+++ b/apps/waypointer/app.js
@@ -1,24 +1,25 @@
-var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
-var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
-var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
+const scale = g.getWidth()/240;
+var pal_by = new Uint16Array([g.getBgColor(),0xFFC0],0,1); // black, yellow
+var pal_bw = new Uint16Array([g.getBgColor(),g.getColor()],0,1); // black, white
+var pal_bb = new Uint16Array([g.getBgColor(),0x07ff],0,1); // black, blue
// having 3 2 color pallette keeps the memory requirement lower
-var buf1 = Graphics.createArrayBuffer(160,160,1, {msb:true});
-var buf2 = Graphics.createArrayBuffer(80,40,1, {msb:true});
+var buf1 = Graphics.createArrayBuffer(160*scale,160*scale,1, {msb:true});
+var buf2 = Graphics.createArrayBuffer(g.getWidth()/3,40*scale,1, {msb:true});
var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo="));
function flip1(x,y) {
- g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y);
+ g.drawImage({width:160*scale,height:160*scale,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y);
buf1.clear();
}
function flip2_bw(x,y) {
- g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y);
+ g.drawImage({width:g.getWidth()/3,height:40*scale,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y);
buf2.clear();
}
function flip2_bb(x,y) {
- g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y);
+ g.drawImage({width:g.getWidth()/3,height:40*scale,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y);
buf2.clear();
}
@@ -51,12 +52,12 @@ function drawCompass(course) {
previous.course = course;
buf1.setColor(1);
- buf1.fillCircle(80,80,79,79);
+ buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,79*scale);
buf1.setColor(0);
- buf1.fillCircle(80,80,69,69);
+ buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,69*scale);
buf1.setColor(1);
- buf1.drawImage(arrow_img, 80, 80, {scale:3, rotate:radians(course)} );
- flip1(40, 30);
+ buf1.drawImage(arrow_img, buf1.getWidth()/2, buf1.getHeight()/2, {scale:3*scale, rotate:radians(course)} );
+ flip1(40*scale, Bangle.appRect.y+6*scale);
}
/***** COMPASS CODE ***********/
@@ -138,7 +139,7 @@ function distance(a,b){
function drawN(){
- buf2.setFont("Vector",24);
+ buf2.setFont("Vector",24*scale);
var bs = wp_bearing.toString();
bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs;
var dst = loc.distance(dist);
@@ -147,12 +148,12 @@ function drawN(){
// show distance on the left
if (previous.dst !== dst) {
- previous.dst = dst
+ previous.dst = dst;
buf2.setColor(1);
buf2.setFontAlign(-1,-1);
- buf2.setFont("Vector", 20);
+ buf2.setFont("Vector", 20*scale);
buf2.drawString(dst,0,0);
- flip2_bw(0, 200);
+ flip2_bw(0, g.getHeight()-40*scale);
}
// bearing, place in middle at bottom of compass
@@ -160,9 +161,9 @@ function drawN(){
previous.bs = bs;
buf2.setColor(1);
buf2.setFontAlign(0, -1);
- buf2.setFont("Vector",38);
- buf2.drawString(bs,40,0);
- flip2_bw(80, 200);
+ buf2.setFont("Vector",38*scale);
+ buf2.drawString(bs,40*scale,0);
+ flip2_bw(g.getWidth()/3, g.getHeight()-40*scale);
}
// waypoint name on right
@@ -170,13 +171,13 @@ function drawN(){
previous.selected = selected;
buf2.setColor(1);
buf2.setFontAlign(1,-1); // right, bottom
- buf2.setFont("Vector", 20);
- buf2.drawString(wp.name, 80, 0);
+ buf2.setFont("Vector", 20*scale);
+ buf2.drawString(wp.name, 80*scale, 0);
if (selected)
- flip2_bw(160, 200);
+ flip2_bw(g.getWidth()/3*2, g.getHeight()-40*scale);
else
- flip2_bb(160, 200);
+ flip2_bb(g.getWidth()/3*2, g.getHeight()-40*scale);
}
}
@@ -229,9 +230,11 @@ function startdraw(){
}
function setButtons(){
- setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
- setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
- setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"});
+ Bangle.setUI("updown", d=>{
+ if (d<0) { nextwp(-1); }
+ else if (d>0) { nextwp(1); }
+ else { doselect(); }
+ });
}
Bangle.on('lcdPower',function(on) {
diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json
index cb477107b..111259bbc 100644
--- a/apps/waypointer/metadata.json
+++ b/apps/waypointer/metadata.json
@@ -1,11 +1,11 @@
{
"id": "waypointer",
"name": "Way Pointer",
- "version": "0.01",
+ "version": "0.02",
"description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation",
"icon": "waypointer.png",
"tags": "tool,outdoors,gps",
- "supports": ["BANGLEJS"],
+ "supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"interface": "waypoints.html",
"storage": [
diff --git a/apps/widclose/ChangeLog b/apps/widclose/ChangeLog
new file mode 100644
index 000000000..4be6afb16
--- /dev/null
+++ b/apps/widclose/ChangeLog
@@ -0,0 +1 @@
+0.01: New widget!
\ No newline at end of file
diff --git a/apps/widclose/README.md b/apps/widclose/README.md
new file mode 100644
index 000000000..55c8de483
--- /dev/null
+++ b/apps/widclose/README.md
@@ -0,0 +1,7 @@
+# Close Button
+
+Adds a  button to close the current app and go back to the clock.
+(Widget is not visible on the clock screen)
+
+
+
\ No newline at end of file
diff --git a/apps/widclose/icon.png b/apps/widclose/icon.png
new file mode 100644
index 000000000..1d95ba0ce
Binary files /dev/null and b/apps/widclose/icon.png differ
diff --git a/apps/widclose/metadata.json b/apps/widclose/metadata.json
new file mode 100644
index 000000000..e044a2d39
--- /dev/null
+++ b/apps/widclose/metadata.json
@@ -0,0 +1,15 @@
+{
+ "id": "widclose",
+ "name": "Close Button",
+ "version": "0.01",
+ "description": "A button to close the current app",
+ "readme": "README.md",
+ "icon": "icon.png",
+ "type": "widget",
+ "tags": "widget,tools",
+ "supports": ["BANGLEJS2"],
+ "screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}],
+ "storage": [
+ {"name":"widclose.wid.js","url":"widget.js"}
+ ]
+}
diff --git a/apps/widclose/preview.png b/apps/widclose/preview.png
new file mode 100644
index 000000000..d90a3b4c5
Binary files /dev/null and b/apps/widclose/preview.png differ
diff --git a/apps/widclose/screenshot_dark.png b/apps/widclose/screenshot_dark.png
new file mode 100644
index 000000000..58067a3b9
Binary files /dev/null and b/apps/widclose/screenshot_dark.png differ
diff --git a/apps/widclose/screenshot_light.png b/apps/widclose/screenshot_light.png
new file mode 100644
index 000000000..32817ea8d
Binary files /dev/null and b/apps/widclose/screenshot_light.png differ
diff --git a/apps/widclose/widget.js b/apps/widclose/widget.js
new file mode 100644
index 000000000..3a354018b
--- /dev/null
+++ b/apps/widclose/widget.js
@@ -0,0 +1,14 @@
+if (!Bangle.CLOCK) WIDGETS.close = {
+ area: "tr", width: 24, sortorder: 10, // we want the right-most spot please
+ draw: function() {
+ Bangle.removeListener("touch", this.touch);
+ Bangle.on("touch", this.touch);
+ g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button
+ // b/w version of preview.png, 24x24
+ "GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA"
+ ), this.x, this.y);
+ }, touch: function(_, c) {
+ const w = WIDGETS.close;
+ if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) load();
+ }
+};
\ No newline at end of file
diff --git a/backup.js b/backup.js
new file mode 100644
index 000000000..75e236049
--- /dev/null
+++ b/backup.js
@@ -0,0 +1,122 @@
+/* Code to handle Backup/Restore functionality */
+
+const BACKUP_STORAGEFILE_DIR = "storage-files";
+
+function bangleDownload() {
+ var zip = new JSZip();
+ Progress.show({title:"Scanning...",sticky:true});
+ var normalFiles, storageFiles;
+ console.log("Listing normal files...");
+ Comms.reset()
+ .then(() => Comms.showMessage("Backing up..."))
+ .then(() => Comms.listFiles({sf:false}))
+ .then(f => {
+ normalFiles = f;
+ console.log(" - "+f.join(","));
+ console.log("Listing StorageFiles...");
+ return Comms.listFiles({sf:true});
+ }).then(f => {
+ storageFiles = f;
+ console.log(" - "+f.join(","));
+ var fileCount = normalFiles.length + storageFiles.length;
+ var promise = Promise.resolve();
+ // Normal files
+ normalFiles.forEach((filename,n) => {
+ if (filename==".firmware") {
+ console.log("Ignoring .firmware file");
+ return;
+ }
+ promise = promise.then(() => {
+ Progress.hide({sticky: true});
+ var percent = n/fileCount;
+ Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0});
+ return Comms.readFile(filename).then(data => zip.file(filename,data));
+ });
+ });
+ // Storage files
+ if (storageFiles.length) {
+ var zipStorageFiles = zip.folder(BACKUP_STORAGEFILE_DIR);
+ storageFiles.forEach((filename,n) => {
+ promise = promise.then(() => {
+ Progress.hide({sticky: true});
+ var percent = (normalFiles.length+n)/fileCount;
+ Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0});
+ return Comms.readStorageFile(filename).then(data => zipStorageFiles.file(filename,data));
+ });
+ });
+ }
+ return promise;
+ }).then(() => {
+ return Comms.showMessage(Const.MESSAGE_RELOAD);
+ }).then(() => {
+ return zip.generateAsync({type:"binarystring"});
+ }).then(content => {
+ Progress.hide({ sticky: true });
+ showToast('Backup complete!', 'success');
+ Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip");
+ }).catch(err => {
+ Progress.hide({ sticky: true });
+ showToast('Backup failed, ' + err, 'error');
+ });
+}
+
+function bangleUpload() {
+ Espruino.Core.Utils.fileOpenDialog({
+ id:"backup",
+ type:"arraybuffer",
+ mimeType:".zip,application/zip"}, function(data) {
+ if (data===undefined) return;
+ var promise = Promise.resolve();
+ var zip = new JSZip();
+ var cmds = "";
+ zip.loadAsync(data).then(function(zip) {
+ return showPrompt("Restore from ZIP","Are you sure? This will remove all existing apps");
+ }).then(()=>{
+ Progress.show({title:`Reading ZIP`});
+ zip.forEach(function (path, file){
+ console.log("path");
+ promise = promise
+ .then(() => file.async("string"))
+ .then(data => {
+ console.log("decoded", path);
+ if (path.startsWith(BACKUP_STORAGEFILE_DIR)) {
+ path = path.substr(BACKUP_STORAGEFILE_DIR.length+1);
+ cmds += AppInfo.getStorageFileUploadCommands(path, data)+"\n";
+ } else if (!path.includes("/")) {
+ cmds += AppInfo.getFileUploadCommands(path, data)+"\n";
+ } else console.log("Ignoring "+path);
+ });
+ });
+ return promise;
+ })
+ .then(() => {
+ Progress.hide({sticky:true});
+ Progress.show({title:`Erasing...`});
+ return Comms.removeAllApps(); })
+ .then(() => {
+ Progress.hide({sticky:true});
+ Progress.show({title:`Restoring...`, sticky:true});
+ return Comms.showMessage(`Restoring...`); })
+ .then(() => Comms.write("\x10"+Comms.getProgressCmd()+"\n"))
+ .then(() => Comms.uploadCommandList(cmds, 0, cmds.length))
+ .then(() => Comms.showMessage(Const.MESSAGE_RELOAD))
+ .then(() => {
+ Progress.hide({sticky:true});
+ showToast('Restore complete!', 'success');
+ })
+ .catch(err => {
+ Progress.hide({sticky:true});
+ showToast('Restore failed, ' + err, 'error');
+ });
+ return promise;
+ });
+}
+
+window.addEventListener('load', (event) => {
+ document.getElementById("downloadallapps").addEventListener("click",event=>{
+ bangleDownload();
+ });
+ document.getElementById("uploadallapps").addEventListener("click",event=>{
+ bangleUpload();
+ });
+});
diff --git a/core b/core
index affb0b15b..27c7db603 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit affb0b15b41eb35a1548373831af7001bad64435
+Subproject commit 27c7db6035832837ca3909ea52939f60803df72f
diff --git a/css/main.css b/css/main.css
index f4850babe..a986df22e 100644
--- a/css/main.css
+++ b/css/main.css
@@ -81,7 +81,7 @@ a.btn.btn-link.dropdown-toggle {
min-height: 8em;
}
-.tile-content { position: relative; }
+.tile-content { position: relative; word-break: break-all; }
.link-github {
position:absolute;
top: 36px;
diff --git a/gadgetbridge.js b/gadgetbridge.js
new file mode 100644
index 000000000..679fffc60
--- /dev/null
+++ b/gadgetbridge.js
@@ -0,0 +1,162 @@
+/* Detects if we're running under Gadgetbridge in a WebView, and if
+so it overwrites the 'Puck' library with a special one that calls back
+into Gadgetbridge to handle watch communications */
+
+/*// test code
+Android = {
+ bangleTx : function(data) {
+ console.log("TX : "+JSON.stringify(data));
+ }
+};*/
+
+if (typeof Android!=="undefined") {
+ console.log("Running under Gadgetbridge, overwrite Puck library");
+
+ var isBusy = false;
+ var queue = [];
+ var connection = {
+ cb : function(data) {},
+ write : function(data, writecb) {
+ Android.bangleTx(data);
+ Puck.writeProgress(data.length, data.length);
+ if (writecb) setTimeout(writecb,10);
+ },
+ close : function() {},
+ received : "",
+ hadData : false
+ }
+
+ function bangleRx(data) {
+// document.getElementById("status").innerText = "RX:"+data;
+ connection.received += data;
+ connection.hadData = true;
+ if (connection.cb) connection.cb(data);
+ }
+
+ function log(level, s) {
+ if (Puck.log) Puck.log(level, s);
+ }
+
+ function handleQueue() {
+ if (!queue.length) return;
+ var q = queue.shift();
+ log(3,"Executing "+JSON.stringify(q)+" from queue");
+ if (q.type == "write") Puck.write(q.data, q.callback, q.callbackNewline);
+ else log(1,"Unknown queue item "+JSON.stringify(q));
+ }
+
+ /* convenience function... Write data, call the callback with data:
+ callbackNewline = false => if no new data received for ~0.2 sec
+ callbackNewline = true => after a newline */
+ function write(data, callback, callbackNewline) {
+ let result;
+ /// If there wasn't a callback function, then promisify
+ if (typeof callback !== 'function') {
+ callbackNewline = callback;
+
+ result = new Promise((resolve, reject) => callback = (value, err) => {
+ if (err) reject(err);
+ else resolve(value);
+ });
+ }
+
+ if (isBusy) {
+ log(3, "Busy - adding Puck.write to queue");
+ queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline});
+ return result;
+ }
+
+ var cbTimeout;
+ function onWritten() {
+ if (callbackNewline) {
+ connection.cb = function(d) {
+ var newLineIdx = connection.received.indexOf("\n");
+ if (newLineIdx>=0) {
+ var l = connection.received.substr(0,newLineIdx);
+ connection.received = connection.received.substr(newLineIdx+1);
+ connection.cb = undefined;
+ if (cbTimeout) clearTimeout(cbTimeout);
+ cbTimeout = undefined;
+ if (callback)
+ callback(l);
+ isBusy = false;
+ handleQueue();
+ }
+ };
+ }
+ // wait for any received data if we have a callback...
+ var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data
+ var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/;
+ var maxDataTime = dataWaitTime; // max time we wait after having received data
+ cbTimeout = setTimeout(function timeout() {
+ cbTimeout = undefined;
+ if (maxTime) maxTime--;
+ if (maxDataTime) maxDataTime--;
+ if (connection.hadData) maxDataTime=dataWaitTime;
+ if (maxDataTime && maxTime) {
+ cbTimeout = setTimeout(timeout, 100);
+ } else {
+ connection.cb = undefined;
+ if (callback)
+ callback(connection.received);
+ isBusy = false;
+ handleQueue();
+ connection.received = "";
+ }
+ connection.hadData = false;
+ }, 100);
+ }
+
+ if (!connection.txInProgress) connection.received = "";
+ isBusy = true;
+ connection.write(data, onWritten);
+ return result
+ }
+
+ // ----------------------------------------------------------
+
+ Puck = {
+ /// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
+ debug : Puck.debug,
+ /// Should we use flow control? Default is true
+ flowControl : true,
+ /// Used internally to write log information - you can replace this with your own function
+ log : function(level, s) { if (level <= this.debug) console.log(" "+s)},
+ /// Called with the current send progress or undefined when done - you can replace this with your own function
+ writeProgress : Puck.writeProgress,
+ connect : function(callback) {
+ setTimeout(callback, 10);
+ },
+ write : write,
+ eval : function(expr, cb) {
+ const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true)
+ .then(function (d) {
+ try {
+ return JSON.parse(d);
+ } catch (e) {
+ log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString());
+ return Promise.reject(d);
+ }
+ });
+ if (cb) {
+ return void response.then(cb, (err) => cb(null, err));
+ } else {
+ return response;
+ }
+ },
+ isConnected : function() { return true; },
+ getConnection : function() { return connection; },
+ close : function() {
+ if (connection)
+ connection.close();
+ },
+ };
+ // no need for header
+ document.getElementsByTagName("header")[0].style="display:none";
+ // force connection attempt automatically
+ setTimeout(function() {
+ getInstalledApps(true).catch(err => {
+ showToast("Device connection failed, "+err,"error");
+ });
+ }, 100);
+}
diff --git a/index.html b/index.html
index 6c9a21bf8..bd8ddea5a 100644
--- a/index.html
+++ b/index.html
@@ -131,6 +131,8 @@
+