Merge branch 'espruino:master' into master

pull/1633/head
KungPhoo 2022-03-27 19:07:58 +02:00 committed by GitHub
commit c4bce45288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 1851 additions and 460 deletions

140
apps/2047pp/2047pp.app.js Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

9
apps/2047pp/README.md Normal file
View File

@ -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).
![Screenshot](./2047pp_screenshot.png)

1
apps/2047pp/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))

BIN
apps/2047pp/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

15
apps/2047pp/metadata.json Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
0.01: Initial upload
0.2: Added scrollable calendar and swipe gestures
0.3: Configurable drag gestures

View File

@ -9,25 +9,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu
|![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds|
|![big calendar](screenshot3.png)|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)

View File

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

View File

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

View File

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

1
apps/cycling/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial version

34
apps/cycling/README.md Normal file
View File

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

111
apps/cycling/blecsc-emu.js Normal file
View File

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

150
apps/cycling/blecsc.js Normal file
View File

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

453
apps/cycling/cycling.app.js Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

57
apps/cycling/settings.js Normal file
View File

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

View File

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

View File

@ -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.
![Screenshot from the Banglejs 2 watch with the game in dark theme](./scrnshot_dn_300.jpg)
![Screenshot from the Banglejs 2 watch with the game in light theme](./scrnshot_lc_300.jpg)
In Dark theme with numbers:
![Screenshot from the Banglejs 2 watch with the game in dark theme](./game1024_sc_dump_dark.png)
In Light theme with characters:
![Screenshot from the Banglejs 2 watch with the game in light theme](./game1024_sc_dump_light.png)

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

1
apps/gsat/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Added Source Code

3
apps/gsat/README.md Normal file
View File

@ -0,0 +1,3 @@
# Geek Squad Appointment Timer
An app dedicated to setting a 20 minute timer for Geek Squad Appointments.

1
apps/gsat/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV"))

38
apps/gsat/app.js Normal file
View File

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

BIN
apps/gsat/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

16
apps/gsat/metadata.json Normal file
View File

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

BIN
apps/gsat/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -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": [

View File

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

View File

@ -1 +1,2 @@
0.01: First release
0.02: Make sure to reset turns

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Make Bangle.js 2 compatible

View File

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

View File

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

View File

@ -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": [

162
gadgetbridge.js Normal file
View File

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

View File

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

View File

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