Merge branch 'espruino:master' into master
12
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
|
## Testing
|
||||||
|
|
||||||
### Online
|
### Online
|
||||||
|
|
|
@ -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<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
|
||||||
|
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const yo = 20;
|
||||||
|
const xo = yo/2;
|
||||||
|
h = g.getHeight()-yo;
|
||||||
|
w = g.getWidth()-yo;
|
||||||
|
bh = Math.floor(h/4);
|
||||||
|
bw = Math.floor(w/4);
|
||||||
|
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
|
||||||
|
g.setFont("Vector", 16).setColor("#fff").drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
|
||||||
|
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
|
||||||
|
for (y=0; y<4; ++y)
|
||||||
|
for (x=0; x<4; ++x) {
|
||||||
|
b = this.b[y][x];
|
||||||
|
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
|
||||||
|
if (b > 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});
|
||||||
|
}
|
After Width: | Height: | Size: 4.4 KiB |
|
@ -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).
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))
|
After Width: | Height: | Size: 759 B |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"))
|
require("heatshrink").decompress(atob("mEkgIRO4AFJgPgAocDAoswAocHAokGjAFDhgFFhgFDjEOAoc4gxSE44FDuPjAod//+AAoXfn4FCgPMjJUCmIJBAoU7AoJUCv4CBsACBtwCBuACB4w3CEQIaCKgMBFgQFBgYFCLQMDMIfAg55D4BcDg/gNAcD+B0DSIMcOgiGEjCYEjgFEhhVCUgQ"))
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY="))
|
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="))
|
||||||
|
|
|
@ -4,3 +4,4 @@
|
||||||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||||
0.05: Add support for bulk importing and exporting tokens
|
0.05: Add support for bulk importing and exporting tokens
|
||||||
0.06: Add spaces to codes for improved readability (thanks @BartS23)
|
0.06: Add spaces to codes for improved readability (thanks @BartS23)
|
||||||
|
0.07: Bangle 2: Improve drag responsiveness and exit on button press
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const tokenextraheight = 16;
|
const COUNTER_TRIANGLE_SIZE = 10;
|
||||||
var tokendigitsheight = 30;
|
const TOKEN_EXTRA_HEIGHT = 16;
|
||||||
var tokenheight = tokendigitsheight + tokenextraheight;
|
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
|
// Hash functions
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const algos = {
|
const algos = {
|
||||||
|
@ -8,33 +12,24 @@ const algos = {
|
||||||
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
|
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
|
||||||
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
|
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
|
||||||
};
|
};
|
||||||
const calculating = "Calculating";
|
const CALCULATING = /*LANG*/"Calculating";
|
||||||
const notokens = "No tokens";
|
const NO_TOKENS = /*LANG*/"No tokens";
|
||||||
const notsupported = "Not supported";
|
const NOT_SUPPORTED = /*LANG*/"Not supported";
|
||||||
|
|
||||||
// sample settings:
|
// sample settings:
|
||||||
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
|
// {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.data ) tokens = settings.data ; /* v0.02 settings */
|
||||||
if (settings.tokens) tokens = settings.tokens; /* v0.03+ 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) {
|
function b32decode(seedstr) {
|
||||||
// RFC4648
|
// RFC4648 Base16/32/64 Data Encodings
|
||||||
var i, buf = 0, bitcount = 0, retstr = "";
|
let buf = 0, bitcount = 0, retstr = "";
|
||||||
for (i in seedstr) {
|
for (let c of seedstr.toUpperCase()) {
|
||||||
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
if (c == '0') c = 'O';
|
||||||
|
if (c == '1') c = 'I';
|
||||||
|
if (c == '8') c = 'B';
|
||||||
|
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
|
||||||
if (c != -1) {
|
if (c != -1) {
|
||||||
buf <<= 5;
|
buf <<= 5;
|
||||||
buf |= c;
|
buf |= c;
|
||||||
|
@ -46,195 +41,127 @@ function b32decode(seedstr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var retbuf = new Uint8Array(retstr.length);
|
let retbuf = new Uint8Array(retstr.length);
|
||||||
for (i in retstr) {
|
for (let i in retstr) {
|
||||||
retbuf[i] = retstr.charCodeAt(i);
|
retbuf[i] = retstr.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return retbuf;
|
return retbuf;
|
||||||
}
|
}
|
||||||
function do_hmac(key, message, algo) {
|
|
||||||
var a = algos[algo];
|
function hmac(key, message, algo) {
|
||||||
// RFC2104
|
let a = algos[algo.toUpperCase()];
|
||||||
|
// RFC2104 HMAC
|
||||||
if (key.length > a.blksz) {
|
if (key.length > a.blksz) {
|
||||||
key = a.sha(key);
|
key = a.sha(key);
|
||||||
}
|
}
|
||||||
var istr = new Uint8Array(a.blksz + message.length);
|
let istr = new Uint8Array(a.blksz + message.length);
|
||||||
var ostr = new Uint8Array(a.blksz + a.retsz);
|
let ostr = new Uint8Array(a.blksz + a.retsz);
|
||||||
for (var i = 0; i < a.blksz; ++i) {
|
for (let i = 0; i < a.blksz; ++i) {
|
||||||
var c = (i < key.length) ? key[i] : 0;
|
let c = (i < key.length) ? key[i] : 0;
|
||||||
istr[i] = c ^ 0x36;
|
istr[i] = c ^ 0x36;
|
||||||
ostr[i] = c ^ 0x5C;
|
ostr[i] = c ^ 0x5C;
|
||||||
}
|
}
|
||||||
istr.set(message, a.blksz);
|
istr.set(message, a.blksz);
|
||||||
ostr.set(a.sha(istr), a.blksz);
|
ostr.set(a.sha(istr), a.blksz);
|
||||||
var ret = a.sha(ostr);
|
let ret = a.sha(ostr);
|
||||||
// RFC4226 dynamic truncation
|
// RFC4226 HOTP (dynamic truncation)
|
||||||
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||||
return v.getUint32(0) & 0x7FFFFFFF;
|
return v.getUint32(0) & 0x7FFFFFFF;
|
||||||
}
|
}
|
||||||
function hotp(d, token, dohmac) {
|
|
||||||
var tick;
|
function formatOtp(otp, digits) {
|
||||||
if (token.period > 0) {
|
// add 0 padding
|
||||||
// RFC6238 - timed
|
let ret = "" + otp % Math.pow(10, digits);
|
||||||
var seconds = Math.floor(d.getTime() / 1000);
|
while (ret.length < digits) {
|
||||||
tick = Math.floor(seconds / token.period);
|
|
||||||
} else {
|
|
||||||
// RFC4226 - counter
|
|
||||||
tick = -token.period;
|
|
||||||
}
|
|
||||||
var msg = new Uint8Array(8);
|
|
||||||
var 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;
|
ret = "0" + ret;
|
||||||
}
|
}
|
||||||
// add a space after every 3rd or 4th digit
|
// 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)) ? "" : ".";
|
let re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : ".";
|
||||||
ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
||||||
} catch(err) {
|
|
||||||
ret = notsupported;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hotp(token) {
|
||||||
|
let d = Date.now();
|
||||||
|
let tick, next;
|
||||||
|
if (token.period > 0) {
|
||||||
|
// RFC6238 - timed
|
||||||
|
tick = Math.floor(Math.floor(d / 1000) / token.period);
|
||||||
|
next = (tick + 1) * token.period * 1000;
|
||||||
|
} else {
|
||||||
|
// RFC4226 - counter
|
||||||
|
tick = -token.period;
|
||||||
|
next = d + 30000;
|
||||||
|
}
|
||||||
|
let msg = new Uint8Array(8);
|
||||||
|
let v = new DataView(msg.buffer);
|
||||||
|
v.setUint32(0, tick >> 16 >> 16);
|
||||||
|
v.setUint32(4, tick & 0xFFFFFFFF);
|
||||||
|
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: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 = {
|
var state = {
|
||||||
listy: 0,
|
listy:0, // list scroll position
|
||||||
prevcur:0,
|
id:-1, // current token ID
|
||||||
curtoken:-1,
|
hotp:{hotp:"",next:0}
|
||||||
nextTime:0,
|
|
||||||
otp:"",
|
|
||||||
rem:0,
|
|
||||||
hide:0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function drawToken(id, r) {
|
function sizeFont(id, txt, w) {
|
||||||
var x1 = r.x;
|
let sz = fontszCache[id];
|
||||||
var y1 = r.y;
|
if (!sz) {
|
||||||
var x2 = r.x + r.w - 1;
|
sz = TOKEN_DIGITS_HEIGHT;
|
||||||
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;
|
|
||||||
do {
|
do {
|
||||||
g.setFont("Vector", sz--);
|
g.setFont("Vector", sz--);
|
||||||
} while (g.stringWidth(lbl) > r.w);
|
} while (g.stringWidth(txt) > w);
|
||||||
// center in box
|
fontszCache[id] = ++sz;
|
||||||
g.setFontAlign(0, 0, 0);
|
|
||||||
adj = (y1 + y2) / 2;
|
|
||||||
}
|
}
|
||||||
g.clearRect(x1, y1, x2, y2)
|
g.setFont("Vector", sz);
|
||||||
.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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
|
||||||
var timerfn = exitApp;
|
half = n => Math.floor(n / 2);
|
||||||
var timerdly = 10000;
|
|
||||||
var d = new Date();
|
function timerCalc() {
|
||||||
if (state.curtoken != -1) {
|
let timerfn = exitApp;
|
||||||
var t = tokens[state.curtoken];
|
let timerdly = 10000;
|
||||||
if (state.otp == calculating) {
|
if (state.id >= 0 && state.hotp.hotp != "") {
|
||||||
state.otp = hotp(d, t, true).hotp;
|
if (tokens[state.id].period > 0) {
|
||||||
}
|
// timed HOTP
|
||||||
if (d.getTime() > state.nextTime) {
|
if (state.hotp.next < Date.now()) {
|
||||||
if (state.hide == 0) {
|
if (state.cnt > 0) {
|
||||||
// auto-hide the current token
|
state.cnt--;
|
||||||
if (state.curtoken != -1) {
|
state.hotp = hotp(tokens[state.id]);
|
||||||
state.prevcur = state.curtoken;
|
|
||||||
state.curtoken = -1;
|
|
||||||
}
|
|
||||||
state.nextTime = 0;
|
|
||||||
} else {
|
} else {
|
||||||
// time to generate a new token
|
state.hotp.hotp = "";
|
||||||
var r = hotp(d, t, state.otp != "");
|
|
||||||
state.nextTime = r.next;
|
|
||||||
state.otp = r.hotp;
|
|
||||||
if (t.period <= 0) {
|
|
||||||
state.hide = 1;
|
|
||||||
}
|
}
|
||||||
state.hide--;
|
timerdly = 1;
|
||||||
}
|
timerfn = updateCurrentToken;
|
||||||
}
|
|
||||||
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 {
|
} else {
|
||||||
timerdly = state.nexttime - d.getTime(); // counter
|
timerdly = 1000;
|
||||||
}
|
timerfn = updateProgressBar;
|
||||||
timerfn = draw;
|
|
||||||
if (tokens[state.curtoken].period <= 0) {
|
|
||||||
state.hide = 0;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// de-select the current token if it is scrolled out of view
|
// counter HOTP
|
||||||
if (state.curtoken != -1) {
|
if (state.cnt > 0) {
|
||||||
state.prevcur = state.curtoken;
|
state.cnt--;
|
||||||
state.curtoken = -1;
|
timerdly = 30000;
|
||||||
}
|
|
||||||
state.nexttime = 0;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
g.setFont("Vector", tokendigitsheight)
|
state.hotp.hotp = "";
|
||||||
.setFontAlign(0, 0, 0)
|
timerdly = 1;
|
||||||
.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
}
|
||||||
|
timerfn = updateCurrentToken;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state.drawtimer) {
|
if (state.drawtimer) {
|
||||||
clearTimeout(state.drawtimer);
|
clearTimeout(state.drawtimer);
|
||||||
|
@ -242,97 +169,236 @@ function draw() {
|
||||||
state.drawtimer = setTimeout(timerfn, timerdly);
|
state.drawtimer = setTimeout(timerfn, timerdly);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouch(zone, e) {
|
function updateCurrentToken() {
|
||||||
if (e) {
|
drawToken(state.id);
|
||||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight);
|
timerCalc();
|
||||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
|
||||||
id = -1;
|
|
||||||
}
|
}
|
||||||
if (state.curtoken != id) {
|
|
||||||
if (id != -1) {
|
function updateProgressBar() {
|
||||||
var y = id * tokenheight - state.listy;
|
drawProgressBar();
|
||||||
if (y < 0) {
|
timerCalc();
|
||||||
state.listy += y;
|
|
||||||
y = 0;
|
|
||||||
}
|
}
|
||||||
y += tokenheight;
|
|
||||||
if (y > Bangle.appRect.h) {
|
function drawProgressBar() {
|
||||||
state.listy += (y - Bangle.appRect.h);
|
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);
|
||||||
}
|
}
|
||||||
state.otp = "";
|
} else {
|
||||||
}
|
// token not visible
|
||||||
state.nextTime = 0;
|
state.id = -1;
|
||||||
state.curtoken = id;
|
}
|
||||||
state.hide = 2;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
draw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrag(e) {
|
function onDrag(e) {
|
||||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
state.cnt = IDLE_REPEATS;
|
||||||
if (e.dx == 0 && e.dy == 0) return;
|
if (e.b != 0 && e.dy != 0) {
|
||||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h);
|
let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
|
||||||
state.listy = Math.max(0, newy);
|
if (state.listy != y) {
|
||||||
draw();
|
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) {
|
function onSwipe(e) {
|
||||||
if (e == 1) {
|
state.cnt = IDLE_REPEATS;
|
||||||
|
switch (e) {
|
||||||
|
case 1:
|
||||||
exitApp();
|
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);
|
||||||
}
|
}
|
||||||
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
break;
|
||||||
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 (tokens.length > 0) {
|
||||||
if (state.curtoken == -1) {
|
let id = E.clip(state.id + e, 0, tokens.length - 1);
|
||||||
state.curtoken = state.prevcur;
|
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))});
|
||||||
} else {
|
changeId(id);
|
||||||
switch (e) {
|
drawProgressBar();
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
timerCalc();
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitApp() {
|
function exitApp() {
|
||||||
|
if (state.drawtimer) {
|
||||||
|
clearTimeout(state.drawtimer);
|
||||||
|
}
|
||||||
Bangle.showLauncher();
|
Bangle.showLauncher();
|
||||||
}
|
}
|
||||||
|
|
||||||
Bangle.on('touch', onTouch);
|
Bangle.on('touch', onTouch);
|
||||||
Bangle.on('drag' , onDrag );
|
Bangle.on('drag' , onDrag );
|
||||||
Bangle.on('swipe', onSwipe);
|
Bangle.on('swipe', onSwipe);
|
||||||
if (typeof BTN2 == 'number') {
|
if (typeof BTN1 == 'number') {
|
||||||
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
|
if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
|
||||||
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
|
setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||||
setWatch(function(){bangle1Btn( 1);}, BTN3, {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();
|
Bangle.loadWidgets();
|
||||||
|
const AR = Bangle.appRect;
|
||||||
// Clear the screen once, at startup
|
// draw the initial display
|
||||||
g.clear();
|
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();
|
Bangle.drawWidgets();
|
||||||
|
|
|
@ -54,9 +54,9 @@ var tokens = settings.tokens;
|
||||||
*/
|
*/
|
||||||
function base32clean(val, nows) {
|
function base32clean(val, nows) {
|
||||||
var ret = val.replaceAll(/\s+/g, ' ');
|
var ret = val.replaceAll(/\s+/g, ' ');
|
||||||
ret = ret.replaceAll(/0/g, 'O');
|
ret = ret.replaceAll('0', 'O');
|
||||||
ret = ret.replaceAll(/1/g, 'I');
|
ret = ret.replaceAll('1', 'I');
|
||||||
ret = ret.replaceAll(/8/g, 'B');
|
ret = ret.replaceAll('8', 'B');
|
||||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||||
if (nows) {
|
if (nows) {
|
||||||
ret = ret.replaceAll(/\s+/g, '');
|
ret = ret.replaceAll(/\s+/g, '');
|
||||||
|
@ -81,9 +81,9 @@ function b32encode(str) {
|
||||||
|
|
||||||
function b32decode(seedstr) {
|
function b32decode(seedstr) {
|
||||||
// RFC4648
|
// RFC4648
|
||||||
var i, buf = 0, bitcount = 0, ret = '';
|
var buf = 0, bitcount = 0, ret = '';
|
||||||
for (i in seedstr) {
|
for (var c of seedstr.toUpperCase()) {
|
||||||
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
|
||||||
if (c != -1) {
|
if (c != -1) {
|
||||||
buf <<= 5;
|
buf <<= 5;
|
||||||
buf |= c;
|
buf |= c;
|
||||||
|
@ -405,7 +405,7 @@ class proto3decoder {
|
||||||
constructor(str) {
|
constructor(str) {
|
||||||
this.buf = [];
|
this.buf = [];
|
||||||
for (let i in str) {
|
for (let i in str) {
|
||||||
this.buf = this.buf.concat(str.charCodeAt(i));
|
this.buf.push(str.charCodeAt(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getVarint() {
|
getVarint() {
|
||||||
|
@ -487,7 +487,7 @@ function startScan(handler,cancel) {
|
||||||
document.body.className = 'scanning';
|
document.body.className = 'scanning';
|
||||||
navigator.mediaDevices
|
navigator.mediaDevices
|
||||||
.getUserMedia({video:{facingMode:'environment'}})
|
.getUserMedia({video:{facingMode:'environment'}})
|
||||||
.then(function(stream){
|
.then(stream => {
|
||||||
scanning=true;
|
scanning=true;
|
||||||
video.setAttribute('playsinline',true);
|
video.setAttribute('playsinline',true);
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"shortName": "AuthWatch",
|
"shortName": "AuthWatch",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.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.",
|
"description": "Google Authenticator compatible tool.",
|
||||||
"tags": "tool",
|
"tags": "tool",
|
||||||
"interface": "interface.html",
|
"interface": "interface.html",
|
||||||
|
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.4 KiB |
|
@ -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:**<br>
|
||||||
|
Bike Speedometer App by <i>github.com/HilmarSt</i><br>
|
||||||
|
Big parts of the software are based on <i>github.com/espruino/BangleApps/tree/master/apps/speedalt</i><br>
|
||||||
|
Compass and Compass Calibration based on <i>github.com/espruino/BangleApps/tree/master/apps/magnav</i>
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+64A/AC+sF1uBgAwsq1W1krGEmswIFDlcAFoMrqyGjlcrGAQDB1guBBQJghKYZZCMYhqBlYugFAesgAuFYgQIHAE2sYMZDfwIABbgIuowMAqwABb4wAjFVQAEqyMrF4cAlYABqwypR4RgBwIyplYnF1hnBGIo8BAAQvhGIj6C1hpBgChBGCqGBqwdCRQQnCB4gJBGAgtWc4WBPoi9JH4ILBGYQATPoRHJRYoACwLFBLi4tGLIyLEA5QuPCoYpEMhBBBGDIuFgArIYQIUHA4b+GABLUBAwoQIXorDGI5RNGCB9WRQ0AJwwHGDxChOH4oDCRI4/GXpAaB1gyLEwlWKgTrBT46ALCogQKZoryFCwzgGBgz/NZpaQHHBCdEF5hKBBxWBUwoGBgEAEoIyHHYesBg7aBJQ7SBBAIvEIIJCBD4IFBgBIGEAcAUA8rGAIWHS4QvDCAJAHG4JfRCgKCFeAovCdRIiBDYq/NABi0Cfo5IEBgjUGACZ6BqwcGwLxBFYRsEHIKBIJwLkBNoIHDF468GYgIBBXY4EDE4IHDYwSwCN4IGBCIp5CJYtWgBZBHAgFEMoRjEE4QDCLYJUEUoaCBPYoQCgA4FGozxFLYwfEQgqrGexIYFBoxbDS4YHCIAYVEEAZcCYwwvGfoQHEcwQHHIg9WIAS9BIoYYESoowIABQuBUgg1DVwwACEpIwBChDLFDQ5JLlZnHJAajBQwgLEO4LDBHKAhBFxQxFCIIACAwadLHgJJBAAUrQJxYFAAbKPCwRGCCqAAm"))
|
|
@ -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();
|
After Width: | Height: | Size: 751 B |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Fixed issue with wrong device informations
|
0.02: Fixed issue with wrong device informations
|
||||||
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
||||||
|
0.04: Set Bangle.js 2 compatible, show widgets
|
||||||
|
|
|
@ -5,6 +5,7 @@ let menu = {
|
||||||
|
|
||||||
function showMainMenu() {
|
function showMainMenu() {
|
||||||
menu["< Back"] = () => load();
|
menu["< Back"] = () => load();
|
||||||
|
Bangle.drawWidgets();
|
||||||
return E.showMenu(menu);
|
return E.showMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,5 +56,6 @@ function waitMessage() {
|
||||||
E.showMessage("scanning");
|
E.showMessage("scanning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
scan();
|
scan();
|
||||||
waitMessage();
|
waitMessage();
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "bledetect",
|
"id": "bledetect",
|
||||||
"name": "BLE Detector",
|
"name": "BLE Detector",
|
||||||
"shortName": "BLE Detector",
|
"shortName": "BLE Detector",
|
||||||
"version": "0.03",
|
"version": "0.04",
|
||||||
"description": "Detect BLE devices and show some informations.",
|
"description": "Detect BLE devices and show some informations.",
|
||||||
"icon": "bledetect.png",
|
"icon": "bledetect.png",
|
||||||
"tags": "app,bluetooth,tool",
|
"tags": "app,bluetooth,tool",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"bledetect.app.js","url":"bledetect.js"},
|
{"name":"bledetect.app.js","url":"bledetect.js"},
|
||||||
|
|
|
@ -46,3 +46,7 @@
|
||||||
0.40: Bootloader now rebuilds for new firmware versions
|
0.40: Bootloader now rebuilds for new firmware versions
|
||||||
0.41: Add Keyboard and Mouse Bluetooth HID option
|
0.41: Add Keyboard and Mouse Bluetooth HID option
|
||||||
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.boot.js
|
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.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
|
||||||
|
|
|
@ -14,6 +14,6 @@ if (!clockApp) {
|
||||||
if (clockApp)
|
if (clockApp)
|
||||||
clockApp = require("Storage").read(clockApp.src);
|
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);
|
eval(clockApp);
|
||||||
delete clockApp;
|
delete clockApp;
|
||||||
|
|
|
@ -4,7 +4,7 @@ of the time. */
|
||||||
E.showMessage("Updating boot0...");
|
E.showMessage("Updating boot0...");
|
||||||
var s = require('Storage').readJSON('setting.json',1)||{};
|
var s = require('Storage').readJSON('setting.json',1)||{};
|
||||||
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
|
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
|
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);
|
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})`;
|
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 += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
|
||||||
boot += `E.setFlags({pretokenise:1});\n`;
|
boot += `E.setFlags({pretokenise:1});\n`;
|
||||||
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\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.ble!==false) {
|
||||||
if (s.HID) { // Human interface device
|
if (s.HID) { // Human interface device
|
||||||
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
|
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
|
||||||
|
@ -38,7 +39,7 @@ LoopbackA.setConsole(true);\n`;
|
||||||
boot += `
|
boot += `
|
||||||
Bluetooth.line="";
|
Bluetooth.line="";
|
||||||
Bluetooth.on('data',function(d) {
|
Bluetooth.on('data',function(d) {
|
||||||
var l = (Bluetooth.line + d).split("\n");
|
var l = (Bluetooth.line + d).split(/[\\n\\r]/);
|
||||||
Bluetooth.line = l.pop();
|
Bluetooth.line = l.pop();
|
||||||
l.forEach(n=>Bluetooth.emit("line",n));
|
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
|
// Append *.boot.js files
|
||||||
// These could change bleServices/bleServiceOptions if needed
|
// These could change bleServices/bleServiceOptions if needed
|
||||||
|
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
|
||||||
var getPriority = /.*\.(\d+)\.boot\.js$/;
|
var getPriority = /.*\.(\d+)\.boot\.js$/;
|
||||||
require('Storage').list(/\.boot\.js/).sort((a,b)=>{
|
|
||||||
var aPriority = a.match(getPriority);
|
var aPriority = a.match(getPriority);
|
||||||
var bPriority = b.match(getPriority);
|
var bPriority = b.match(getPriority);
|
||||||
if (aPriority && bPriority){
|
if (aPriority && bPriority){
|
||||||
|
@ -206,18 +207,40 @@ require('Storage').list(/\.boot\.js/).sort((a,b)=>{
|
||||||
} else if (!aPriority && bPriority){
|
} else if (!aPriority && bPriority){
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return a > b;
|
return a==b ? 0 : (a>b ? 1 : -1);
|
||||||
}).forEach(bootFile=>{
|
});
|
||||||
|
// 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(){ ... }()
|
// we add a semicolon so if the file is wrapped in (function(){ ... }()
|
||||||
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
|
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
|
||||||
// which would cause an error!
|
// 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
|
require('Storage').write('.boot0',bootPost,fileOffset);
|
||||||
boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`;
|
|
||||||
// write file
|
|
||||||
require('Storage').write('.boot0',boot);
|
|
||||||
delete boot;
|
delete boot;
|
||||||
|
delete bootPost;
|
||||||
|
delete bootFiles;
|
||||||
|
delete fileSize;
|
||||||
|
delete fileOffset;
|
||||||
E.showMessage("Reloading...");
|
E.showMessage("Reloading...");
|
||||||
eval(require('Storage').read('.boot0'));
|
eval(require('Storage').read('.boot0'));
|
||||||
// .bootcde should be run automatically after if required, since
|
// .bootcde should be run automatically after if required, since
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "boot",
|
"id": "boot",
|
||||||
"name": "Bootloader",
|
"name": "Bootloader",
|
||||||
"version": "0.42",
|
"version": "0.46",
|
||||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||||
"icon": "bootloader.png",
|
"icon": "bootloader.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
|
|
|
@ -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).
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+AA/TADwoIFkYyOF0owIF04wGUSqvVBZQtZGJYJIFzomKF0onIF07EKF0owLF9wNEnwACE6oZILxovbMBov/F/4v/C54uWF/4vKBQQLLF/4YPFwYMLF7AZGF5Y5KF5xJIFwoMJD44vaBhwvcLQpgHF8gGRF6xYNBpQvTXBoNOF65QJBIgvjBywvUV5YOOF64OIB54v/cQwAKB5ov/F84wKADYuIF+AwkFIwwnE45hmExCSlEpTEiERr3KADw+PF0ownUSoseA=="))
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -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 = "<del>";
|
||||||
|
layout.bt4.label = "<ent>";
|
||||||
|
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<w.length; ++i) {
|
||||||
|
c = w[i];
|
||||||
|
col = g.theme.bg;
|
||||||
|
if (sol[i]==c) {
|
||||||
|
sol = sol.substr(0,i) + '?' + sol.substr(i+1);
|
||||||
|
col = "#0f0";
|
||||||
|
++correct;
|
||||||
|
}
|
||||||
|
else if (sol.includes(c)) col = "#ff0";
|
||||||
|
if (col!=g.theme.bg) this.keyColors[c] = this.keyColors[c] || col;
|
||||||
|
else this.keyColors[c] = "#00f";
|
||||||
|
this.guessColors[this.nGuesses].push(col);
|
||||||
|
}
|
||||||
|
if (correct==5) {
|
||||||
|
E.showAlert("The word is\n"+this.word, "You won in "+(this.nGuesses+1)+" guesses!").then(function(){load();});
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (this.nGuesses==5) {
|
||||||
|
E.showAlert("The word was\n"+this.word, "You lost!").then(function(){load();});
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wordle = new Wordle("rnd");
|
||||||
|
layout = getKeyLayout("");
|
||||||
|
wordle.render(true);
|
||||||
|
|
||||||
|
Bangle.on('swipe', function (dir) {
|
||||||
|
if (dir==1 || dir==-1) {
|
||||||
|
g.clear();
|
||||||
|
if (gameState==0) {
|
||||||
|
layout.render();
|
||||||
|
gameState = 1;
|
||||||
|
}
|
||||||
|
else if (gameState==1) {
|
||||||
|
wordle.render();
|
||||||
|
gameState = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
{ "id": "bordle",
|
||||||
|
"name": "Bordle",
|
||||||
|
"shortName":"Bordle",
|
||||||
|
"icon": "app.png",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Bangle version of a popular word search game",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"tags": "game, text",
|
||||||
|
"storage": [
|
||||||
|
{"name":"bordle.app.js","url":"bordle.app.js"},
|
||||||
|
{"name":"wordlencr.txt","url":"wordlencr.txt"},
|
||||||
|
{"name":"bordle.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
0.01: Initial upload
|
0.01: Initial upload
|
||||||
|
0.2: Added scrollable calendar and swipe gestures
|
||||||
|
0.3: Configurable drag gestures
|
||||||
|
|
|
@ -3,19 +3,30 @@
|
||||||
This is my "Hello World". I first made this watchface almost 10 years ago for my original Pebble and Pebble Time and I missed this so much, that I had to write it for the BangleJS2.
|
This is my "Hello World". I first made this watchface almost 10 years ago for my original Pebble and Pebble Time and I missed this so much, that I had to write it for the BangleJS2.
|
||||||
I know that it seems redundant because there already **is** a *time&cal*-app, but it didn't fit my style.
|
I know that it seems redundant because there already **is** a *time&cal*-app, but it didn't fit my style.
|
||||||
|
|
||||||
- locked screen with only one minimal update/minute
|
|Screenshot|description|
|
||||||
- 
|
|:--:|:-|
|
||||||
- unlocked screen (twist?) with seconds
|
||locked: triggers only one minimal update/min|
|
||||||
- 
|
||unlocked: smaller clock, but with seconds|
|
||||||
|
||swipe up for big calendar, (up down to scroll, left/right to exit)|
|
||||||
|
|
||||||
## Configurable Features
|
## Configurable Features
|
||||||
- Number of calendar rows (weeks)
|
- Number of calendar rows (weeks)
|
||||||
- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included)
|
- Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included)
|
||||||
- Clock Mode (24h/12h). Doesn't have an am/pm indicator. It's only there because it was easy.
|
- Clock Mode (24h/12h). (No am/pm indicator)
|
||||||
- First day of the week
|
- First day of the week
|
||||||
- Red Saturday
|
- Red Saturday/Sunday
|
||||||
- Red Sunday
|
- Swipe/Drag gestures to launch features or apps.
|
||||||
|
|
||||||
|
## Auto detects your message/music apps:
|
||||||
|
- swiping down will search your files for an app with the string "message" in its filename and launch it. (configurable)
|
||||||
|
- swiping right will search your files for an app with the string "music" in its filename and launch it. (configurable)
|
||||||
|
|
||||||
## Feedback
|
## Feedback
|
||||||
The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings.
|
The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings.
|
||||||
So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues
|
So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues
|
||||||
|
|
||||||
|
## Planned features:
|
||||||
|
- Internal lightweight music control, because switching apps has a loading time.
|
||||||
|
- Clean up settings
|
||||||
|
- Maybe am/pm indicator for 12h-users
|
||||||
|
- Step count (optional)
|
||||||
|
|
|
@ -4,18 +4,120 @@ var s = Object.assign({
|
||||||
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
|
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
|
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
|
MODE24: true, //24h mode vs 12h mode
|
||||||
FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon
|
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
|
||||||
REDSUN: true, // Use red color for sunday?
|
REDSUN: true, // Use red color for sunday?
|
||||||
REDSAT: true, // Use red color for saturday?
|
REDSAT: true, // Use red color for saturday?
|
||||||
|
DRAGDOWN: "[AI:messg]",
|
||||||
|
DRAGRIGHT: "[AI:music]",
|
||||||
|
DRAGLEFT: "[ignore]",
|
||||||
|
DRAGUP: "[calend.]"
|
||||||
}, require('Storage').readJSON("clockcal.json", true) || {});
|
}, require('Storage').readJSON("clockcal.json", true) || {});
|
||||||
|
|
||||||
const h = g.getHeight();
|
const h = g.getHeight();
|
||||||
const w = g.getWidth();
|
const w = g.getWidth();
|
||||||
const CELL_W = w / 7;
|
const CELL_W = w / 7;
|
||||||
|
const CELL2_W = w / 8;//full calendar
|
||||||
const CELL_H = 15;
|
const CELL_H = 15;
|
||||||
const CAL_Y = h - s.CAL_ROWS * CELL_H;
|
const CAL_Y = h - s.CAL_ROWS * CELL_H;
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
var state = "watch";
|
||||||
|
var monthOffset = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Calendar features
|
||||||
|
*/
|
||||||
|
function drawFullCalendar(monthOffset) {
|
||||||
|
addMonths = function (_d, _am) {
|
||||||
|
var ay = 0, m = _d.getMonth(), y = _d.getFullYear();
|
||||||
|
while ((m + _am) > 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() {
|
function drawMinutes() {
|
||||||
if (DEBUG) console.log("|-->minutes");
|
if (DEBUG) console.log("|-->minutes");
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
|
@ -52,15 +154,17 @@ function drawSeconds() {
|
||||||
if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000);
|
if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawCalendar() {
|
function drawWatch() {
|
||||||
if (DEBUG) console.log("CALENDAR");
|
if (DEBUG) console.log("CALENDAR");
|
||||||
|
monthOffset = 0;
|
||||||
|
state = "watch";
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
g.reset();
|
g.reset();
|
||||||
g.setBgColor(0);
|
g.setBgColor(0);
|
||||||
g.clear();
|
g.clear();
|
||||||
drawMinutes();
|
drawMinutes();
|
||||||
if (!dimSeconds) drawSeconds();
|
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();
|
const today = d.getDate();
|
||||||
var rD = new Date(d.getTime());
|
var rD = new Date(d.getTime());
|
||||||
rD.setDate(rD.getDate() - dow);
|
rD.setDate(rD.getDate() - dow);
|
||||||
|
@ -91,7 +195,7 @@ function drawCalendar() {
|
||||||
var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1);
|
var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1);
|
||||||
if (DEBUG) console.log("Next Day:" + (nextday / 3600));
|
if (DEBUG) console.log("Next Day:" + (nextday / 3600));
|
||||||
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
|
if (typeof dayInterval !== "undefined") clearTimeout(dayInterval);
|
||||||
dayInterval = setTimeout(drawCalendar, nextday * 1000);
|
dayInterval = setTimeout(drawWatch, nextday * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BTevent() {
|
function BTevent() {
|
||||||
|
@ -102,18 +206,105 @@ function BTevent() {
|
||||||
setTimeout(function () { Bangle.buzz(interval); }, interval * 3);
|
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
|
//register events
|
||||||
Bangle.on('lock', locked => {
|
Bangle.on('lock', locked => {
|
||||||
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
|
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
|
||||||
dimSeconds = locked; //dim seconds if lock=on
|
dimSeconds = locked; //dim seconds if lock=on
|
||||||
drawCalendar();
|
drawWatch();
|
||||||
});
|
});
|
||||||
NRF.on('connect', BTevent);
|
NRF.on('connect', BTevent);
|
||||||
NRF.on('disconnect', BTevent);
|
NRF.on('disconnect', BTevent);
|
||||||
|
|
||||||
|
|
||||||
dimSeconds = Bangle.isLocked();
|
dimSeconds = Bangle.isLocked();
|
||||||
drawCalendar();
|
drawWatch();
|
||||||
|
|
||||||
Bangle.setUI("clock");
|
Bangle.setUI("clock");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "clockcal",
|
"id": "clockcal",
|
||||||
"name": "Clock & Calendar",
|
"name": "Clock & Calendar",
|
||||||
"version": "0.01",
|
"version": "0.3",
|
||||||
"description": "Clock with Calendar",
|
"description": "Clock with Calendar",
|
||||||
"readme":"README.md",
|
"readme":"README.md",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
|
|
After Width: | Height: | Size: 5.7 KiB |
|
@ -1,15 +1,21 @@
|
||||||
(function (back) {
|
(function (back) {
|
||||||
var FILE = "clockcal.json";
|
var FILE = "clockcal.json";
|
||||||
|
defaults={
|
||||||
settings = Object.assign({
|
|
||||||
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
|
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
|
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
|
MODE24: true, //24h mode vs 12h mode
|
||||||
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
|
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
|
||||||
REDSUN: true, // Use red color for sunday?
|
REDSUN: true, // Use red color for sunday?
|
||||||
REDSAT: true, // Use red color for saturday?
|
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() {
|
function writeSettings() {
|
||||||
require('Storage').writeJSON(FILE, settings);
|
require('Storage').writeJSON(FILE, settings);
|
||||||
|
@ -67,26 +73,55 @@
|
||||||
writeSettings();
|
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?': {
|
'Load deafauls?': {
|
||||||
value: 0,
|
value: 0,
|
||||||
min: 0, max: 1,
|
min: 0, max: 1,
|
||||||
format: v => ["No", "Yes"][v],
|
format: v => ["No", "Yes"][v],
|
||||||
onchange: v => {
|
onchange: v => {
|
||||||
if (v == 1) {
|
if (v == 1) {
|
||||||
settings = {
|
settings = 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.
|
|
||||||
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?
|
|
||||||
};
|
|
||||||
writeSettings();
|
writeSettings();
|
||||||
load()
|
load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
// Show the menu
|
// Show the menu
|
||||||
E.showMenu(menu);
|
E.showMenu(menu);
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
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.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.
|
||||||
|
|
|
@ -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 - 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.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];
|
let ticks = [0, 90, 180, 270];
|
||||||
ticks.forEach((item)=>{
|
ticks.forEach((item)=>{
|
||||||
let agl = item+180;
|
let agl = item+180;
|
||||||
|
@ -92,13 +92,13 @@ function draw_clock(){
|
||||||
let minute_agl = minute_angle(date);
|
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(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.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.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);
|
g.fillCircle(clock_center.x, clock_center.y, 3);
|
||||||
|
|
||||||
// draw minute ticks. Takes long time to draw!
|
// 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++){
|
for (var i=0; i<60; i++){
|
||||||
let agl = i*6+180;
|
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});
|
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});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "crowclk",
|
"id": "crowclk",
|
||||||
"name": "Crow Clock",
|
"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",
|
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
|
||||||
"icon": "crow_clock.png",
|
"icon": "crow_clock.png",
|
||||||
"screenshots": [{"url":"screenshot_crow.png"}],
|
"screenshots": [{"url":"screenshot_crow.png"}],
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
0.05: Add cadence sensor support
|
0.05: Add cadence sensor support
|
||||||
0.06: Now read wheel rev as well as cadence sensor
|
0.06: Now read wheel rev as well as cadence sensor
|
||||||
Improve connection code
|
Improve connection code
|
||||||
|
0.07: Make Bangle.js 2 compatible
|
||||||
|
|
|
@ -11,9 +11,9 @@ Currently the app displays the following data:
|
||||||
- total distance traveled
|
- total distance traveled
|
||||||
- an icon with the battery status of the remote sensor
|
- 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.
|
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 will attempt to reconnect to the sensor.
|
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 switches between the display for cycling speed and cadence.
|
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.
|
Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app.
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,11 @@ const SETTINGS_FILE = 'cscsensor.json';
|
||||||
const storage = require('Storage');
|
const storage = require('Storage');
|
||||||
const W = g.getWidth();
|
const W = g.getWidth();
|
||||||
const H = g.getHeight();
|
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 {
|
class CSCSensor {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -22,7 +27,6 @@ class CSCSensor {
|
||||||
this.speed = 0;
|
this.speed = 0;
|
||||||
this.maxSpeed = 0;
|
this.maxSpeed = 0;
|
||||||
this.lastSpeed = 0;
|
this.lastSpeed = 0;
|
||||||
this.qUpdateScreen = true;
|
|
||||||
this.lastRevsStart = -1;
|
this.lastRevsStart = -1;
|
||||||
this.qMetric = !require("locale").speed(1).toString().endsWith("mph");
|
this.qMetric = !require("locale").speed(1).toString().endsWith("mph");
|
||||||
this.speedUnit = this.qMetric ? "km/h" : "mph";
|
this.speedUnit = this.qMetric ? "km/h" : "mph";
|
||||||
|
@ -49,6 +53,7 @@ class CSCSensor {
|
||||||
toggleDisplayCadence() {
|
toggleDisplayCadence() {
|
||||||
this.showCadence = !this.showCadence;
|
this.showCadence = !this.showCadence;
|
||||||
this.screenInit = true;
|
this.screenInit = true;
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
setBatteryLevel(level) {
|
setBatteryLevel(level) {
|
||||||
|
@ -63,14 +68,16 @@ class CSCSensor {
|
||||||
}
|
}
|
||||||
|
|
||||||
drawBatteryIcon() {
|
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!=-1) {
|
||||||
if (this.batteryLevel<25) g.setColor(1, 0, 0);
|
if (this.batteryLevel<25) g.setColor(1, 0, 0);
|
||||||
else if (this.batteryLevel<50) g.setColor(1, 0.5, 0);
|
else if (this.batteryLevel<50) g.setColor(1, 0.5, 0);
|
||||||
else g.setColor(0, 1, 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() {
|
updateScreenRevs() {
|
||||||
|
@ -88,36 +95,36 @@ class CSCSensor {
|
||||||
for (var i=0; i<6; ++i) {
|
for (var i=0; i<6; ++i) {
|
||||||
if ((i&1)==0) g.setColor(0, 0, 0);
|
if ((i&1)==0) g.setColor(0, 0, 0);
|
||||||
else g.setColor(0x30cd);
|
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);
|
if ((i&1)==1) g.setColor(0);
|
||||||
else g.setColor(0x30cd);
|
else g.setColor(0x30cd);
|
||||||
g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
|
g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight);
|
||||||
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.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, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
|
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.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0);
|
||||||
g.drawString("Time:", 87, 66);
|
g.drawString("Time:", yCol1, yStart+rowHeight/2+0*rowHeight);
|
||||||
g.drawString("Speed:", 87, 98);
|
g.drawString("Speed:", yCol1, yStart+rowHeight/2+1*rowHeight);
|
||||||
g.drawString("Ave spd:", 87, 130);
|
g.drawString("Avg spd:", yCol1, yStart+rowHeight/2+2*rowHeight);
|
||||||
g.drawString("Max spd:", 87, 162);
|
g.drawString("Max spd:", yCol1, yStart+rowHeight/2+3*rowHeight);
|
||||||
g.drawString("Trip:", 87, 194);
|
g.drawString("Trip:", yCol1, yStart+rowHeight/2+4*rowHeight);
|
||||||
g.drawString("Total:", 87, 226);
|
g.drawString("Total:", yCol1, yStart+rowHeight/2+5*rowHeight);
|
||||||
this.drawBatteryIcon();
|
this.drawBatteryIcon();
|
||||||
this.screenInit = false;
|
this.screenInit = false;
|
||||||
}
|
}
|
||||||
g.setFontAlign(-1, 0, 0).setFontVector(26);
|
g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue);
|
||||||
g.setColor(0x30cd).fillRect(88, 49, 238, 79);
|
g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*0, 238, 47+1*rowHeight);
|
||||||
g.setColor(0xffff).drawString(dmins+":"+dsecs, 92, 66);
|
g.setColor(0xffff).drawString(dmins+":"+dsecs, yCol1+5, 50+rowHeight/2+0*rowHeight);
|
||||||
g.setColor(0).fillRect(88, 81, 238, 111);
|
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight);
|
||||||
g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, 92, 98);
|
g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, yCol1+5, 50+rowHeight/2+1*rowHeight);
|
||||||
g.setColor(0x30cd).fillRect(88, 113, 238, 143);
|
g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*2, 238, 47+3*rowHeight);
|
||||||
g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, 92, 130);
|
g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+2*rowHeight);
|
||||||
g.setColor(0).fillRect(88, 145, 238, 175);
|
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*3, 238, 47+4*rowHeight);
|
||||||
g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, 92, 162);
|
g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+3*rowHeight);
|
||||||
g.setColor(0x30cd).fillRect(88, 177, 238, 207);
|
g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*4, 238, 47+5*rowHeight);
|
||||||
g.setColor(0xffff).drawString(ddist + " " + this.distUnit, 92, 194);
|
g.setColor(0xffff).drawString(ddist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+4*rowHeight);
|
||||||
g.setColor(0).fillRect(88, 209, 238, 238);
|
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*5, 238, 47+6*rowHeight);
|
||||||
g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226);
|
g.setColor(0xffff).drawString(tdist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+5*rowHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScreenCadence() {
|
updateScreenCadence() {
|
||||||
|
@ -125,21 +132,21 @@ class CSCSensor {
|
||||||
for (var i=0; i<2; ++i) {
|
for (var i=0; i<2; ++i) {
|
||||||
if ((i&1)==0) g.setColor(0, 0, 0);
|
if ((i&1)==0) g.setColor(0, 0, 0);
|
||||||
else g.setColor(0x30cd);
|
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);
|
if ((i&1)==1) g.setColor(0);
|
||||||
else g.setColor(0x30cd);
|
else g.setColor(0x30cd);
|
||||||
g.fillRect(87, 48+i*32, 239, 48+(i+1)*32);
|
g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight);
|
||||||
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.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, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80);
|
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.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0);
|
||||||
g.drawString("Cadence:", 87, 98);
|
g.drawString("Cadence:", yCol1, yStart+rowHeight/2+1*rowHeight);
|
||||||
this.drawBatteryIcon();
|
this.drawBatteryIcon();
|
||||||
this.screenInit = false;
|
this.screenInit = false;
|
||||||
}
|
}
|
||||||
g.setFontAlign(-1, 0, 0).setFontVector(26);
|
g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue);
|
||||||
g.setColor(0).fillRect(88, 81, 238, 111);
|
g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight);
|
||||||
g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98);
|
g.setColor(0xffff).drawString(Math.round(this.cadence), yCol1+5, 50+rowHeight/2+1*rowHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScreen() {
|
updateScreen() {
|
||||||
|
@ -163,7 +170,7 @@ class CSCSensor {
|
||||||
}
|
}
|
||||||
this.lastCrankRevs = crankRevs;
|
this.lastCrankRevs = crankRevs;
|
||||||
this.lastCrankTime = crankTime;
|
this.lastCrankTime = crankTime;
|
||||||
}
|
} else {
|
||||||
// wheel revolution
|
// wheel revolution
|
||||||
var wheelRevs = event.target.value.getUint32(1, true);
|
var wheelRevs = event.target.value.getUint32(1, true);
|
||||||
var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
|
var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0);
|
||||||
|
@ -189,8 +196,7 @@ class CSCSensor {
|
||||||
this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
|
this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT;
|
||||||
this.speedFailed = 0;
|
this.speedFailed = 0;
|
||||||
this.movingTime += dT;
|
this.movingTime += dT;
|
||||||
}
|
} else if (!this.showCadence) {
|
||||||
else {
|
|
||||||
this.speedFailed++;
|
this.speedFailed++;
|
||||||
qChanged = false;
|
qChanged = false;
|
||||||
if (this.speedFailed>3) {
|
if (this.speedFailed>3) {
|
||||||
|
@ -201,7 +207,8 @@ class CSCSensor {
|
||||||
this.lastSpeed = 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 (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
|
NRF.on('disconnect', connection_setup); // restart if disconnected
|
||||||
Bangle.setUI("updown", d=>{
|
Bangle.setUI("updown", d=>{
|
||||||
if (d<0) { mySensor.reset(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); }
|
if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
|
||||||
if (d==0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
|
else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }
|
||||||
if (d>0) { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); }
|
else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); }
|
||||||
});
|
});
|
||||||
|
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "cscsensor",
|
"id": "cscsensor",
|
||||||
"name": "Cycling speed sensor",
|
"name": "Cycling speed sensor",
|
||||||
"shortName": "CSCSensor",
|
"shortName": "CSCSensor",
|
||||||
"version": "0.06",
|
"version": "0.07",
|
||||||
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
|
"description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch",
|
||||||
"icon": "icons8-cycling-48.png",
|
"icon": "icons8-cycling-48.png",
|
||||||
"tags": "outdoors,exercise,ble,bluetooth",
|
"tags": "outdoors,exercise,ble,bluetooth",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"cscsensor.app.js","url":"cscsensor.app.js"},
|
{"name":"cscsensor.app.js","url":"cscsensor.app.js"},
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Initial version
|
|
@ -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.
|
|
@ -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",`<FILE CONTENT HERE>`);
|
||||||
|
*/
|
||||||
|
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;
|
|
@ -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;
|
|
@ -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();
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
||||||
|
})
|
|
@ -2,3 +2,6 @@
|
||||||
0.02: added emulator capability and display of widgets
|
0.02: added emulator capability and display of widgets
|
||||||
0.03: bug of advancing time fixed; doztime now correct within ca. 1 second
|
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.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
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
// Positioning values for graphics buffers
|
// Positioning values for graphics buffers
|
||||||
const g_height = 80; // total graphics height
|
const g_height = 80; // total graphics height
|
||||||
const g_x_off = 0; // position from left was 16, then 8 here
|
const g_x_off = 0; // position from left
|
||||||
const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240
|
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_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 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_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
|
// Other vars
|
||||||
const A1 = [30,30,30,30,31,31,31,31,31,31,30,30];
|
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 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 B2 = [30,30,30,30,31,31,31,31,31,30,30,30];
|
||||||
const timeColour = "#ffffff";
|
const timeColour = "#ffffff";
|
||||||
const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff
|
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 ft w 32, 32-g, step 20
|
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 ft w 32, 62-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
|
||||||
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 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 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 baseYear = 11584;
|
||||||
const baseDate = Date(2020,11,21); // month values run from 0 to 11
|
const baseDate = Date(2020,11,21); // month values run from 0 to 11
|
||||||
|
@ -59,11 +59,8 @@ g.flip = function()
|
||||||
}, g_x_off, g_y_off + g_y_off_t);
|
}, g_x_off, g_y_off + g_y_off_t);
|
||||||
};
|
};
|
||||||
|
|
||||||
setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1
|
setWatch(function(){ modeTime(); }, BTN, {repeat:true} );
|
||||||
setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2
|
setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" });
|
||||||
//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true});
|
|
||||||
//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true});
|
|
||||||
//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true});
|
|
||||||
|
|
||||||
Bangle.on('touch', function(button, xy) { //from Gordon Williams
|
Bangle.on('touch', function(button, xy) { //from Gordon Williams
|
||||||
if (button==1) toggleTimeDigits();
|
if (button==1) toggleTimeDigits();
|
||||||
|
@ -134,8 +131,8 @@ function writeDozTime(text,def){
|
||||||
g_t.clear();
|
g_t.clear();
|
||||||
g_t.setFont("Vector",def.size);
|
g_t.setFont("Vector",def.size);
|
||||||
for(let i in text){
|
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
|
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); } //+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); }
|
||||||
else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
|
else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
|
||||||
x = x+def.step[0];
|
x = x+def.step[0];
|
||||||
y = y+def.step[1];
|
y = y+def.step[1];
|
||||||
|
@ -150,18 +147,25 @@ function writeDozDate(text,def,colour){
|
||||||
g_d.clear();
|
g_d.clear();
|
||||||
g_d.setFont("Vector",def.size);
|
g_d.setFont("Vector",def.size);
|
||||||
for(let i in text){
|
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
|
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); } //+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); }
|
||||||
else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
|
else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
|
||||||
x = x+def.step[0];
|
x = x+def.step[0];
|
||||||
y = y+def.step[1];
|
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
|
// Functions for time mode
|
||||||
function drawTime()
|
function drawTime()
|
||||||
{
|
{
|
||||||
let dt = new Date();
|
let dt = adjustedNow();
|
||||||
let date = "";
|
let date = "";
|
||||||
let timeDef;
|
let timeDef;
|
||||||
let x = 0;
|
let x = 0;
|
||||||
|
@ -204,41 +208,17 @@ function drawTime()
|
||||||
}
|
}
|
||||||
function modeTime()
|
function modeTime()
|
||||||
{
|
{
|
||||||
timeActiveUntil = new Date();
|
timeActiveUntil = adjustedNow();
|
||||||
timeActiveUntil.setDate(timeActiveUntil.getDate());
|
timeActiveUntil.setDate(timeActiveUntil.getDate());
|
||||||
timeActiveUntil.setSeconds(timeActiveUntil.getSeconds()+86400);
|
timeActiveUntil.setSeconds(timeActiveUntil.getSeconds()+604800);
|
||||||
if (typeof drawtime_timeout !== 'undefined')
|
if (typeof drawtime_timeout !== 'undefined')
|
||||||
{
|
{
|
||||||
clearTimeout(drawtime_timeout);
|
clearTimeout(drawtime_timeout);
|
||||||
}
|
}
|
||||||
drawTime();
|
drawTime();
|
||||||
}
|
}
|
||||||
Bangle.loadWidgets();
|
|
||||||
Bangle.drawWidgets();
|
|
||||||
|
|
||||||
// Functions for weather mode - TODO
|
|
||||||
// function drawWeather() {}
|
|
||||||
// function modeWeather() {}
|
|
||||||
|
|
||||||
// Start time on twist
|
// Start time on twist
|
||||||
Bangle.on('twist',function() {
|
Bangle.on('twist',function() {
|
||||||
modeTime();
|
modeTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Time fix with GPS
|
|
||||||
function fixTime() {
|
|
||||||
Bangle.on("GPS",function cb(g) {
|
|
||||||
Bangle.setGPSPower(0,"time");
|
|
||||||
Bangle.removeListener("GPS",cb);
|
|
||||||
if (!g.time || (g.time.getFullYear()<2000) ||
|
|
||||||
(g.time.getFullYear()>2200)) {
|
|
||||||
} 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);
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "doztime",
|
"id": "doztime",
|
||||||
"name": "Dozenal Time",
|
"name": "Dozenal Time",
|
||||||
"shortName": "Dozenal Time",
|
"shortName": "Dozenal Time",
|
||||||
"version": "0.04",
|
"version": "0.05",
|
||||||
"description": "A dozenal Holocene calendar and dozenal diurnal clock",
|
"description": "A dozenal Holocene calendar and dozenal diurnal clock",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
|
|
|
@ -8,3 +8,4 @@
|
||||||
0.08: Optimize line wrapping for Bangle 2
|
0.08: Optimize line wrapping for Bangle 2
|
||||||
0.09: fix the trasparent widget bar if there are no widgets 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.10: added "one click exit" setting for Bangle 2
|
||||||
|
0.11: Fix bangle.js 1 white icons not displaying
|
||||||
|
|
|
@ -48,6 +48,7 @@ function draw_icon(p,n,selected) {
|
||||||
var x = (n%3)*80;
|
var x = (n%3)*80;
|
||||||
var y = n>2?130:40;
|
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);
|
(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.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);
|
g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1);
|
||||||
var txt = apps[p*6+n].name.split(" ");
|
var txt = apps[p*6+n].name.split(" ");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "dtlaunch",
|
"id": "dtlaunch",
|
||||||
"name": "Desktop Launcher",
|
"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.",
|
"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"}],
|
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
|
|
|
@ -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
|
|
@ -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:
|
||||||
|

|
|
@ -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="))
|
|
@ -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!"));
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
require("Storage").write("timer.info",{
|
||||||
|
"id":"game1024",
|
||||||
|
"name":"1024 Game",
|
||||||
|
"src":"game1024.app.js",
|
||||||
|
"icon":"game1024.img"
|
||||||
|
});
|
|
@ -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}
|
After Width: | Height: | Size: 582 B |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 5.9 KiB |
|
@ -0,0 +1 @@
|
||||||
|
0.01: Added Source Code
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Geek Squad Appointment Timer
|
||||||
|
|
||||||
|
An app dedicated to setting a 20 minute timer for Geek Squad Appointments.
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))
|
|
@ -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);
|
After Width: | Height: | Size: 929 B |
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -1 +1,2 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
|
0.02: Make Bangle.js 2 compatible
|
||||||
|
|
|
@ -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 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 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) {
|
const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
|
||||||
try {
|
try {
|
||||||
|
@ -20,31 +99,17 @@ const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
|
||||||
|
|
||||||
function drawApp() {
|
function drawApp() {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.setFont("6x8",2);
|
Bangle.loadWidgets();
|
||||||
g.setFontAlign(0,0);
|
Bangle.drawWidgets();
|
||||||
g.drawString("Joystick", 120, 120);
|
layout.render();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const btn1 = BTN1.read();
|
const btn1 = BTN1 ? BTN1.read() : 0;
|
||||||
const btn2 = BTN2.read();
|
const btn2 = !BANGLEJS2 ? BTN2.read() : touchBtn2;
|
||||||
const btn3 = BTN3.read();
|
const btn3 = !BANGLEJS2 ? BTN3.read() : touchBtn3;
|
||||||
const btn4 = BTN4.read();
|
const btn4 = !BANGLEJS2 ? BTN4.read() : touchBtn4;
|
||||||
const btn5 = BTN5.read();
|
const btn5 = !BANGLEJS2 ? BTN5.read() : touchBtn5;
|
||||||
const acc = Bangle.getAccel();
|
const acc = Bangle.getAccel();
|
||||||
var x = acc.x*-127;
|
var x = acc.x*-127;
|
||||||
var y = acc.y*-127;
|
var y = acc.y*-127;
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "hidjoystick",
|
"id": "hidjoystick",
|
||||||
"name": "Bluetooth Joystick",
|
"name": "Bluetooth Joystick",
|
||||||
"shortName": "Joystick",
|
"shortName": "Joystick",
|
||||||
"version": "0.01",
|
"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.",
|
"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",
|
"icon": "app.png",
|
||||||
"tags": "bluetooth",
|
"tags": "bluetooth",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"hidjoystick.app.js","url":"app.js"},
|
{"name":"hidjoystick.app.js","url":"app.js"},
|
||||||
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
|
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
0.06: Add widgets
|
0.06: Add widgets
|
||||||
0.07: Update scaling for new firmware
|
0.07: Update scaling for new firmware
|
||||||
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
||||||
|
0.09: Grey out BPM until confidence is over 50%
|
||||||
|
|
|
@ -35,9 +35,9 @@ function onHRM(h) {
|
||||||
g.clearRect(0,24,g.getWidth(),80);
|
g.clearRect(0,24,g.getWidth(),80);
|
||||||
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
|
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
|
||||||
var str = hrmInfo.bpm;
|
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;
|
px += g.stringWidth(str)/2;
|
||||||
g.setFont("6x8");
|
g.setFont("6x8").setColor(g.theme.fg);
|
||||||
g.drawString("BPM",px+15,45);
|
g.drawString("BPM",px+15,45);
|
||||||
}
|
}
|
||||||
Bangle.on('HRM', onHRM);
|
Bangle.on('HRM', onHRM);
|
||||||
|
@ -101,4 +101,3 @@ function readHRM() {
|
||||||
lastHrmPt = [hrmOffset, y];
|
lastHrmPt = [hrmOffset, y];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "hrm",
|
"id": "hrm",
|
||||||
"name": "Heart Rate Monitor",
|
"name": "Heart Rate Monitor",
|
||||||
"version": "0.08",
|
"version": "0.09",
|
||||||
"description": "Measure your heart rate and see live sensor data",
|
"description": "Measure your heart rate and see live sensor data",
|
||||||
"icon": "heartrate.png",
|
"icon": "heartrate.png",
|
||||||
"tags": "health",
|
"tags": "health",
|
||||||
|
|
|
@ -7,7 +7,7 @@ screen. Very useful if combined with pattern launcher ;)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
|
@ -16,3 +16,4 @@
|
||||||
0.16: Improved stability. Wind can now be shown.
|
0.16: Improved stability. Wind can now be shown.
|
||||||
0.17: Settings for mph/kph and other minor improvements.
|
0.17: Settings for mph/kph and other minor improvements.
|
||||||
0.18: Fullscreen mode can now be enabled or disabled in the settings.
|
0.18: Fullscreen mode can now be enabled or disabled in the settings.
|
||||||
|
0.19: Alarms can not go bigger than 100.
|
||||||
|
|
|
@ -626,7 +626,7 @@ Bangle.on('charging',function(charging) {
|
||||||
|
|
||||||
|
|
||||||
function increaseAlarm(){
|
function increaseAlarm(){
|
||||||
if(isAlarmEnabled()){
|
if(isAlarmEnabled() && getAlarmMinutes() < 95){
|
||||||
settings.alarm += 5;
|
settings.alarm += 5;
|
||||||
} else {
|
} else {
|
||||||
settings.alarm = getCurrentTimeInMinutes() + 5;
|
settings.alarm = getCurrentTimeInMinutes() + 5;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "LCARS Clock",
|
"name": "LCARS Clock",
|
||||||
"shortName":"LCARS",
|
"shortName":"LCARS",
|
||||||
"icon": "lcars.png",
|
"icon": "lcars.png",
|
||||||
"version":"0.18",
|
"version":"0.19",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Add the option to enable touching the widget only on clock and settings.
|
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)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "lightswitch",
|
"id": "lightswitch",
|
||||||
"name": "Light Switch Widget",
|
"name": "Light Switch Widget",
|
||||||
"shortName": "Light Switch",
|
"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.",
|
"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",
|
"icon": "images/app.png",
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
|
|
|
@ -44,9 +44,11 @@
|
||||||
// return entry for string value
|
// return entry for string value
|
||||||
return {
|
return {
|
||||||
value: entry.value.indexOf(settings[key]),
|
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],
|
format: v => entry.title ? entry.title[v] : entry.value[v],
|
||||||
onchange: function(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);
|
writeSetting(key, entry.value[v], entry.drawWidgets);
|
||||||
if (entry.exec) entry.exec(entry.value[v]);
|
if (entry.exec) entry.exec(entry.value[v]);
|
||||||
}
|
}
|
||||||
|
@ -57,8 +59,10 @@
|
||||||
value: settings[key] * entry.factor,
|
value: settings[key] * entry.factor,
|
||||||
step: entry.step,
|
step: entry.step,
|
||||||
format: v => v > 0 ? v + entry.unit : "off",
|
format: v => v > 0 ? v + entry.unit : "off",
|
||||||
|
min : entry.min,
|
||||||
|
max : entry.max,
|
||||||
|
wrap : true,
|
||||||
onchange: function(v) {
|
onchange: function(v) {
|
||||||
this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v;
|
|
||||||
writeSetting(key, v / entry.factor, entry.drawWidgets);
|
writeSetting(key, v / entry.factor, entry.drawWidgets);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -133,16 +137,16 @@
|
||||||
title: "Light Switch"
|
title: "Light Switch"
|
||||||
},
|
},
|
||||||
"< Back": () => back(),
|
"< Back": () => back(),
|
||||||
"-- Widget --------": 0,
|
"-- Widget": 0,
|
||||||
"Bulb col": getEntry("colors"),
|
"Bulb col": getEntry("colors"),
|
||||||
"Image": getEntry("image"),
|
"Image": getEntry("image"),
|
||||||
"-- Control -------": 0,
|
"-- Control": 0,
|
||||||
"Touch": getEntry("touchOn"),
|
"Touch": getEntry("touchOn"),
|
||||||
"Drag Delay": getEntry("dragDelay"),
|
"Drag Delay": getEntry("dragDelay"),
|
||||||
"Min Value": getEntry("minValue"),
|
"Min Value": getEntry("minValue"),
|
||||||
"-- Unlock --------": 0,
|
"-- Unlock": 0,
|
||||||
"TapSide": getEntry("unlockSide"),
|
"TapSide": getEntry("unlockSide"),
|
||||||
"-- Flash ---------": 0,
|
"-- Flash": 0,
|
||||||
"TapSide ": getEntry("tapSide"),
|
"TapSide ": getEntry("tapSide"),
|
||||||
"Tap": getEntry("tapOn"),
|
"Tap": getEntry("tapOn"),
|
||||||
"Timeout": getEntry("tOut"),
|
"Timeout": getEntry("tOut"),
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
0.01: First release
|
0.01: First release
|
||||||
|
0.02: Make sure to reset turns
|
||||||
|
|