Merge branch 'espruino:master' into master
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||
0.05: Add support for bulk importing and exporting tokens
|
||||
0.06: Add spaces to codes for improved readability (thanks @BartS23)
|
||||
0.07: Bangle 2: Improve drag responsiveness and exit on button press
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
const tokenextraheight = 16;
|
||||
var tokendigitsheight = 30;
|
||||
var tokenheight = tokendigitsheight + tokenextraheight;
|
||||
const COUNTER_TRIANGLE_SIZE = 10;
|
||||
const TOKEN_EXTRA_HEIGHT = 16;
|
||||
var TOKEN_DIGITS_HEIGHT = 30;
|
||||
var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT;
|
||||
const PROGRESSBAR_HEIGHT = 3;
|
||||
const IDLE_REPEATS = 1; // when idle, the number of extra timed periods to show before hiding
|
||||
const SETTINGS = "authentiwatch.json";
|
||||
// Hash functions
|
||||
const crypto = require("crypto");
|
||||
const algos = {
|
||||
|
@ -8,33 +12,24 @@ const algos = {
|
|||
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
|
||||
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
|
||||
};
|
||||
const calculating = "Calculating";
|
||||
const notokens = "No tokens";
|
||||
const notsupported = "Not supported";
|
||||
const CALCULATING = /*LANG*/"Calculating";
|
||||
const NO_TOKENS = /*LANG*/"No tokens";
|
||||
const NOT_SUPPORTED = /*LANG*/"Not supported";
|
||||
|
||||
// sample settings:
|
||||
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
|
||||
var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}};
|
||||
var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[], misc:{}};
|
||||
if (settings.data ) tokens = settings.data ; /* v0.02 settings */
|
||||
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
|
||||
|
||||
// QR Code Text
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret}
|
||||
//
|
||||
// ${algorithm} : one of SHA1 / SHA256 / SHA512
|
||||
// ${digits} : one of 6 / 8
|
||||
// ${period} : one of 30 / 60
|
||||
// ${url} : a domain name "example.com"
|
||||
// ${secret} : the seed code
|
||||
|
||||
function b32decode(seedstr) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, retstr = "";
|
||||
for (i in seedstr) {
|
||||
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
// RFC4648 Base16/32/64 Data Encodings
|
||||
let buf = 0, bitcount = 0, retstr = "";
|
||||
for (let c of seedstr.toUpperCase()) {
|
||||
if (c == '0') c = 'O';
|
||||
if (c == '1') c = 'I';
|
||||
if (c == '8') c = 'B';
|
||||
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
|
@ -46,195 +41,127 @@ function b32decode(seedstr) {
|
|||
}
|
||||
}
|
||||
}
|
||||
var retbuf = new Uint8Array(retstr.length);
|
||||
for (i in retstr) {
|
||||
let retbuf = new Uint8Array(retstr.length);
|
||||
for (let i in retstr) {
|
||||
retbuf[i] = retstr.charCodeAt(i);
|
||||
}
|
||||
return retbuf;
|
||||
}
|
||||
function do_hmac(key, message, algo) {
|
||||
var a = algos[algo];
|
||||
// RFC2104
|
||||
|
||||
function hmac(key, message, algo) {
|
||||
let a = algos[algo.toUpperCase()];
|
||||
// RFC2104 HMAC
|
||||
if (key.length > a.blksz) {
|
||||
key = a.sha(key);
|
||||
}
|
||||
var istr = new Uint8Array(a.blksz + message.length);
|
||||
var ostr = new Uint8Array(a.blksz + a.retsz);
|
||||
for (var i = 0; i < a.blksz; ++i) {
|
||||
var c = (i < key.length) ? key[i] : 0;
|
||||
let istr = new Uint8Array(a.blksz + message.length);
|
||||
let ostr = new Uint8Array(a.blksz + a.retsz);
|
||||
for (let i = 0; i < a.blksz; ++i) {
|
||||
let c = (i < key.length) ? key[i] : 0;
|
||||
istr[i] = c ^ 0x36;
|
||||
ostr[i] = c ^ 0x5C;
|
||||
}
|
||||
istr.set(message, a.blksz);
|
||||
ostr.set(a.sha(istr), a.blksz);
|
||||
var ret = a.sha(ostr);
|
||||
// RFC4226 dynamic truncation
|
||||
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||
let ret = a.sha(ostr);
|
||||
// RFC4226 HOTP (dynamic truncation)
|
||||
let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||
return v.getUint32(0) & 0x7FFFFFFF;
|
||||
}
|
||||
function hotp(d, token, dohmac) {
|
||||
var tick;
|
||||
|
||||
function formatOtp(otp, digits) {
|
||||
// add 0 padding
|
||||
let ret = "" + otp % Math.pow(10, digits);
|
||||
while (ret.length < digits) {
|
||||
ret = "0" + ret;
|
||||
}
|
||||
// add a space after every 3rd or 4th digit
|
||||
let re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : ".";
|
||||
return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
||||
}
|
||||
|
||||
function hotp(token) {
|
||||
let d = Date.now();
|
||||
let tick, next;
|
||||
if (token.period > 0) {
|
||||
// RFC6238 - timed
|
||||
var seconds = Math.floor(d.getTime() / 1000);
|
||||
tick = Math.floor(seconds / token.period);
|
||||
tick = Math.floor(Math.floor(d / 1000) / token.period);
|
||||
next = (tick + 1) * token.period * 1000;
|
||||
} else {
|
||||
// RFC4226 - counter
|
||||
tick = -token.period;
|
||||
next = d + 30000;
|
||||
}
|
||||
var msg = new Uint8Array(8);
|
||||
var v = new DataView(msg.buffer);
|
||||
let msg = new Uint8Array(8);
|
||||
let v = new DataView(msg.buffer);
|
||||
v.setUint32(0, tick >> 16 >> 16);
|
||||
v.setUint32(4, tick & 0xFFFFFFFF);
|
||||
var ret = calculating;
|
||||
if (dohmac) {
|
||||
try {
|
||||
var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase());
|
||||
ret = "" + hash % Math.pow(10, token.digits);
|
||||
while (ret.length < token.digits) {
|
||||
ret = "0" + ret;
|
||||
}
|
||||
// add a space after every 3rd or 4th digit
|
||||
var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : ".";
|
||||
ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
||||
} catch(err) {
|
||||
ret = notsupported;
|
||||
}
|
||||
let ret;
|
||||
try {
|
||||
ret = hmac(b32decode(token.secret), msg, token.algorithm);
|
||||
ret = formatOtp(ret, token.digits);
|
||||
} catch(err) {
|
||||
ret = NOT_SUPPORTED;
|
||||
}
|
||||
return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)};
|
||||
return {hotp:ret, next:next};
|
||||
}
|
||||
|
||||
// Tokens are displayed in three states:
|
||||
// 1. Unselected (state.id<0)
|
||||
// 2. Selected, inactive (no code) (state.id>=0,state.hotp.hotp=="")
|
||||
// 3. Selected, active (code showing) (state.id>=0,state.hotp.hotp!="")
|
||||
var fontszCache = {};
|
||||
var state = {
|
||||
listy: 0,
|
||||
prevcur:0,
|
||||
curtoken:-1,
|
||||
nextTime:0,
|
||||
otp:"",
|
||||
rem:0,
|
||||
hide:0
|
||||
listy:0, // list scroll position
|
||||
id:-1, // current token ID
|
||||
hotp:{hotp:"",next:0}
|
||||
};
|
||||
|
||||
function drawToken(id, r) {
|
||||
var x1 = r.x;
|
||||
var y1 = r.y;
|
||||
var x2 = r.x + r.w - 1;
|
||||
var y2 = r.y + r.h - 1;
|
||||
var adj, lbl, sz;
|
||||
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
|
||||
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
|
||||
lbl = tokens[id].label.substr(0, 10);
|
||||
if (id == state.curtoken) {
|
||||
// current token
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH)
|
||||
.setFont("Vector", tokenextraheight)
|
||||
// center just below top line
|
||||
.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
g.setColor(g.theme.fg)
|
||||
.setBgColor(g.theme.bg);
|
||||
sz = tokendigitsheight;
|
||||
function sizeFont(id, txt, w) {
|
||||
let sz = fontszCache[id];
|
||||
if (!sz) {
|
||||
sz = TOKEN_DIGITS_HEIGHT;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(lbl) > r.w);
|
||||
// center in box
|
||||
g.setFontAlign(0, 0, 0);
|
||||
adj = (y1 + y2) / 2;
|
||||
} while (g.stringWidth(txt) > w);
|
||||
fontszCache[id] = ++sz;
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2)
|
||||
.drawString(lbl, (x1 + x2) / 2, adj, false);
|
||||
if (id == state.curtoken) {
|
||||
if (tokens[id].period > 0) {
|
||||
// timed - draw progress bar
|
||||
let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period);
|
||||
g.fillRect(x1, y2 - 4, xr, y2 - 1);
|
||||
adj = 0;
|
||||
} else {
|
||||
// counter - draw triangle as swipe hint
|
||||
let yc = (y1 + y2) / 2;
|
||||
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
|
||||
adj = 12;
|
||||
}
|
||||
// digits just below label
|
||||
sz = tokendigitsheight;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(state.otp) > (r.w - adj));
|
||||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
|
||||
}
|
||||
// shaded lines top and bottom
|
||||
g.setColor(0.5, 0.5, 0.5)
|
||||
.drawLine(x1, y1, x2, y1)
|
||||
.drawLine(x1, y2, x2, y2)
|
||||
.setClipRect(0, 0, g.getWidth(), g.getHeight());
|
||||
g.setFont("Vector", sz);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
var timerfn = exitApp;
|
||||
var timerdly = 10000;
|
||||
var d = new Date();
|
||||
if (state.curtoken != -1) {
|
||||
var t = tokens[state.curtoken];
|
||||
if (state.otp == calculating) {
|
||||
state.otp = hotp(d, t, true).hotp;
|
||||
}
|
||||
if (d.getTime() > state.nextTime) {
|
||||
if (state.hide == 0) {
|
||||
// auto-hide the current token
|
||||
if (state.curtoken != -1) {
|
||||
state.prevcur = state.curtoken;
|
||||
state.curtoken = -1;
|
||||
tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
|
||||
half = n => Math.floor(n / 2);
|
||||
|
||||
function timerCalc() {
|
||||
let timerfn = exitApp;
|
||||
let timerdly = 10000;
|
||||
if (state.id >= 0 && state.hotp.hotp != "") {
|
||||
if (tokens[state.id].period > 0) {
|
||||
// timed HOTP
|
||||
if (state.hotp.next < Date.now()) {
|
||||
if (state.cnt > 0) {
|
||||
state.cnt--;
|
||||
state.hotp = hotp(tokens[state.id]);
|
||||
} else {
|
||||
state.hotp.hotp = "";
|
||||
}
|
||||
state.nextTime = 0;
|
||||
timerdly = 1;
|
||||
timerfn = updateCurrentToken;
|
||||
} else {
|
||||
// time to generate a new token
|
||||
var r = hotp(d, t, state.otp != "");
|
||||
state.nextTime = r.next;
|
||||
state.otp = r.hotp;
|
||||
if (t.period <= 0) {
|
||||
state.hide = 1;
|
||||
}
|
||||
state.hide--;
|
||||
}
|
||||
}
|
||||
state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000));
|
||||
}
|
||||
if (tokens.length > 0) {
|
||||
var drewcur = false;
|
||||
var id = Math.floor(state.listy / tokenheight);
|
||||
var y = id * tokenheight + Bangle.appRect.y - state.listy;
|
||||
while (id < tokens.length && y < Bangle.appRect.y2) {
|
||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight});
|
||||
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
||||
drewcur = true;
|
||||
}
|
||||
id += 1;
|
||||
y += tokenheight;
|
||||
}
|
||||
if (drewcur) {
|
||||
// the current token has been drawn - schedule a redraw
|
||||
if (tokens[state.curtoken].period > 0) {
|
||||
timerdly = (state.otp == calculating) ? 1 : 1000; // timed
|
||||
} else {
|
||||
timerdly = state.nexttime - d.getTime(); // counter
|
||||
}
|
||||
timerfn = draw;
|
||||
if (tokens[state.curtoken].period <= 0) {
|
||||
state.hide = 0;
|
||||
timerdly = 1000;
|
||||
timerfn = updateProgressBar;
|
||||
}
|
||||
} else {
|
||||
// de-select the current token if it is scrolled out of view
|
||||
if (state.curtoken != -1) {
|
||||
state.prevcur = state.curtoken;
|
||||
state.curtoken = -1;
|
||||
// counter HOTP
|
||||
if (state.cnt > 0) {
|
||||
state.cnt--;
|
||||
timerdly = 30000;
|
||||
} else {
|
||||
state.hotp.hotp = "";
|
||||
timerdly = 1;
|
||||
}
|
||||
state.nexttime = 0;
|
||||
timerfn = updateCurrentToken;
|
||||
}
|
||||
} else {
|
||||
g.setFont("Vector", tokendigitsheight)
|
||||
.setFontAlign(0, 0, 0)
|
||||
.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
}
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
|
@ -242,97 +169,236 @@ function draw() {
|
|||
state.drawtimer = setTimeout(timerfn, timerdly);
|
||||
}
|
||||
|
||||
function onTouch(zone, e) {
|
||||
if (e) {
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight);
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
if (id != -1) {
|
||||
var y = id * tokenheight - state.listy;
|
||||
if (y < 0) {
|
||||
state.listy += y;
|
||||
y = 0;
|
||||
function updateCurrentToken() {
|
||||
drawToken(state.id);
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
drawProgressBar();
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function drawProgressBar() {
|
||||
let id = state.id;
|
||||
if (id >= 0 && tokens[id].period > 0) {
|
||||
let rem = Math.min(tokens[id].period, Math.floor((state.hotp.next - Date.now()) / 1000));
|
||||
if (rem >= 0) {
|
||||
let y1 = tokenY(id);
|
||||
let y2 = y1 + TOKEN_HEIGHT - 1;
|
||||
if (y2 >= AR.y && y1 <= AR.y2) {
|
||||
// token visible
|
||||
y1 = y2 - PROGRESSBAR_HEIGHT;
|
||||
if (y1 <= AR.y2)
|
||||
{
|
||||
// progress bar visible
|
||||
y2 = Math.min(y2, AR.y2);
|
||||
let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x;
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH)
|
||||
.fillRect(AR.x, y1, xr, y2)
|
||||
.clearRect(xr + 1, y1, AR.x2, y2);
|
||||
}
|
||||
y += tokenheight;
|
||||
if (y > Bangle.appRect.h) {
|
||||
state.listy += (y - Bangle.appRect.h);
|
||||
}
|
||||
state.otp = "";
|
||||
} else {
|
||||
// token not visible
|
||||
state.id = -1;
|
||||
}
|
||||
state.nextTime = 0;
|
||||
state.curtoken = id;
|
||||
state.hide = 2;
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
// id = token ID number (0...)
|
||||
function drawToken(id) {
|
||||
let x1 = AR.x;
|
||||
let y1 = tokenY(id);
|
||||
let x2 = AR.x2;
|
||||
let y2 = y1 + TOKEN_HEIGHT - 1;
|
||||
let lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : "";
|
||||
let adj;
|
||||
g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2));
|
||||
if (id === state.id) {
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH);
|
||||
} else {
|
||||
g.setColor(g.theme.fg)
|
||||
.setBgColor(g.theme.bg);
|
||||
}
|
||||
if (id == state.id && state.hotp.hotp != "") {
|
||||
// small label centered just below top line
|
||||
g.setFont("Vector", TOKEN_EXTRA_HEIGHT)
|
||||
.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
// large label centered in box
|
||||
sizeFont("l" + id, lbl, AR.w);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
adj = half(y1 + y2);
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2)
|
||||
.drawString(lbl, half(x1 + x2), adj, false);
|
||||
if (id == state.id && state.hotp.hotp != "") {
|
||||
adj = 0;
|
||||
if (tokens[id].period <= 0) {
|
||||
// counter - draw triangle as swipe hint
|
||||
let yc = half(y1 + y2);
|
||||
adj = COUNTER_TRIANGLE_SIZE;
|
||||
g.fillPoly([AR.x, yc, AR.x + adj, yc - adj, AR.x + adj, yc + adj]);
|
||||
adj += 2;
|
||||
}
|
||||
// digits just below label
|
||||
x1 = half(x1 + adj + x2);
|
||||
y1 += TOKEN_EXTRA_HEIGHT;
|
||||
if (state.hotp.hotp == CALCULATING) {
|
||||
sizeFont("c", CALCULATING, AR.w - adj);
|
||||
g.drawString(CALCULATING, x1, y1, false)
|
||||
.flip();
|
||||
state.hotp = hotp(tokens[id]);
|
||||
g.clearRect(AR.x + adj, y1, AR.x2, y2);
|
||||
}
|
||||
sizeFont("d" + id, state.hotp.hotp, AR.w - adj);
|
||||
g.drawString(state.hotp.hotp, x1, y1, false);
|
||||
if (tokens[id].period > 0) {
|
||||
drawProgressBar();
|
||||
}
|
||||
}
|
||||
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
|
||||
}
|
||||
|
||||
function changeId(id) {
|
||||
if (id != state.id) {
|
||||
state.hotp.hotp = CALCULATING;
|
||||
let pid = state.id;
|
||||
state.id = id;
|
||||
if (pid >= 0) {
|
||||
drawToken(pid);
|
||||
}
|
||||
if (id >= 0) {
|
||||
drawToken( id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDrag(e) {
|
||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
||||
if (e.dx == 0 && e.dy == 0) return;
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(0, newy);
|
||||
draw();
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (e.b != 0 && e.dy != 0) {
|
||||
let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
|
||||
if (state.listy != y) {
|
||||
let id, dy = state.listy - y;
|
||||
state.listy = y;
|
||||
g.setClipRect(AR.x, AR.y, AR.x2, AR.y2)
|
||||
.scroll(0, dy);
|
||||
if (dy > 0) {
|
||||
id = Math.floor((state.listy + dy) / TOKEN_HEIGHT);
|
||||
y = tokenY(id + 1);
|
||||
do {
|
||||
drawToken(id);
|
||||
id--;
|
||||
y -= TOKEN_HEIGHT;
|
||||
} while (y > AR.y);
|
||||
}
|
||||
if (dy < 0) {
|
||||
id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT);
|
||||
y = tokenY(id);
|
||||
while (y < AR.y2) {
|
||||
drawToken(id);
|
||||
id++;
|
||||
y += TOKEN_HEIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.b == 0) {
|
||||
timerCalc();
|
||||
}
|
||||
}
|
||||
|
||||
function onTouch(zone, e) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (e) {
|
||||
let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT);
|
||||
if (id == state.id || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.id != id) {
|
||||
if (id >= 0) {
|
||||
// scroll token into view if necessary
|
||||
let dy = 0;
|
||||
let y = id * TOKEN_HEIGHT - state.listy;
|
||||
if (y < 0) {
|
||||
dy -= y;
|
||||
y = 0;
|
||||
}
|
||||
y += TOKEN_HEIGHT;
|
||||
if (y > AR.h) {
|
||||
dy -= (y - AR.h);
|
||||
}
|
||||
onDrag({b:1, dy:dy});
|
||||
}
|
||||
changeId(id);
|
||||
}
|
||||
}
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function onSwipe(e) {
|
||||
if (e == 1) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
switch (e) {
|
||||
case 1:
|
||||
exitApp();
|
||||
break;
|
||||
case -1:
|
||||
if (state.id >= 0 && tokens[state.id].period <= 0) {
|
||||
tokens[state.id].period--;
|
||||
require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc});
|
||||
state.hotp.hotp = CALCULATING;
|
||||
drawToken(state.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
||||
tokens[state.curtoken].period--;
|
||||
let newsettings={tokens:tokens,misc:settings.misc};
|
||||
require("Storage").writeJSON("authentiwatch.json", newsettings);
|
||||
state.nextTime = 0;
|
||||
state.otp = "";
|
||||
state.hide = 2;
|
||||
}
|
||||
draw();
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function bangle1Btn(e) {
|
||||
function bangleBtn(e) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (tokens.length > 0) {
|
||||
if (state.curtoken == -1) {
|
||||
state.curtoken = state.prevcur;
|
||||
} else {
|
||||
switch (e) {
|
||||
case -1: state.curtoken--; break;
|
||||
case 1: state.curtoken++; break;
|
||||
}
|
||||
}
|
||||
state.curtoken = Math.max(state.curtoken, 0);
|
||||
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
|
||||
state.listy = state.curtoken * tokenheight;
|
||||
state.listy -= (Bangle.appRect.h - tokenheight) / 2;
|
||||
state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(state.listy, 0);
|
||||
var fakee = {};
|
||||
fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y;
|
||||
state.curtoken = -1;
|
||||
state.nextTime = 0;
|
||||
onTouch(0, fakee);
|
||||
} else {
|
||||
draw(); // resets idle timer
|
||||
let id = E.clip(state.id + e, 0, tokens.length - 1);
|
||||
onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))});
|
||||
changeId(id);
|
||||
drawProgressBar();
|
||||
}
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function exitApp() {
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
}
|
||||
Bangle.showLauncher();
|
||||
}
|
||||
|
||||
Bangle.on('touch', onTouch);
|
||||
Bangle.on('drag' , onDrag );
|
||||
Bangle.on('swipe', onSwipe);
|
||||
if (typeof BTN2 == 'number') {
|
||||
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
|
||||
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true});
|
||||
if (typeof BTN1 == 'number') {
|
||||
if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
|
||||
setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||
setWatch(()=>exitApp() , BTN2, {edge:"falling", debounce:50});
|
||||
setWatch(()=>bangleBtn( 1), BTN3, {edge:"rising" , debounce:50, repeat:true});
|
||||
} else {
|
||||
setWatch(()=>exitApp() , BTN1, {edge:"falling", debounce:50});
|
||||
}
|
||||
}
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Clear the screen once, at startup
|
||||
const AR = Bangle.appRect;
|
||||
// draw the initial display
|
||||
g.clear();
|
||||
draw();
|
||||
if (tokens.length > 0) {
|
||||
state.listy = AR.h;
|
||||
onDrag({b:1, dy:AR.h});
|
||||
} else {
|
||||
g.setFont("Vector", TOKEN_DIGITS_HEIGHT)
|
||||
.setFontAlign(0, 0, 0)
|
||||
.drawString(NO_TOKENS, AR.x + half(AR.w), AR.y + half(AR.h), false);
|
||||
}
|
||||
timerCalc();
|
||||
Bangle.drawWidgets();
|
||||
|
|
|
@ -54,9 +54,9 @@ var tokens = settings.tokens;
|
|||
*/
|
||||
function base32clean(val, nows) {
|
||||
var ret = val.replaceAll(/\s+/g, ' ');
|
||||
ret = ret.replaceAll(/0/g, 'O');
|
||||
ret = ret.replaceAll(/1/g, 'I');
|
||||
ret = ret.replaceAll(/8/g, 'B');
|
||||
ret = ret.replaceAll('0', 'O');
|
||||
ret = ret.replaceAll('1', 'I');
|
||||
ret = ret.replaceAll('8', 'B');
|
||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||
if (nows) {
|
||||
ret = ret.replaceAll(/\s+/g, '');
|
||||
|
@ -81,9 +81,9 @@ function b32encode(str) {
|
|||
|
||||
function b32decode(seedstr) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, ret = '';
|
||||
for (i in seedstr) {
|
||||
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
var buf = 0, bitcount = 0, ret = '';
|
||||
for (var c of seedstr.toUpperCase()) {
|
||||
c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
|
@ -405,7 +405,7 @@ class proto3decoder {
|
|||
constructor(str) {
|
||||
this.buf = [];
|
||||
for (let i in str) {
|
||||
this.buf = this.buf.concat(str.charCodeAt(i));
|
||||
this.buf.push(str.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
getVarint() {
|
||||
|
@ -487,7 +487,7 @@ function startScan(handler,cancel) {
|
|||
document.body.className = 'scanning';
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video:{facingMode:'environment'}})
|
||||
.then(function(stream){
|
||||
.then(stream => {
|
||||
scanning=true;
|
||||
video.setAttribute('playsinline',true);
|
||||
video.srcObject = stream;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"shortName": "AuthWatch",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Google Authenticator compatible tool.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
|
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 |
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fixed issue with wrong device informations
|
||||
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
||||
0.04: Set Bangle.js 2 compatible, show widgets
|
||||
|
|
|
@ -5,6 +5,7 @@ let menu = {
|
|||
|
||||
function showMainMenu() {
|
||||
menu["< Back"] = () => load();
|
||||
Bangle.drawWidgets();
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
|
@ -55,5 +56,6 @@ function waitMessage() {
|
|||
E.showMessage("scanning");
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
scan();
|
||||
waitMessage();
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "bledetect",
|
||||
"name": "BLE Detector",
|
||||
"shortName": "BLE Detector",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Detect BLE devices and show some informations.",
|
||||
"icon": "bledetect.png",
|
||||
"tags": "app,bluetooth,tool",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"bledetect.app.js","url":"bledetect.js"},
|
||||
|
|
|
@ -49,3 +49,4 @@
|
|||
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)
|
||||
clockApp = require("Storage").read(clockApp.src);
|
||||
}
|
||||
if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, BTN2, {repeat:false,edge:"falling"});`;
|
||||
if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`;
|
||||
eval(clockApp);
|
||||
delete clockApp;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boot",
|
||||
"name": "Bootloader",
|
||||
"version": "0.45",
|
||||
"version": "0.46",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"icon": "bootloader.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: Initial upload
|
||||
0.2: Added scrollable calendar and swipe gestures
|
||||
0.3: Configurable drag gestures
|
||||
|
|
|
@ -9,25 +9,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu
|
|||
||unlocked: smaller clock, but with seconds|
|
||||
||swipe up for big calendar, (up down to scroll, left/right to exit)|
|
||||
|
||||
|
||||
|
||||
|
||||
## Configurable Features
|
||||
- Number of calendar rows (weeks)
|
||||
- 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
|
||||
- Red Saturday
|
||||
- Red Sunday
|
||||
- Swipes (to disable all gestures)
|
||||
- Swipes: music (swipe down)
|
||||
- Spipes: messages (swipe right)
|
||||
- Red Saturday/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 "music" in its filename and launch it
|
||||
- swiping right will search your files for an app with the string "message" in its filename and launch it.
|
||||
- Configurable apps coming soon.
|
||||
- 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
|
||||
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
|
||||
|
||||
## 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,12 +4,13 @@ var s = Object.assign({
|
|||
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
|
||||
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
|
||||
MODE24: true, //24h mode vs 12h mode
|
||||
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?
|
||||
REDSAT: true, // Use red color for saturday?
|
||||
DRAGENABLED: true,
|
||||
DRAGMUSIC: true,
|
||||
DRAGMESSAGES: true
|
||||
DRAGDOWN: "[AI:messg]",
|
||||
DRAGRIGHT: "[AI:music]",
|
||||
DRAGLEFT: "[ignore]",
|
||||
DRAGUP: "[calend.]"
|
||||
}, require('Storage').readJSON("clockcal.json", true) || {});
|
||||
|
||||
const h = g.getHeight();
|
||||
|
@ -27,13 +28,13 @@ var monthOffset = 0;
|
|||
*/
|
||||
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;
|
||||
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";
|
||||
|
@ -45,22 +46,22 @@ function drawFullCalendar(monthOffset) {
|
|||
if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval);
|
||||
d = addMonths(Date(), monthOffset);
|
||||
tdy = Date().getDate() + "." + Date().getMonth();
|
||||
newmonth=false;
|
||||
newmonth = false;
|
||||
c_y = 0;
|
||||
g.reset();
|
||||
g.setBgColor(0);
|
||||
g.clear();
|
||||
var prevmonth = addMonths(d, -1)
|
||||
var prevmonth = addMonths(d, -1);
|
||||
const today = prevmonth.getDate();
|
||||
var rD = new Date(prevmonth.getTime());
|
||||
rD.setDate(rD.getDate() - (today - 1));
|
||||
const dow = (s.FIRSTDAYOFFSET + rD.getDay()) % 7;
|
||||
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];
|
||||
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;
|
||||
|
@ -69,14 +70,14 @@ function drawFullCalendar(monthOffset) {
|
|||
rMonth = rD.getMonth();
|
||||
rDate = rD.getDate();
|
||||
if (tdy == rDate + "." + rMonth) {
|
||||
caldrawToday(rDate);
|
||||
caldrawToday(rDate);
|
||||
} else if (rDate == 1) {
|
||||
caldrawFirst(rDate);
|
||||
caldrawFirst(rDate);
|
||||
} else {
|
||||
caldrawNormal(rDate,fg[rD.getDay()]);
|
||||
caldrawNormal(rDate, fg[rD.getDay()]);
|
||||
}
|
||||
if (newmonth && x == 7) {
|
||||
caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD);
|
||||
caldrawMonth(rDate, monthclr[rMonth % 6], months[rMonth], rD);
|
||||
}
|
||||
rD.setDate(rDate + 1);
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ function drawFullCalendar(monthOffset) {
|
|||
delete addMonths;
|
||||
if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start));
|
||||
}
|
||||
function caldrawMonth(rDate,c,m,rD) {
|
||||
function caldrawMonth(rDate, c, m, rD) {
|
||||
g.setColor(c);
|
||||
g.setFont("Vector", 18);
|
||||
g.setFontAlign(-1, 1, 1);
|
||||
|
@ -93,29 +94,29 @@ function caldrawMonth(rDate,c,m,rD) {
|
|||
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);
|
||||
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);
|
||||
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 caldrawNormal(rDate, c) {
|
||||
g.setFont("Vector", 16);
|
||||
g.setFontAlign(1, 1);
|
||||
g.setColor(c);
|
||||
g.drawString(rDate, bottomrightX, bottomrightY);//100
|
||||
}
|
||||
function drawMinutes() {
|
||||
if (DEBUG) console.log("|-->minutes");
|
||||
|
@ -163,7 +164,7 @@ function drawWatch() {
|
|||
g.clear();
|
||||
drawMinutes();
|
||||
if (!dimSeconds) drawSeconds();
|
||||
const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6
|
||||
const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6
|
||||
const today = d.getDate();
|
||||
var rD = new Date(d.getTime());
|
||||
rD.setDate(rD.getDate() - dow);
|
||||
|
@ -205,27 +206,52 @@ function BTevent() {
|
|||
setTimeout(function () { Bangle.buzz(interval); }, interval * 3);
|
||||
}
|
||||
}
|
||||
|
||||
function action(a) {
|
||||
g.reset();
|
||||
if (typeof secondInterval !== "undefined") clearTimeout(secondInterval);
|
||||
if (DEBUG) console.log("action:" + a);
|
||||
switch (a) {
|
||||
case "[ignore]":
|
||||
break;
|
||||
case "[calend.]":
|
||||
drawFullCalendar();
|
||||
break;
|
||||
case "[AI:music]":
|
||||
l = require("Storage").list(RegExp("music.*app.js"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else E.showAlert("Music app not found", "Not found").then(drawWatch);
|
||||
break;
|
||||
case "[AI:messg]":
|
||||
l = require("Storage").list(RegExp("message.*app.js"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else E.showAlert("Message app not found", "Not found").then(drawWatch);
|
||||
break;
|
||||
default:
|
||||
l = require("Storage").list(RegExp(a + ".app.js"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else E.showAlert(a + ": App not found", "Not found").then(drawWatch);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function input(dir) {
|
||||
if (s.DRAGENABLED) {
|
||||
Bangle.buzz(100,1);
|
||||
console.log("swipe:"+dir);
|
||||
Bangle.buzz(100, 1);
|
||||
if (DEBUG) console.log("swipe:" + dir);
|
||||
switch (dir) {
|
||||
case "r":
|
||||
if (state == "calendar") {
|
||||
drawWatch();
|
||||
} else {
|
||||
if (s.DRAGMUSIC) {
|
||||
l=require("Storage").list(RegExp("music.*app"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else Bangle.buzz(3000,1);//not found
|
||||
}
|
||||
action(s.DRAGRIGHT);
|
||||
}
|
||||
break;
|
||||
case "l":
|
||||
if (state == "calendar") {
|
||||
drawWatch();
|
||||
} else {
|
||||
action(s.DRAGLEFT);
|
||||
}
|
||||
break;
|
||||
case "d":
|
||||
|
@ -233,21 +259,15 @@ function input(dir) {
|
|||
monthOffset--;
|
||||
drawFullCalendar(monthOffset);
|
||||
} else {
|
||||
if (s.DRAGMESSAGES) {
|
||||
l=require("Storage").list(RegExp("message.*app"));
|
||||
if (l.length > 0) {
|
||||
load(l[0]);
|
||||
} else Bangle.buzz(3000,1);//not found
|
||||
}
|
||||
action(s.DRAGDOWN);
|
||||
}
|
||||
break;
|
||||
case "u":
|
||||
if (state == "watch") {
|
||||
state = "calendar";
|
||||
drawFullCalendar(0);
|
||||
} else if (state == "calendar") {
|
||||
if (state == "calendar") {
|
||||
monthOffset++;
|
||||
drawFullCalendar(monthOffset);
|
||||
} else {
|
||||
action(s.DRAGUP);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -255,26 +275,24 @@ function input(dir) {
|
|||
drawWatch();
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let drag;
|
||||
Bangle.on("drag", e => {
|
||||
if (s.DRAGENABLED) {
|
||||
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) + 10) {
|
||||
dir = (dx > 0) ? "r" : "l";
|
||||
} else if (Math.abs(dy) > Math.abs(dx) + 10) {
|
||||
dir = (dy > 0) ? "d" : "u";
|
||||
}
|
||||
drag = null;
|
||||
input(dir);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "clockcal",
|
||||
"name": "Clock & Calendar",
|
||||
"version": "0.2",
|
||||
"version": "0.3",
|
||||
"description": "Clock with Calendar",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
(function (back) {
|
||||
var FILE = "clockcal.json";
|
||||
|
||||
settings = Object.assign({
|
||||
defaults={
|
||||
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
|
||||
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
|
||||
MODE24: true, //24h mode vs 12h mode
|
||||
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
|
||||
REDSUN: true, // Use red color for sunday?
|
||||
REDSAT: true, // Use red color for saturday?
|
||||
DRAGENABLED: true, //Enable drag gestures (bigger calendar etc)
|
||||
DRAGMUSIC: true, //Enable drag down for music (looks for "music*app")
|
||||
DRAGMESSAGES: true //Enable drag right for messages (looks for "message*app")
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
DRAGDOWN: "[AI:messg]",
|
||||
DRAGRIGHT: "[AI:music]",
|
||||
DRAGLEFT: "[ignore]",
|
||||
DRAGUP: "[calend.]"
|
||||
};
|
||||
settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"];
|
||||
require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js","")));
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
@ -70,27 +73,39 @@
|
|||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipes (big cal.)?': {
|
||||
value: settings.DRAGENABLED,
|
||||
format: v => v ? "On" : "Off",
|
||||
'Drag Up ': {
|
||||
min:0, max:actions.length-1,
|
||||
value: actions.indexOf(settings.DRAGUP),
|
||||
format: v => actions[v],
|
||||
onchange: v => {
|
||||
settings.DRAGENABLED = v;
|
||||
settings.DRAGUP = actions[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipes (music)?': {
|
||||
value: settings.DRAGMUSIC,
|
||||
format: v => v ? "On" : "Off",
|
||||
'Drag Right': {
|
||||
min:0, max:actions.length-1,
|
||||
value: actions.indexOf(settings.DRAGRIGHT),
|
||||
format: v => actions[v],
|
||||
onchange: v => {
|
||||
settings.DRAGMUSIC = v;
|
||||
settings.DRAGRIGHT = actions[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipes (messg)?': {
|
||||
value: settings.DRAGMESSAGES,
|
||||
format: v => v ? "On" : "Off",
|
||||
'Drag Down': {
|
||||
min:0, max:actions.length-1,
|
||||
value: actions.indexOf(settings.DRAGDOWN),
|
||||
format: v => actions[v],
|
||||
onchange: v => {
|
||||
settings.DRAGMESSAGES = 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();
|
||||
}
|
||||
},
|
||||
|
@ -100,17 +115,7 @@
|
|||
format: v => ["No", "Yes"][v],
|
||||
onchange: v => {
|
||||
if (v == 1) {
|
||||
settings = {
|
||||
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
|
||||
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect.
|
||||
MODE24: true, //24h mode vs 12h mode
|
||||
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
|
||||
REDSUN: true, // Use red color for sunday?
|
||||
REDSAT: true, // Use red color for saturday?
|
||||
DRAGENABLED: true,
|
||||
DRAGMUSIC: true,
|
||||
DRAGMESSAGES: true
|
||||
};
|
||||
settings = defaults;
|
||||
writeSettings();
|
||||
load();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
})
|
|
@ -1,4 +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.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
|
|
@ -11,6 +11,8 @@ When two tiles with the same number are squashed together they will add up as ex
|
|||
|
||||
**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.)
|
||||
|
@ -27,5 +29,8 @@ Use the side **BTN** to exit the game, score and tile positions will be saved.
|
|||
|
||||
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:
|
||||

|
|
@ -17,7 +17,7 @@ const cellChars = [
|
|||
const maxUndoLevels = 4;
|
||||
const noExceptions = true;
|
||||
let charIndex = 0; // plain numbers on the grid
|
||||
|
||||
const themeBg = g.theme.bg;
|
||||
|
||||
|
||||
const scores = {
|
||||
|
@ -144,6 +144,13 @@ const buttons = {
|
|||
},
|
||||
add: function(btn) {
|
||||
this.all.push(btn);
|
||||
},
|
||||
isPopUpActive: false,
|
||||
activatePopUp: function() {
|
||||
this.isPopUpActive = true;
|
||||
},
|
||||
deActivatePopUp: function() {
|
||||
this.isPopUpActive = false;
|
||||
}
|
||||
};
|
||||
/**
|
||||
|
@ -253,7 +260,6 @@ const dragThreshold = 10;
|
|||
const clickThreshold = 3;
|
||||
|
||||
let allSquares = [];
|
||||
// let buttons = [];
|
||||
|
||||
class Button {
|
||||
constructor(name, x0, y0, width, height, text, bg, fg, cb, enabled) {
|
||||
|
@ -304,13 +310,29 @@ class Cell {
|
|||
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() {
|
||||
g.setColor(this.getColor(this.expVal).bg)
|
||||
.fillRect(this.x0, this.y0, this.x1, this.y1);
|
||||
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) {
|
||||
|
@ -346,6 +368,19 @@ class Cell {
|
|||
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() {
|
||||
|
@ -387,11 +422,12 @@ function createGrid () {
|
|||
}
|
||||
}
|
||||
function messageGameOver () {
|
||||
g.setColor("#1a0d00")
|
||||
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("#ffffff")
|
||||
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);
|
||||
}
|
||||
|
@ -417,11 +453,13 @@ function addRandomNumber() {
|
|||
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();
|
||||
});
|
||||
}
|
||||
|
@ -451,6 +489,7 @@ function initGame() {
|
|||
Bangle.drawWidgets();
|
||||
}
|
||||
function drawPopUp(message,cb) {
|
||||
buttons.activatePopUp();
|
||||
g.setColor('#FFFFFF');
|
||||
let rDims = Bangle.appRect;
|
||||
g.fillPoly([rDims.x+10, rDims.y+20,
|
||||
|
@ -473,6 +512,7 @@ function drawPopUp(message,cb) {
|
|||
g.drawString(message, rDims.x+20, rDims.y+20);
|
||||
buttons.add(btnYes);
|
||||
buttons.add(btnNo);
|
||||
|
||||
}
|
||||
function handlePopUpClicks(btn) {
|
||||
const name = btn.name;
|
||||
|
@ -480,6 +520,7 @@ function handlePopUpClicks(btn) {
|
|||
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();
|
||||
|
@ -497,7 +538,7 @@ function handlePopUpClicks(btn) {
|
|||
function resetGame() {
|
||||
g.clear();
|
||||
scores.reset();
|
||||
allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();});
|
||||
allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();sq.setRndmFalse();});
|
||||
addRandomNumber();
|
||||
addRandomNumber();
|
||||
drawGrid();
|
||||
|
@ -536,14 +577,13 @@ function handleclick(e) {
|
|||
|
||||
// Handle a drag event (moving the stones around)
|
||||
function handledrag(e) {
|
||||
/*debug(Math.abs(e.dx) > Math.abs(e.dy) ?
|
||||
(e.dx > 0 ? e => console.log('To the right') : e => console.log('To the left') ) :
|
||||
(e.dy > 0 ? e => console.log('Move down') : e => console.log('Move up') ));
|
||||
*/
|
||||
// [move.right, move.left, move.up, move.down]
|
||||
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 )));
|
||||
// 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
|
||||
|
@ -614,6 +654,7 @@ function runGame(dir){
|
|||
mover.nonEmptyCells(dir);
|
||||
mover.mergeEqlCells(dir);
|
||||
mover.nonEmptyCells(dir);
|
||||
allSquares.forEach(sq => {sq.setRndmFalse();});
|
||||
addRandomNumber();
|
||||
drawGrid();
|
||||
scores.check();
|
||||
|
|
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
|
@ -1,7 +1,7 @@
|
|||
{ "id": "game1024",
|
||||
"name": "1024 Game",
|
||||
"shortName" : "1024 Game",
|
||||
"version": "0.04",
|
||||
"version": "0.06",
|
||||
"icon": "game1024.png",
|
||||
"screenshots": [ {"url":"screenshot.png" } ],
|
||||
"readme":"README.md",
|
||||
|
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 54 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 |
|
@ -6,3 +6,4 @@
|
|||
0.06: Add widgets
|
||||
0.07: Update scaling for new firmware
|
||||
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
||||
0.09: Grey out BPM until confidence is over 50%
|
||||
|
|
|
@ -35,9 +35,9 @@ function onHRM(h) {
|
|||
g.clearRect(0,24,g.getWidth(),80);
|
||||
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
|
||||
var str = hrmInfo.bpm;
|
||||
g.setFontVector(40).drawString(str,px,45);
|
||||
g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45);
|
||||
px += g.stringWidth(str)/2;
|
||||
g.setFont("6x8");
|
||||
g.setFont("6x8").setColor(g.theme.fg);
|
||||
g.drawString("BPM",px+15,45);
|
||||
}
|
||||
Bangle.on('HRM', onHRM);
|
||||
|
@ -101,4 +101,3 @@ function readHRM() {
|
|||
lastHrmPt = [hrmOffset, y];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "hrm",
|
||||
"name": "Heart Rate Monitor",
|
||||
"version": "0.08",
|
||||
"version": "0.09",
|
||||
"description": "Measure your heart rate and see live sensor data",
|
||||
"icon": "heartrate.png",
|
||||
"tags": "health",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Add the option to enable touching the widget only on clock and settings.
|
||||
0.03: Settings page now uses built-in min/max/wrap (fix #1607)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "lightswitch",
|
||||
"name": "Light Switch Widget",
|
||||
"shortName": "Light Switch",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
|
||||
"icon": "images/app.png",
|
||||
"screenshots": [
|
||||
|
|
|
@ -44,9 +44,11 @@
|
|||
// return entry for string value
|
||||
return {
|
||||
value: entry.value.indexOf(settings[key]),
|
||||
min : 0,
|
||||
max : entry.value.length-1,
|
||||
wrap : true,
|
||||
format: v => entry.title ? entry.title[v] : entry.value[v],
|
||||
onchange: function(v) {
|
||||
this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v;
|
||||
writeSetting(key, entry.value[v], entry.drawWidgets);
|
||||
if (entry.exec) entry.exec(entry.value[v]);
|
||||
}
|
||||
|
@ -57,8 +59,10 @@
|
|||
value: settings[key] * entry.factor,
|
||||
step: entry.step,
|
||||
format: v => v > 0 ? v + entry.unit : "off",
|
||||
min : entry.min,
|
||||
max : entry.max,
|
||||
wrap : true,
|
||||
onchange: function(v) {
|
||||
this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v;
|
||||
writeSetting(key, v / entry.factor, entry.drawWidgets);
|
||||
},
|
||||
};
|
||||
|
@ -133,16 +137,16 @@
|
|||
title: "Light Switch"
|
||||
},
|
||||
"< Back": () => back(),
|
||||
"-- Widget --------": 0,
|
||||
"-- Widget": 0,
|
||||
"Bulb col": getEntry("colors"),
|
||||
"Image": getEntry("image"),
|
||||
"-- Control -------": 0,
|
||||
"-- Control": 0,
|
||||
"Touch": getEntry("touchOn"),
|
||||
"Drag Delay": getEntry("dragDelay"),
|
||||
"Min Value": getEntry("minValue"),
|
||||
"-- Unlock --------": 0,
|
||||
"-- Unlock": 0,
|
||||
"TapSide": getEntry("unlockSide"),
|
||||
"-- Flash ---------": 0,
|
||||
"-- Flash": 0,
|
||||
"TapSide ": getEntry("tapSide"),
|
||||
"Tap": getEntry("tapOn"),
|
||||
"Timeout": getEntry("tOut"),
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: First release
|
||||
0.02: Make sure to reset turns
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Classic Mind Game",
|
||||
"shortName":"Master Mind",
|
||||
"icon": "mmind.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "This is the classic game for masterminds",
|
||||
"screenshots": [{"url":"screenshot_mmind.png"}],
|
||||
"type": "app",
|
||||
|
|
|
@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) {
|
|||
break;
|
||||
case 4:
|
||||
//new game
|
||||
turn = 0;
|
||||
play = [-1,-1,-1,-1];
|
||||
game = [];
|
||||
endgame=false;
|
||||
|
@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) {
|
|||
game = [];
|
||||
get_secret();
|
||||
draw();
|
||||
//Bangle.loadWidgets();
|
||||
//Bangle.drawWidgets();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -17,3 +17,4 @@
|
|||
0.11: Fix KML and GPX export when there is no GPS data
|
||||
0.12: Fix 'Back' label positioning on track/graph display, make translateable
|
||||
0.13: Fix for when widget is used before app
|
||||
0.14: Remove unneeded variable assignment
|
|
@ -2,7 +2,7 @@
|
|||
"id": "recorder",
|
||||
"name": "Recorder",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
|
|
|
@ -248,7 +248,7 @@
|
|||
}
|
||||
var buttons={Yes:"yes",No:"no"};
|
||||
if (newFileName) buttons["New"] = "new";
|
||||
var prompt = E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{
|
||||
return E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{
|
||||
if (selection==="no") return false; // just cancel
|
||||
if (selection==="yes") {
|
||||
require("Storage").open(settings.file,"r").erase();
|
||||
|
@ -259,7 +259,6 @@
|
|||
}
|
||||
return WIDGETS["recorder"].setRecording(1);
|
||||
});
|
||||
return prompt;
|
||||
}
|
||||
settings.recording = isOn;
|
||||
updateSettings(settings);
|
||||
|
|
|
@ -9,3 +9,4 @@
|
|||
0.08: Added support for notifications from exstats. Support all stats from exstats
|
||||
0.09: Fix broken start/stop if recording not enabled (fix #1561)
|
||||
0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578)
|
||||
0.11: Notifications fixes
|
|
@ -59,7 +59,7 @@ function onStartStop() {
|
|||
layout.render();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
} else if (!settings.record && WIDGETS["recorder"]) {
|
||||
prepPromises.push(
|
||||
WIDGETS["recorder"].setRecording(false)
|
||||
);
|
||||
|
@ -124,7 +124,7 @@ function configureNotification(stat) {
|
|||
}
|
||||
|
||||
Object.keys(settings.notify).forEach((statType) => {
|
||||
if (settings.notify[statType].increment > 0) {
|
||||
if (settings.notify[statType].increment > 0 && exs.stats[statType]) {
|
||||
configureNotification(exs.stats[statType]);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "run",
|
||||
"name": "Run",
|
||||
"version":"0.10",
|
||||
"version":"0.11",
|
||||
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors,gps",
|
||||
|
|
|
@ -90,8 +90,8 @@
|
|||
[[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]],
|
||||
];
|
||||
notificationsMenu[/*LANG*/"Dist Pattern"] = {
|
||||
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
|
||||
min: 0, max: vibPatterns.length,
|
||||
value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
|
||||
min: 0, max: vibTimes.length,
|
||||
format: v => vibPatterns[v]||/*LANG*/"Off",
|
||||
onchange: v => {
|
||||
settings.notify.dist.notifications = vibTimes[v];
|
||||
|
@ -100,8 +100,8 @@
|
|||
}
|
||||
}
|
||||
notificationsMenu[/*LANG*/"Step Pattern"] = {
|
||||
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
|
||||
min: 0, max: vibPatterns.length,
|
||||
value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
|
||||
min: 0, max: vibTimes.length,
|
||||
format: v => vibPatterns[v]||/*LANG*/"Off",
|
||||
onchange: v => {
|
||||
settings.notify.step.notifications = vibTimes[v];
|
||||
|
@ -110,8 +110,8 @@
|
|||
}
|
||||
}
|
||||
notificationsMenu[/*LANG*/"Time Pattern"] = {
|
||||
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
|
||||
min: 0, max: vibPatterns.length,
|
||||
value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
|
||||
min: 0, max: vibTimes.length,
|
||||
format: v => vibPatterns[v]||/*LANG*/"Off",
|
||||
onchange: v => {
|
||||
settings.notify.time.notifications = vibTimes[v];
|
||||
|
|
|
@ -29,11 +29,11 @@ function calc_ess(val) {
|
|||
if (nonmot) {
|
||||
slsnds+=1;
|
||||
if (slsnds >= sleepthresh) {
|
||||
return true; // awake
|
||||
return true; // sleep
|
||||
}
|
||||
} else {
|
||||
slsnds=0;
|
||||
return false; // sleep
|
||||
return false; // awake
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
0.01: Initial creation of the touch timer app
|
||||
0.02: Add settings menu
|
||||
0.02: Add settings menu
|
||||
0.03: Add ability to repeat last timer
|
||||
0.04: Add 5 second count down buzzer
|
||||
|
|
|
@ -126,6 +126,14 @@ var main = () => {
|
|||
timerIntervalId = setInterval(() => {
|
||||
timerCountDown.draw();
|
||||
|
||||
// Buzz lightly when there are less then 5 seconds left
|
||||
if (settings.countDownBuzz) {
|
||||
var remainingSeconds = timerCountDown.getAdjustedTime().seconds;
|
||||
if (remainingSeconds <= 5 && remainingSeconds > 0) {
|
||||
Bangle.buzz();
|
||||
}
|
||||
}
|
||||
|
||||
if (timerCountDown.isFinished()) {
|
||||
buttonStartPause.value = "FINISHED!";
|
||||
buttonStartPause.draw();
|
||||
|
@ -141,6 +149,13 @@ var main = () => {
|
|||
if (buzzCount >= settings.buzzCount) {
|
||||
clearInterval(buzzIntervalId);
|
||||
buzzIntervalId = undefined;
|
||||
|
||||
buttonStartPause.value = "REPEAT";
|
||||
buttonStartPause.draw();
|
||||
buttonStartPause.value = "START";
|
||||
timerCountDown = undefined;
|
||||
timerEdit.draw();
|
||||
|
||||
return;
|
||||
} else {
|
||||
Bangle.buzz(settings.buzzDuration * 1000, 1);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "touchtimer",
|
||||
"name": "Touch Timer",
|
||||
"shortName": "Touch Timer",
|
||||
"version": "0.02",
|
||||
"version": "0.04",
|
||||
"description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.",
|
||||
"icon": "app.png",
|
||||
"tags": "tools",
|
||||
|
|
|
@ -31,6 +31,14 @@
|
|||
writeSettings(settings);
|
||||
},
|
||||
},
|
||||
"CountDown Buzz": {
|
||||
value: !!settings.countDownBuzz,
|
||||
format: value => value?"On":"Off",
|
||||
onchange: (value) => {
|
||||
settings.countDownBuzz = value;
|
||||
writeSettings(settings);
|
||||
},
|
||||
},
|
||||
"Pause Between": {
|
||||
value: settings.pauseBetween,
|
||||
min: 1,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Load widgets after setUI so widclk knows when to hide
|
||||
0.03: Show the day of the week
|
||||
|
|
|
@ -41,6 +41,7 @@ function draw() {
|
|||
var date = new Date();
|
||||
var timeStr = require("locale").time(date,1);
|
||||
var dateStr = require("locale").date(date).toUpperCase();
|
||||
var dowStr = require("locale").dow(date).toUpperCase();
|
||||
// draw time
|
||||
g.setFontAlign(0,0).setFont("ZCOOL");
|
||||
g.drawString(timeStr,x,y);
|
||||
|
@ -48,6 +49,9 @@ function draw() {
|
|||
y += 35;
|
||||
g.setFontAlign(0,0,1).setFont("6x8");
|
||||
g.drawString(dateStr,g.getWidth()-8,g.getHeight()/2);
|
||||
// draw the day of the week
|
||||
g.setFontAlign(0,0,3).setFont("6x8");
|
||||
g.drawString(dowStr,8,g.getHeight()/2);
|
||||
// queue draw in one minute
|
||||
queueDraw();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "waveclk",
|
||||
"name": "Wave Clock",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/)",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.1 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Make Bangle.js 2 compatible
|
|
@ -24,7 +24,7 @@ need to travel in to reach the selected waypoint. The blue text is
|
|||
the name of the current waypoint. NONE means that there is no
|
||||
waypoint set and so bearing and distance will remain at 0. To select
|
||||
a waypoint, press BTN2 (middle) and wait for the blue text to turn
|
||||
white. Then use BTN1 and BTN3 to select a waypoint. The waypoint
|
||||
white. Then use BTN1 and BTN3 (swipe up/down on Bangle.js 2) to select a waypoint. The waypoint
|
||||
choice is fixed by pressing BTN2 again. In the screen shot below a
|
||||
waypoint giving the location of Stone Henge has been selected.
|
||||
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
|
||||
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
|
||||
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
|
||||
const scale = g.getWidth()/240;
|
||||
var pal_by = new Uint16Array([g.getBgColor(),0xFFC0],0,1); // black, yellow
|
||||
var pal_bw = new Uint16Array([g.getBgColor(),g.getColor()],0,1); // black, white
|
||||
var pal_bb = new Uint16Array([g.getBgColor(),0x07ff],0,1); // black, blue
|
||||
|
||||
// having 3 2 color pallette keeps the memory requirement lower
|
||||
var buf1 = Graphics.createArrayBuffer(160,160,1, {msb:true});
|
||||
var buf2 = Graphics.createArrayBuffer(80,40,1, {msb:true});
|
||||
var buf1 = Graphics.createArrayBuffer(160*scale,160*scale,1, {msb:true});
|
||||
var buf2 = Graphics.createArrayBuffer(g.getWidth()/3,40*scale,1, {msb:true});
|
||||
var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo="));
|
||||
|
||||
function flip1(x,y) {
|
||||
g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y);
|
||||
g.drawImage({width:160*scale,height:160*scale,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y);
|
||||
buf1.clear();
|
||||
}
|
||||
|
||||
function flip2_bw(x,y) {
|
||||
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y);
|
||||
g.drawImage({width:g.getWidth()/3,height:40*scale,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y);
|
||||
buf2.clear();
|
||||
}
|
||||
|
||||
function flip2_bb(x,y) {
|
||||
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y);
|
||||
g.drawImage({width:g.getWidth()/3,height:40*scale,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y);
|
||||
buf2.clear();
|
||||
}
|
||||
|
||||
|
@ -51,12 +52,12 @@ function drawCompass(course) {
|
|||
previous.course = course;
|
||||
|
||||
buf1.setColor(1);
|
||||
buf1.fillCircle(80,80,79,79);
|
||||
buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,79*scale);
|
||||
buf1.setColor(0);
|
||||
buf1.fillCircle(80,80,69,69);
|
||||
buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,69*scale);
|
||||
buf1.setColor(1);
|
||||
buf1.drawImage(arrow_img, 80, 80, {scale:3, rotate:radians(course)} );
|
||||
flip1(40, 30);
|
||||
buf1.drawImage(arrow_img, buf1.getWidth()/2, buf1.getHeight()/2, {scale:3*scale, rotate:radians(course)} );
|
||||
flip1(40*scale, Bangle.appRect.y+6*scale);
|
||||
}
|
||||
|
||||
/***** COMPASS CODE ***********/
|
||||
|
@ -138,7 +139,7 @@ function distance(a,b){
|
|||
|
||||
|
||||
function drawN(){
|
||||
buf2.setFont("Vector",24);
|
||||
buf2.setFont("Vector",24*scale);
|
||||
var bs = wp_bearing.toString();
|
||||
bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs;
|
||||
var dst = loc.distance(dist);
|
||||
|
@ -147,12 +148,12 @@ function drawN(){
|
|||
|
||||
// show distance on the left
|
||||
if (previous.dst !== dst) {
|
||||
previous.dst = dst
|
||||
previous.dst = dst;
|
||||
buf2.setColor(1);
|
||||
buf2.setFontAlign(-1,-1);
|
||||
buf2.setFont("Vector", 20);
|
||||
buf2.setFont("Vector", 20*scale);
|
||||
buf2.drawString(dst,0,0);
|
||||
flip2_bw(0, 200);
|
||||
flip2_bw(0, g.getHeight()-40*scale);
|
||||
}
|
||||
|
||||
// bearing, place in middle at bottom of compass
|
||||
|
@ -160,9 +161,9 @@ function drawN(){
|
|||
previous.bs = bs;
|
||||
buf2.setColor(1);
|
||||
buf2.setFontAlign(0, -1);
|
||||
buf2.setFont("Vector",38);
|
||||
buf2.drawString(bs,40,0);
|
||||
flip2_bw(80, 200);
|
||||
buf2.setFont("Vector",38*scale);
|
||||
buf2.drawString(bs,40*scale,0);
|
||||
flip2_bw(g.getWidth()/3, g.getHeight()-40*scale);
|
||||
}
|
||||
|
||||
// waypoint name on right
|
||||
|
@ -170,13 +171,13 @@ function drawN(){
|
|||
previous.selected = selected;
|
||||
buf2.setColor(1);
|
||||
buf2.setFontAlign(1,-1); // right, bottom
|
||||
buf2.setFont("Vector", 20);
|
||||
buf2.drawString(wp.name, 80, 0);
|
||||
buf2.setFont("Vector", 20*scale);
|
||||
buf2.drawString(wp.name, 80*scale, 0);
|
||||
|
||||
if (selected)
|
||||
flip2_bw(160, 200);
|
||||
flip2_bw(g.getWidth()/3*2, g.getHeight()-40*scale);
|
||||
else
|
||||
flip2_bb(160, 200);
|
||||
flip2_bb(g.getWidth()/3*2, g.getHeight()-40*scale);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,9 +230,11 @@ function startdraw(){
|
|||
}
|
||||
|
||||
function setButtons(){
|
||||
setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
|
||||
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
|
||||
setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"});
|
||||
Bangle.setUI("updown", d=>{
|
||||
if (d<0) { nextwp(-1); }
|
||||
else if (d>0) { nextwp(1); }
|
||||
else { doselect(); }
|
||||
});
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"id": "waypointer",
|
||||
"name": "Way Pointer",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation",
|
||||
"icon": "waypointer.png",
|
||||
"tags": "tool,outdoors,gps",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"interface": "waypoints.html",
|
||||
"storage": [
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/* Detects if we're running under Gadgetbridge in a WebView, and if
|
||||
so it overwrites the 'Puck' library with a special one that calls back
|
||||
into Gadgetbridge to handle watch communications */
|
||||
|
||||
/*// test code
|
||||
Android = {
|
||||
bangleTx : function(data) {
|
||||
console.log("TX : "+JSON.stringify(data));
|
||||
}
|
||||
};*/
|
||||
|
||||
if (typeof Android!=="undefined") {
|
||||
console.log("Running under Gadgetbridge, overwrite Puck library");
|
||||
|
||||
var isBusy = false;
|
||||
var queue = [];
|
||||
var connection = {
|
||||
cb : function(data) {},
|
||||
write : function(data, writecb) {
|
||||
Android.bangleTx(data);
|
||||
Puck.writeProgress(data.length, data.length);
|
||||
if (writecb) setTimeout(writecb,10);
|
||||
},
|
||||
close : function() {},
|
||||
received : "",
|
||||
hadData : false
|
||||
}
|
||||
|
||||
function bangleRx(data) {
|
||||
// document.getElementById("status").innerText = "RX:"+data;
|
||||
connection.received += data;
|
||||
connection.hadData = true;
|
||||
if (connection.cb) connection.cb(data);
|
||||
}
|
||||
|
||||
function log(level, s) {
|
||||
if (Puck.log) Puck.log(level, s);
|
||||
}
|
||||
|
||||
function handleQueue() {
|
||||
if (!queue.length) return;
|
||||
var q = queue.shift();
|
||||
log(3,"Executing "+JSON.stringify(q)+" from queue");
|
||||
if (q.type == "write") Puck.write(q.data, q.callback, q.callbackNewline);
|
||||
else log(1,"Unknown queue item "+JSON.stringify(q));
|
||||
}
|
||||
|
||||
/* convenience function... Write data, call the callback with data:
|
||||
callbackNewline = false => if no new data received for ~0.2 sec
|
||||
callbackNewline = true => after a newline */
|
||||
function write(data, callback, callbackNewline) {
|
||||
let result;
|
||||
/// If there wasn't a callback function, then promisify
|
||||
if (typeof callback !== 'function') {
|
||||
callbackNewline = callback;
|
||||
|
||||
result = new Promise((resolve, reject) => callback = (value, err) => {
|
||||
if (err) reject(err);
|
||||
else resolve(value);
|
||||
});
|
||||
}
|
||||
|
||||
if (isBusy) {
|
||||
log(3, "Busy - adding Puck.write to queue");
|
||||
queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline});
|
||||
return result;
|
||||
}
|
||||
|
||||
var cbTimeout;
|
||||
function onWritten() {
|
||||
if (callbackNewline) {
|
||||
connection.cb = function(d) {
|
||||
var newLineIdx = connection.received.indexOf("\n");
|
||||
if (newLineIdx>=0) {
|
||||
var l = connection.received.substr(0,newLineIdx);
|
||||
connection.received = connection.received.substr(newLineIdx+1);
|
||||
connection.cb = undefined;
|
||||
if (cbTimeout) clearTimeout(cbTimeout);
|
||||
cbTimeout = undefined;
|
||||
if (callback)
|
||||
callback(l);
|
||||
isBusy = false;
|
||||
handleQueue();
|
||||
}
|
||||
};
|
||||
}
|
||||
// wait for any received data if we have a callback...
|
||||
var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data
|
||||
var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/;
|
||||
var maxDataTime = dataWaitTime; // max time we wait after having received data
|
||||
cbTimeout = setTimeout(function timeout() {
|
||||
cbTimeout = undefined;
|
||||
if (maxTime) maxTime--;
|
||||
if (maxDataTime) maxDataTime--;
|
||||
if (connection.hadData) maxDataTime=dataWaitTime;
|
||||
if (maxDataTime && maxTime) {
|
||||
cbTimeout = setTimeout(timeout, 100);
|
||||
} else {
|
||||
connection.cb = undefined;
|
||||
if (callback)
|
||||
callback(connection.received);
|
||||
isBusy = false;
|
||||
handleQueue();
|
||||
connection.received = "";
|
||||
}
|
||||
connection.hadData = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
if (!connection.txInProgress) connection.received = "";
|
||||
isBusy = true;
|
||||
connection.write(data, onWritten);
|
||||
return result
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
Puck = {
|
||||
/// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
|
||||
debug : Puck.debug,
|
||||
/// Should we use flow control? Default is true
|
||||
flowControl : true,
|
||||
/// Used internally to write log information - you can replace this with your own function
|
||||
log : function(level, s) { if (level <= this.debug) console.log("<BLE> "+s)},
|
||||
/// Called with the current send progress or undefined when done - you can replace this with your own function
|
||||
writeProgress : Puck.writeProgress,
|
||||
connect : function(callback) {
|
||||
setTimeout(callback, 10);
|
||||
},
|
||||
write : write,
|
||||
eval : function(expr, cb) {
|
||||
const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true)
|
||||
.then(function (d) {
|
||||
try {
|
||||
return JSON.parse(d);
|
||||
} catch (e) {
|
||||
log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString());
|
||||
return Promise.reject(d);
|
||||
}
|
||||
});
|
||||
if (cb) {
|
||||
return void response.then(cb, (err) => cb(null, err));
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
},
|
||||
isConnected : function() { return true; },
|
||||
getConnection : function() { return connection; },
|
||||
close : function() {
|
||||
if (connection)
|
||||
connection.close();
|
||||
},
|
||||
};
|
||||
// no need for header
|
||||
document.getElementsByTagName("header")[0].style="display:none";
|
||||
// force connection attempt automatically
|
||||
setTimeout(function() {
|
||||
getInstalledApps(true).catch(err => {
|
||||
showToast("Device connection failed, "+err,"error");
|
||||
});
|
||||
}, 100);
|
||||
}
|
|
@ -179,5 +179,6 @@
|
|||
<script src="core/js/appinfo.js"></script>
|
||||
<script src="core/js/index.js"></script>
|
||||
<script src="core/js/pwa.js" defer></script>
|
||||
<script src="gadgetbridge.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -139,9 +139,9 @@ Bangle.on("GPS", function(fix) {
|
|||
if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]);
|
||||
if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]);
|
||||
if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]);
|
||||
if (state.notify.dist.increment > 0 && state.notify.dist.next <= stats["dist"]) {
|
||||
if (state.notify.dist.increment > 0 && state.notify.dist.next <= state.distance) {
|
||||
stats["dist"].emit("notify",stats["dist"]);
|
||||
state.notify.dist.next = stats["dist"] + state.notify.dist.increment;
|
||||
state.notify.dist.next = state.notify.dist.next + state.notify.dist.increment;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -152,7 +152,7 @@ Bangle.on("step", function(steps) {
|
|||
state.lastStepCount = steps;
|
||||
if (state.notify.step.increment > 0 && state.notify.step.next <= steps) {
|
||||
stats["step"].emit("notify",stats["step"]);
|
||||
state.notify.step.next = steps + state.notify.step.increment;
|
||||
state.notify.step.next = state.notify.step.next + state.notify.step.increment;
|
||||
}
|
||||
});
|
||||
Bangle.on("HRM", function(h) {
|
||||
|
@ -285,7 +285,7 @@ exports.getStats = function(statIDs, options) {
|
|||
}
|
||||
if (state.notify.time.increment > 0 && state.notify.time.next <= now) {
|
||||
stats["time"].emit("notify",stats["time"]);
|
||||
state.notify.time.next = now + state.notify.time.increment;
|
||||
state.notify.time.next = state.notify.time.next + state.notify.time.increment;
|
||||
}
|
||||
}, 1000);
|
||||
function reset() {
|
||||
|
@ -299,6 +299,8 @@ exports.getStats = function(statIDs, options) {
|
|||
state.curSpeed = 0;
|
||||
state.BPM = 0;
|
||||
state.BPMage = 0;
|
||||
state.thisGPS = {};
|
||||
state.lastGPS = {};
|
||||
state.notify = options.notify;
|
||||
if (options.notify.dist.increment > 0) {
|
||||
state.notify.dist.next = state.distance + options.notify.dist.increment;
|
||||
|
@ -338,7 +340,7 @@ exports.appendMenuItems = function(menu, settings, saveSettings) {
|
|||
}
|
||||
exports.appendNotifyMenuItems = function(menu, settings, saveSettings) {
|
||||
var distNames = ['Off', "1000m","1 mile","1/2 Mthn", "Marathon",];
|
||||
var distAmts = [0, 1000,1609,21098,42195];
|
||||
var distAmts = [0, 1000, 1609, 21098, 42195];
|
||||
menu['Ntfy Dist'] = {
|
||||
min: 0, max: distNames.length-1,
|
||||
value: Math.max(distAmts.indexOf(settings.notify.dist.increment),0),
|
||||
|
|