Merge remote-tracking branch 'upstream/master'
|
@ -2,4 +2,5 @@ apps/animclk/V29.LBM.js
|
|||
apps/banglerun/rollup.config.js
|
||||
apps/schoolCalendar/fullcalendar/main.js
|
||||
apps/authentiwatch/qr_packed.js
|
||||
apps/qrcode/qr-scanner.umd.min.js
|
||||
*.test.js
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
name: Node CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository and submodules
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: install testing dependencies
|
||||
run: npm i
|
||||
- name: test all apps and widgets
|
||||
run: npm run test
|
||||
- name: install typescript dependencies
|
||||
working-directory: ./typescript
|
||||
run: npm ci
|
||||
- name: build types
|
||||
working-directory: ./typescript
|
||||
run: npm run build:types
|
||||
- name: build all TS apps and widgets
|
||||
working-directory: ./typescript
|
||||
run: npm run build
|
|
@ -1,3 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
13
README.md
|
@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer.
|
|||
},
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
|
||||
In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { url:"screenshot.png" } ],`
|
||||
|
||||
To get a screenshot you can:
|
||||
|
||||
* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then
|
||||
right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is
|
||||
unable to read data back from the LCD controller).
|
||||
* Run your code in the emulator and use the screenshot button in the bottom right of the window.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
### Online
|
||||
|
@ -243,6 +255,7 @@ and which gives information about the app for the Launcher.
|
|||
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
|
||||
"type":"...", // optional(if app) -
|
||||
// 'app' - an application
|
||||
// 'clock' - a clock - required for clocks to automatically start
|
||||
// 'widget' - a widget
|
||||
// 'launch' - replacement launcher app
|
||||
// 'bootloader' - code that runs at startup only
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
class TwoK {
|
||||
constructor() {
|
||||
this.b = Array(4).fill().map(() => Array(4).fill(0));
|
||||
this.score = 0;
|
||||
this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
|
||||
}
|
||||
drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
|
||||
g.setColor(c);
|
||||
for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
|
||||
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
|
||||
}
|
||||
render() {
|
||||
const yo = 20;
|
||||
const xo = yo/2;
|
||||
h = g.getHeight()-yo;
|
||||
w = g.getWidth()-yo;
|
||||
bh = Math.floor(h/4);
|
||||
bw = Math.floor(w/4);
|
||||
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
|
||||
g.setFont("Vector", 16).setColor("#fff").drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
|
||||
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
|
||||
for (y=0; y<4; ++y)
|
||||
for (x=0; x<4; ++x) {
|
||||
b = this.b[y][x];
|
||||
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
|
||||
if (b > 4) g.setColor(1, 1, 1);
|
||||
else g.setColor(0, 0, 0);
|
||||
g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
|
||||
if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
|
||||
}
|
||||
}
|
||||
shift(d) { // +/-1: shift x, +/- 2: shift y
|
||||
var crc = E.CRC32(this.b.toString());
|
||||
if (d==-1) { // shift x left
|
||||
for (y=0; y<4; ++y) {
|
||||
for (x=2; x>=0; x--)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
|
||||
this.b[y][3] = 0;
|
||||
}
|
||||
for (x=0; x<3; ++x)
|
||||
if (this.b[y][x]==this.b[y][x+1]) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y][x+1];
|
||||
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
|
||||
this.b[y][3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (d==1) { // shift x right
|
||||
for (y=0; y<4; ++y) {
|
||||
for (x=1; x<4; x++)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
|
||||
this.b[y][0] = 0;
|
||||
}
|
||||
for (x=3; x>0; --x)
|
||||
if (this.b[y][x]==this.b[y][x-1]) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y][x-1] ;
|
||||
for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
|
||||
this.b[y][0] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (d==-2) { // shift y down
|
||||
for (x=0; x<4; ++x) {
|
||||
for (y=1; y<4; y++)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
|
||||
this.b[0][x] = 0;
|
||||
}
|
||||
for (y=3; y>0; y--)
|
||||
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y-1][x];
|
||||
for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
|
||||
this.b[0][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (d==2) { // shift y up
|
||||
for (x=0; x<4; ++x) {
|
||||
for (y=2; y>=0; y--)
|
||||
if (this.b[y][x]==0) {
|
||||
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
|
||||
this.b[3][x] = 0;
|
||||
}
|
||||
for (y=0; y<3; ++y)
|
||||
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
|
||||
this.score += 2*this.b[y][x];
|
||||
this.b[y][x] += this.b[y+1][x];
|
||||
for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
|
||||
this.b[3][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (E.CRC32(this.b.toString())!=crc);
|
||||
}
|
||||
addDigit() {
|
||||
var d = Math.random()>0.9 ? 4 : 2;
|
||||
var id = Math.floor(Math.random()*16);
|
||||
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
|
||||
this.b[Math.floor(id/4)][id%4] = d;
|
||||
}
|
||||
}
|
||||
|
||||
function dragHandler(e) {
|
||||
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
|
||||
var res = false;
|
||||
if (Math.abs(e.dx)>Math.abs(e.dy)) {
|
||||
if (e.dx>0) res = twok.shift(1);
|
||||
if (e.dx<0) res = twok.shift(-1);
|
||||
}
|
||||
else {
|
||||
if (e.dy>0) res = twok.shift(-2);
|
||||
if (e.dy<0) res = twok.shift(2);
|
||||
}
|
||||
if (res) twok.addDigit();
|
||||
twok.render();
|
||||
}
|
||||
}
|
||||
|
||||
function swipeHandler() {
|
||||
|
||||
}
|
||||
|
||||
function buttonHandler() {
|
||||
|
||||
}
|
||||
|
||||
var twok = new TwoK();
|
||||
twok.addDigit(); twok.addDigit();
|
||||
twok.render();
|
||||
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
|
||||
if (process.env.HWVERSION==1) {
|
||||
Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
|
||||
setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
|
||||
setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
|
||||
}
|
After Width: | Height: | Size: 4.4 KiB |
|
@ -0,0 +1,9 @@
|
|||
|
||||
# Game of 2047pp (2047++)
|
||||
|
||||
Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024.
|
||||
|
||||
Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using
|
||||
the top/bottom button (Bangle 1).
|
||||
|
||||

|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))
|
After Width: | Height: | Size: 759 B |
|
@ -0,0 +1,15 @@
|
|||
{ "id": "2047pp",
|
||||
"name": "2047pp",
|
||||
"shortName":"2047pp",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Bangle version of a tile shifting game",
|
||||
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"tags": "game",
|
||||
"storage": [
|
||||
{"name":"2047pp.app.js","url":"2047pp.app.js"},
|
||||
{"name":"2047pp.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
7x7DotsClock
|
||||
|
||||
by Peter Kuppelwieser
|
||||
|
||||
*/
|
||||
|
||||
let settings = Object.assign({ swupApp: "",swdownApp: "", swleftApp: "", swrightApp: "", ColorMinutes: ""}, require("Storage").readJSON("7x7dotsclock.json", true) || {});
|
||||
|
||||
// position on screen
|
||||
var Xs = 0, Ys = 30,Xe = 175, Ye=175;
|
||||
//const Xs = 0, Ys = 0,Xe = 175, Ye=175;
|
||||
var SegH = (Ye-Ys)/2,SegW = (Xe-Xs)/2;
|
||||
var Dx = SegW/14, Dy = SegH/16;
|
||||
|
||||
switch(settings.ColorMinutes) {
|
||||
case "blue":
|
||||
var mColor = [0.3,0.3,1];
|
||||
var sColor = [0,0,1];
|
||||
var sbColor = [1,1,1];
|
||||
break;
|
||||
case "pink":
|
||||
var mColor = [1,0.3,1];
|
||||
var sColor = [1,0,1];
|
||||
var sbColor = [1,1,1];
|
||||
break;
|
||||
case "green":
|
||||
var mColor = [0.3,1,0.3];
|
||||
var sColor = [0,1,0];
|
||||
var sbColor = [1,1,1];
|
||||
break;
|
||||
case "yellow":
|
||||
var mColor = [1,1,0.3];
|
||||
var sColor = [1,1,0];
|
||||
var sbColor = [0,0,0];
|
||||
break;
|
||||
default:
|
||||
var sColor = [0,0,1];
|
||||
var mColor = [0.3,0.3,1];
|
||||
var sbColor = [1,1,1];
|
||||
}
|
||||
const bColor = [0.3,0.3,0.3];
|
||||
|
||||
const Font = [
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
],
|
||||
[
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0],
|
||||
],
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
],
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[0,0,0,1,1,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
],
|
||||
[
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,0,1,1,0,0],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[0,0,0,1,1,0,0],
|
||||
[0,0,0,1,1,0,0]
|
||||
],
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,1,1,1,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
],
|
||||
[
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,0,0,0,0,0],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
],
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[0,0,0,0,0,1,1]
|
||||
],
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1]
|
||||
],
|
||||
[
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,0,0,0,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[1,1,1,1,1,1,1],
|
||||
[0,0,0,0,0,1,1],
|
||||
[0,0,0,0,0,1,1]
|
||||
],
|
||||
];
|
||||
|
||||
// Global Vars
|
||||
var dho = -1, eho = -1, dmo = -1, emo = -1;
|
||||
|
||||
|
||||
function drawHSeg(x1,y1,x2,y2,Num,Color,Size) {
|
||||
|
||||
|
||||
g.setColor(g.theme.bg);
|
||||
g.fillRect(x1, y1, x2, y2);
|
||||
for (let i = 1; i < 8; i++) {
|
||||
for (let j = 1; j < 8; j++) {
|
||||
if (Font[Num][j-1][i-1] == 1) {
|
||||
if (Color == "fg") {
|
||||
g.setColor(g.theme.fg);
|
||||
} else {
|
||||
g.setColor(mColor[0],mColor[1],mColor[2]);
|
||||
}
|
||||
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,Size);
|
||||
} else {
|
||||
g.setColor(bColor[0],bColor[1],bColor[2]);
|
||||
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawSSeg(x1,y1,x2,y2,Num,Color,Size) {
|
||||
for (let i = 1; i < 8; i++) {
|
||||
for (let j = 1; j < 8; j++) {
|
||||
if (Font[Num][j-1][i-1] == 1) {
|
||||
if (Color == "fg") {
|
||||
g.setColor(sColor[0],sColor[1],sColor[2]);
|
||||
} else {
|
||||
g.setColor(g.theme.fg);
|
||||
//g.setColor(0.7,0.7,0.7);
|
||||
}
|
||||
g.fillCircle(x1+(i-1)*(x2-x1)/7,y1+(j-1)*(y2-y1)/7,Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function ShowSeconds() {
|
||||
|
||||
g.setColor(sbColor[0],sbColor[1],sbColor[2]);
|
||||
|
||||
g.fillRect((Xe-Xs) / 2 - 14 + Xs -4,
|
||||
(Ye-Ys) / 2 - 7 + Ys -4,
|
||||
(Xe-Xs) / 2 + 14 + Xs +4,
|
||||
(Ye-Ys) / 2 + 7 + Ys +4);
|
||||
|
||||
|
||||
drawSSeg( (Xe-Xs) / 2 - 14 + Xs -1,
|
||||
(Ye-Ys) / 2 - 7 + Ys +1,
|
||||
(Xe-Xs) / 2 + Xs -1,
|
||||
(Ye-Ys) / 2 + 7 + Ys +1,
|
||||
ds,"fg",1);
|
||||
|
||||
drawSSeg( (Xe-Xs) / 2 + Xs +2,
|
||||
(Ye-Ys) / 2 - 7 + Ys +1,
|
||||
(Xe-Xs) / 2 + 14 + Xs +2,
|
||||
(Ye-Ys) / 2 + 7 + Ys +1,
|
||||
es,"fg",1);
|
||||
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// work out how to display the current time
|
||||
var d = new Date();
|
||||
var h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
|
||||
|
||||
|
||||
dh = Math.floor(h/10);
|
||||
eh = h - dh * 10;
|
||||
|
||||
dm = Math.floor(m/10);
|
||||
em = m - dm * 10;
|
||||
|
||||
ds = Math.floor(s/10);
|
||||
es = s - ds * 10;
|
||||
|
||||
|
||||
// Reset the state of the graphics library
|
||||
g.reset();
|
||||
if (dh != dho) {
|
||||
g.setColor(1,1,1);
|
||||
drawHSeg(Xs, Ys, Xs+SegW, Ys+SegH,dh,"fg",4);
|
||||
dho = dh;
|
||||
}
|
||||
|
||||
if (eh != eho) {
|
||||
g.setColor(1,1,1);
|
||||
drawHSeg(Xs+SegW+Dx, Ys, Xs+SegW*2, Ys+SegH,eh,"fg",4);
|
||||
eho = eh;
|
||||
}
|
||||
|
||||
if (dm != dmo) {
|
||||
g.setColor(0.3,0.3,1);
|
||||
drawHSeg(Xs, Ys+SegH+Dy, Xs+SegW, Ys+SegH*2,dm,"",4);
|
||||
dmo = dm;
|
||||
}
|
||||
|
||||
if (em != emo) {
|
||||
g.setColor(0.3,0.3,1);
|
||||
drawHSeg(Xs+SegW+Dx, Ys+SegH+Dy, Xs+SegW*2, Ys+SegH*2,em,"",4);
|
||||
emo = em;
|
||||
}
|
||||
|
||||
if (!Bangle.isLocked()) ShowSeconds();
|
||||
|
||||
}
|
||||
|
||||
|
||||
function actions(v){
|
||||
if(BTN1.read() === true) {
|
||||
print("BTN pressed");
|
||||
Bangle.showLauncher();
|
||||
}
|
||||
|
||||
if(v==-1){
|
||||
print("up swipe event");
|
||||
if(settings.swupApp != "") load(settings.swupApp);
|
||||
print(settings.swupApp);
|
||||
} else if(v==1) {
|
||||
print("down swipe event");
|
||||
if(settings.swdownApp != "") load(settings.swdownApp);
|
||||
print(settings.swdownApp);
|
||||
} else {
|
||||
print("touch event");
|
||||
}
|
||||
}
|
||||
|
||||
// Get Messages status
|
||||
var messages = require("Storage").readJSON("messages.json",1)||[];
|
||||
|
||||
//var BTconnected = NRF.getSecurityStatus().connected;
|
||||
//NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected);
|
||||
//NRF.on('disconnect',BTconnected = NRF.getSecurityStatus().connected);
|
||||
|
||||
|
||||
function drawWidgeds() {
|
||||
|
||||
//Bluetooth
|
||||
//print(BluetoothDevice.connected);
|
||||
var x1Bt = 160;
|
||||
var y1Bt = 0;
|
||||
var x2Bt = x1Bt + 30;
|
||||
var y2Bt = y2Bt;
|
||||
|
||||
if (NRF.getSecurityStatus().connected)
|
||||
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
|
||||
else
|
||||
g.setColor(g.theme.dark ? "#666" : "#999");
|
||||
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
|
||||
|
||||
|
||||
//Battery
|
||||
//print(E.getBattery());
|
||||
//print(Bangle.isCharging());
|
||||
|
||||
var x1B = 130;
|
||||
var y1B = 2;
|
||||
var x2B = x1B + 20;
|
||||
var y2B = y1B + 15;
|
||||
|
||||
g.setColor(g.theme.bg);
|
||||
g.clearRect(x1B,y1B,x2B,y2B);
|
||||
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawRect(x1B,y1B,x2B,y2B);
|
||||
g.fillRect(x1B,y1B,x1B+(E.getBattery()*(x2B-x1B)/100),y2B);
|
||||
g.fillRect(x2B,y1B+(y2B-y1B)/2-3,x2B+4,y1B+(y2B-y1B)/2+3);
|
||||
|
||||
|
||||
|
||||
//Messages
|
||||
|
||||
var x1M = 100;
|
||||
var y1M = y1B;
|
||||
var x2M = x1M + 25;
|
||||
var y2M = y2B;
|
||||
|
||||
if (messages.some(m=>m.new)) {
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillRect(x1M,y1M,x2M,y2M);
|
||||
g.setColor(g.theme.bg);
|
||||
g.drawLine(x1M,y1M,x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2);
|
||||
g.drawLine(x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2,x2M,y1M);
|
||||
}
|
||||
|
||||
var strDow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||||
var d = new Date();
|
||||
var dow = d.getDay(),day = d.getDate(), month = d.getMonth() + 1, year = d.getFullYear();
|
||||
|
||||
print(strDow[dow] + ' ' + day + '.' + month + ' ' + year);
|
||||
|
||||
g.setColor(g.theme.fg);
|
||||
g.setFontAlign(-1, -1,0);
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString(strDow[dow] + ' ' + day, 0, 0, true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function SetFull(on) {
|
||||
dho = -1; eho = -1; dmo = -1; emo = -1;
|
||||
g.clear();
|
||||
|
||||
if (on === true) {
|
||||
Ys = 0;
|
||||
Bangle.setUI("clock");
|
||||
Bangle.on('swipe', function(direction) { });
|
||||
|
||||
} else {
|
||||
Ys = 30;
|
||||
Bangle.setUI("updown",actions);
|
||||
Bangle.on('swipe', function(direction) {
|
||||
switch (direction) {
|
||||
case 1:
|
||||
print("swipe left event");
|
||||
if(settings.swleftApp != "") load(settings.swleftApp);
|
||||
print(settings.swleftApp);
|
||||
break;
|
||||
case -1:
|
||||
print("swipe right event");
|
||||
if(settings.swrightApp != "") load(settings.swrightApp);
|
||||
print(settings.swrightApp);
|
||||
break;
|
||||
default:
|
||||
print("swipe undefined event");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SegH = (Ye-Ys)/2;
|
||||
Dy = SegH/16;
|
||||
|
||||
draw();
|
||||
|
||||
if (on != true) {
|
||||
//Bangle.loadWidgets();
|
||||
//Bangle.drawWidgets();
|
||||
drawWidgeds();
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.on('lock', function(on) {
|
||||
SetFull(on);
|
||||
});
|
||||
|
||||
|
||||
SetFull(Bangle.isLocked());
|
||||
|
||||
var secondInterval = setInterval(draw, 1000);
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkEBAkTmEzkAHDmcjmQBBmcTmICCgMAiMAkE/+P/mEQgMQgH/n/zAIP/l/yA4QvXC4kDkEjFgIACkcSmMTkMyBoQHBI4kvI6wXBn8wA4c/mfzl8y+cfEoIaBVa5HBAAMQF4UgIoIBBBgJNBAwQ3BkfygSnJSQIUBkECiBoCL48DmCPFAA6PCX40jX4hYEU4LNBX4JHIkBHCBgJHBianKj8wO4IvHgSnBmJ3CHYqGCABcRcYTXLAA5KCFAJfCC4KnDX4anNgUgiSnMkQQBO5hvCl8yO4pHEd4oyBH4QBBU5TXHkcimUTkLXFL44HEiTbBO4MhBoQHBI4KECR45HGBoIFBU4y/BC4c/mYXGMQJHFiBHLEAIHCf5gAKhWg1UB0IEBjUA0MB0EAjQKCiANCCQOg0cxmcSmWjU4MqmcDmSnDBASkBmejCQIXFmYXEmYXHicyhRLC0AEBAIJFBAIIFCBAYHDF65fXR66vImUCnS8IkeinUBgERgEgcIMBgRHDBgLvCBYMQmcjBYIAHfwL7JiQLBichkcSnUSO4MhI4MxI5MSmMjPgMinCnCkRHGIgJHFiUgkUalUCAgMRkUCkIvIkUSkMC0EiBxAAI0UKkBHCkCPDgA+CI5Z3BmYPBAB53CV4MSEgcSiCnOR4cyR5JQEgBHCC4I0BC4UjC4MCxQXGF4IlBxRHB0UAlUK0BMBkIEBI5ILB0ZHBF4czlTXHI4mjCQIXOH4KnDC4MKgGqgGgAgIBBIoJHJBoQ="))
|
|
@ -0,0 +1,88 @@
|
|||
(function(back) {
|
||||
|
||||
let settings = Object.assign({ swupApp: "",swdownApp: "", swleftApp: "", swrightApp: "",ColorMinutes: ""}, require("Storage").readJSON("7x7dotsclock.json", true) || {});
|
||||
|
||||
|
||||
|
||||
function setSetting(key,value) {
|
||||
print("call " + key + " = " + value);
|
||||
settings[key] = value;
|
||||
|
||||
print("storing settings 7x7dotsclock.json");
|
||||
storage.write('7x7dotsclock.json', settings);
|
||||
}
|
||||
|
||||
|
||||
// Helper method which uses int-based menu item for set of string values
|
||||
function stringItems(key, startvalue, values) {
|
||||
return {
|
||||
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
|
||||
format: v => values[v],
|
||||
min: 0,
|
||||
max: values.length - 1,
|
||||
wrap: true,
|
||||
step: 1,
|
||||
onchange: v => {
|
||||
setSetting(key,values[v]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper method which breaks string set settings down to local settings object
|
||||
function stringInSettings(name, values) {
|
||||
return stringItems(name,settings[name], values);
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
const mainMenu = {
|
||||
"": {"title": "7x7 Dots Clock Settings"},
|
||||
"< Back": ()=>load(),
|
||||
"Minutes": stringInSettings("ColorMinutes", ["blue","pink","green","yellow"]),
|
||||
"swipe-up": ()=>showSelAppMenu("swupApp"),
|
||||
"swipe-down": ()=>showSelAppMenu("swdownApp"),
|
||||
"swipe-left": ()=>showSelAppMenu("swleftApp"),
|
||||
"swipe-right": ()=>showSelAppMenu("swrightApp")
|
||||
|
||||
};
|
||||
|
||||
E.showMenu(mainMenu);
|
||||
}
|
||||
|
||||
|
||||
function showSelAppMenu(key) {
|
||||
var Apps = require("Storage").list(/\.info$/)
|
||||
.map(app => {var a=storage.readJSON(app, 1);return (
|
||||
a&&a.name != "Launcher"
|
||||
&& a&&a.name != "Bootloader"
|
||||
&& a&&a.type != "clock"
|
||||
&& a&&a.type !="widget"
|
||||
)?a:undefined})
|
||||
.filter(app => app) // filter out any undefined apps
|
||||
.sort((a, b) => a.sortorder - b.sortorder);
|
||||
const SelAppMenu = {
|
||||
'': {
|
||||
'title': /*LANG*/'Select App',
|
||||
},
|
||||
'< Back': ()=>showMainMenu(),
|
||||
};
|
||||
Apps.forEach((app, index) => {
|
||||
var label = app.name;
|
||||
if (settings[key] === app.src) {
|
||||
label = "* " + label;
|
||||
}
|
||||
SelAppMenu[label] = () => {
|
||||
if (settings[key] !== app.src) {
|
||||
setSetting(key,app.src);
|
||||
showMainMenu();
|
||||
}
|
||||
};
|
||||
});
|
||||
if (Apps.length === 0) {
|
||||
SelAppMenu[/*LANG*/"No Apps Found"] = () => { };
|
||||
}
|
||||
return E.showMenu(SelAppMenu);
|
||||
}
|
||||
|
||||
showMainMenu();
|
||||
|
||||
})
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial version for upload
|
||||
0.02: better theme support, configurable colors, small improvements
|
|
@ -0,0 +1,15 @@
|
|||
# 7x7 dots clock
|
||||
|
||||

|
||||
|
||||
* A Clock with big numbers made of 7x7 dots
|
||||
* system widgeds ar not (yet) supported
|
||||
* when screen is locked it shows hours and minutes in full screen mode
|
||||
* adjustable color for minutes and seconds
|
||||
|
||||

|
||||
|
||||
* when screen is unlocked it shows additional info: bluetooth, battery, new message state, date and seconds
|
||||
* you can configure an app per swipe direction
|
||||
* when swiping the configured apps are launched
|
||||
* button press opens launcher
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,19 @@
|
|||
{ "id": "7x7dotsclock",
|
||||
"name": "7x7 Dots Clock",
|
||||
"shortName":"7x7 Dots Clock",
|
||||
"version":"0.02",
|
||||
"description": "A clock with a big 7x7 dots Font",
|
||||
"icon": "dotsfontclock.png",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"7x7dotsclock.app.js","url":"7x7dotsclock.app.js"},
|
||||
{"name":"7x7dotsclock.settings.js","url":"7x7dotsclock.settings.js"},
|
||||
{"name":"7x7dotsclock.img","url":"7x7dotsclock.img.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"7x7dotsclock.json"}],
|
||||
"screenshots": [{"url":"dotsfontclock.png"},{"url":"dotsfontclock-scr1.png"},{"url":"dotsfontclock-scr2.png"}]
|
||||
}
|
|
@ -13,4 +13,4 @@
|
|||
Widgets now shown on Alarm screen
|
||||
0.13: Alarm widget state now updates when setting/resetting an alarm
|
||||
0.14: Order of 'back' menu item
|
||||
0.15: Added missing menu 'back' in timer
|
||||
0.15: Fix hour/minute wrapping code for new menu system
|
|
@ -73,12 +73,12 @@ function editAlarm(alarmIndex) {
|
|||
'': { 'title': /*LANG*/'Alarm' },
|
||||
/*LANG*/'< Back' : showMainMenu,
|
||||
/*LANG*/'Hours': {
|
||||
value: hrs,
|
||||
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
value: hrs, min : 0, max : 23, wrap : true,
|
||||
onchange: v => hrs=v
|
||||
},
|
||||
/*LANG*/'Minutes': {
|
||||
value: mins,
|
||||
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
value: mins, min : 0, max : 59, wrap : true,
|
||||
onchange: v => mins=v
|
||||
},
|
||||
/*LANG*/'Enabled': {
|
||||
value: en,
|
||||
|
@ -139,12 +139,12 @@ function editTimer(alarmIndex) {
|
|||
'': { 'title': /*LANG*/'Timer' },
|
||||
/*LANG*/'< Back' : showMainMenu,
|
||||
/*LANG*/'Hours': {
|
||||
value: hrs,
|
||||
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
value: hrs, min : 0, max : 23, wrap : true,
|
||||
onchange: v => hrs=v
|
||||
},
|
||||
/*LANG*/'Minutes': {
|
||||
value: mins,
|
||||
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
value: mins, min : 0, max : 59, wrap : true,
|
||||
onchange: v => mins=v
|
||||
},
|
||||
/*LANG*/'Enabled': {
|
||||
value: en,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Default Alarm & Timer",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.14",
|
||||
"version": "0.15",
|
||||
"description": "Set and respond to alarms and timers",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm,widget",
|
||||
|
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A"))
|
||||
require("heatshrink").decompress(atob("mEkgIRO4AFJgPgAocDAoswAocHAokGjAFDhgFFhgFDjEOAoc4gxSE44FDuPjAod//+AAoXfn4FCgPMjJUCmIJBAoU7AoJUCv4CBsACBtwCBuACB4w3CEQIaCKgMBFgQFBgYFCLQMDMIfAg55D4BcDg/gNAcD+B0DSIMcOgiGEjCYEjgFEhhVCUgQ"))
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
when weekday name "On": weekday name is cut at 6th position and .#<week num> is added
|
||||
0.06: fixes #1271 - wrong settings name
|
||||
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
|
||||
week is buffered until date or timezone changes
|
||||
week is buffered until date or timezone changes
|
||||
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "antonclk",
|
||||
"name": "Anton Clock",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
},
|
||||
"< Back": () => back(),
|
||||
"Seconds...": () => E.showMenu(secmenu),
|
||||
"Date": stringInSettings("dateOnMain", ["Short", "Long", "ISO8601"]),
|
||||
"Date": stringInSettings("dateOnMain", ["Long", "Short", "ISO8601"]),
|
||||
"Show Weekday": {
|
||||
value: (settings.weekDay !== undefined ? settings.weekDay : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
|
@ -56,7 +56,7 @@
|
|||
}
|
||||
},
|
||||
"Uppercase": {
|
||||
value: (settings.upperCase !== undefined ? settings.upperCase : false),
|
||||
value: (settings.upperCase !== undefined ? settings.upperCase : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.upperCase = v;
|
||||
|
@ -81,7 +81,7 @@
|
|||
"< Back": () => E.showMenu(mainmenu),
|
||||
"Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]),
|
||||
"With \":\"": {
|
||||
value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : false),
|
||||
value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.secondsWithColon = v;
|
||||
|
@ -89,14 +89,14 @@
|
|||
}
|
||||
},
|
||||
"Color": {
|
||||
value: (settings.secondsColoured !== undefined ? settings.secondsColoured : false),
|
||||
value: (settings.secondsColoured !== undefined ? settings.secondsColoured : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.secondsColoured = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
"Date": stringInSettings("dateOnSecs", ["No", "Year", "Weekday"])
|
||||
"Date": stringInSettings("dateOnSecs", ["Year", "Weekday", "No"])
|
||||
};
|
||||
|
||||
// Actually display the menu
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
0.01: New App!
|
||||
0.02: Icons, loading screen
|
||||
0.03: Random icon, Shorter "loading" screen
|
||||
0.04: Support for light and dark Themes
|
||||
0.05: Small bugfix
|
||||
0.06: Formatting
|
||||
0.07: Added potato GLaDOS and quote functionality when you tap her
|
||||
0.08: Fixed drawing issues with the quotes and added more
|
|
@ -0,0 +1,11 @@
|
|||
# Description
|
||||
|
||||
This is a simple clock based on the Portal Series.
|
||||
|
||||
# Features
|
||||
|
||||
The button in the center of the screen is interactable and the warning image will change when it is pressed.
|
||||
|
||||
Potato GLaDOS in the bottom left corner is interactable and will display a quote when tapped. (You can add more quotes by editing the `aptsciclkquotes.txt` file seperating each quote with a `^`)
|
||||
|
||||
When the app loads the Apeture Science Logo is displayed.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwgNKxAACEaIVDDKWAhAXGwAtODA4HBLR4YFD4QWICIhABGAoMBJRBZHC4wwHOQ4IFAgQwGUQ4YBAg4uMJIwDDGAjRLIgYLHc5gXJIwbKLC4hICb4gZKfAhgETJKHJLwwXRUooWKCImAJogXRMopGMNwkIC4oWLYYqtHC5rFJC5h0GIxwsGFyD8CC4wwOIxBIQFwoeBCxrwEFwYXTFgTXReI4uQC4apPC4xNERqBlGFx4XCeJ4nHD4kIIxY3KPoIxNBwYXEJRInEP44iGOgwXFBYYcDChCHHC4wMBC5BnJEoouMGAYXEJJCCJC4pOEcpYKBFIpJFZRQXGD4gWKXBUICxjdFIhwyJOJMAA="))
|
|
@ -0,0 +1,368 @@
|
|||
const big = g.getWidth()>200;
|
||||
const timeFontSize = big?5:4;
|
||||
const dateFontSize = big?3:2;
|
||||
const gmtFontSize = 2;
|
||||
const font = "6x8";
|
||||
|
||||
const xyCenter = g.getWidth() / 2;
|
||||
const yposTime = xyCenter*0.73;
|
||||
const yposDate = xyCenter*0.48;
|
||||
const yposYear = xyCenter*1.8;
|
||||
|
||||
const buttonTolerance = 20;
|
||||
const buttonX = 88;
|
||||
const buttonY = 104;
|
||||
|
||||
var pause = false; //set to true to pause any sort of drawing (except for quotes)
|
||||
|
||||
function getImg(img){
|
||||
if (img == "w0"){//drink
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOZFIQOD4EABwnwgEDBwf8g/4h4ODwYQBv4OC+AbDAIP+j/HAQIOC4Hwj4RBBwP8o8B/+PBwWOkEP/l/BwP4+JCB44OCj+Ih/+n4OB+PEoP38YOB/0YkUXGgIOB8cBi9f+IOCkEI+XvBwXigFG64OEg0/t4OEuP7BwkHx/PBwWigF8voOC+Uwg/ig4OCkMgv8QsIOB+cfSoOGLIUR/E/4ljBwPxx/B/0kO4UI/0P+J3C/HHVQOISoWEn+D/iPBBwIwC8IOCwcP84IBBwU4TAMHBwfAv+AcARBBgD3CBwX8gDnBBwfwewIODAgIABBwYHDB3oAEBwIHFByyDBABg"))
|
||||
}
|
||||
}
|
||||
else if (img == "w1"){//cube dispenser
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOI/3/+fvBwYEBnwO/By3APgN/O6IeBh4OF8AOcwADCBwX8g4dM/8fBwt774OE+/9Bwt/BxodH3oOcFgyVG8BhCBwX8hRwCBwXA0C6BBwc/w4OE41MBwtEo6VF84sE/1/54OLDo4sHHYxKHLIxoGO44AD/kAABo"))
|
||||
}
|
||||
}
|
||||
else if (img == "w2"){//acid
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOF+IOGngOF8/D8YGD/wdBB4nv4fzAwf4BwOfGQd/4f7/+//+f74OB4PwHIJKDx8P/4BBBwP8BwIBBBwXvh+Hw5ZD+Pwnl/NAcegJOBBwfgj0fBwvhBxcPgYEBBwXw/F+FghIB84OC/BfBOYQOBk/w/0f4f4nkGgFgh0hwED4H4jOBuF8hk/v/Hzlnx/zFgQZBGYLCD4EHaIn8gAOF8EDBwn+dgQOK/8AN4IOD+EABww0BBwqGEBwIWBBwk8CwIODg/gv4OEv4OD+4OBBAIOBRYIFBh+PcAQdC+gOCDoN+h/vBwPP/wOB/wOBwJCBBwP2oa3BLALgBiA7BOwIvB/+DQoV/d4hPBBwQsB/wJB8ZoEAAZoDAAQOPRQIAM"))
|
||||
}
|
||||
}
|
||||
else if (img == "w3"){//turret
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOi+IOGh4OF8AOF/UNBwthx4OE+0YBwtBh4OE6mQBwn7rEfBwl22IOE99gBwn99UzBwUc/+90YsC8HH+++n98n/+g0++2Z+4OB4Fz73T74OCg877d8/YdC+d7u/v3gsBjEvt/+O4X+gvtIgI7CwG934OD8E326kD/0A+yzEwEO74OD/EArYOEgEDv4OD+PAl4OEnkBaInz0EPBwk3iAdE+XwSIYDBj2Oj4OD/fYvIOEvdHz4OD99unIOD/vt44OE3u4Dou3h4OE+3x/IOE70/Bwn78/9Bwl4LAQ7Dx75DBwP4Awb+EBwgAEBz0AABo="))
|
||||
}
|
||||
}
|
||||
else if (img == "w4"){//falling cube
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOC+YOF/0PBwvgv4OE/kFBwvAyIdFnYeBBwYeDDofng4OE8vYDonx7uPBwkf/+/Bwfh+czBwf+g/5z4OD+FevIdEhMDDon/0E3BwgeBJQgeB+5ZFvAEBBwfzgYOEw/XLInwn3BBwf8gH4LYIOCwUHDonwmE4HYkHwKkE8P4XYQOCv7dCYQkBWYsAWYvAiAsE/EDJQn/wF+CwJZDg/gBwgrBXYIOC8D+FNAL+F4eDBwn4nh2BBweHFYJ3EFYQOC/0P/AOECgIOE/E/BwsHBwvACAIODWAQOEJAIOFAgIOEQ4QsEAAOfBwoACBwgACBw8AABo"))
|
||||
}
|
||||
}
|
||||
else if (img == "w5"){//ball
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOiv4OF8YOFAgQOEyYdGBw3zBw0BBwv4j4OB+EAgOD84OE+/ev4dD/3+BwvcugsE/u7t0f4aRC7e2sF8Bwlxg4dEu8YBwYsB/HDHYsMngOB8EDweHDon//PADoYABz0PBwfwnJKE/0OjZZC/kB4Hxz4OCwEYh+wBwXwgeA/+HBwUP8EP/0/BwPj/0DCQIOB/l/4DQBw4OBDIMPUoJKB+H/wY+B44OBj/4CoJKC+P/g7+FBAL+Fj4OFbwIOEI4IOF8YO6JQwAEaIgORgAANA"))
|
||||
}
|
||||
}
|
||||
else if (img == "w6"){//ball recviver
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOR/YOG34Ob/e7Bwu7CwQOhGgQOD34OF/0LBwvfv4dMuPfBwn29oOFtwONDowsHHY3+h7CNj4OF+IOc4A7NDo7gGJQ4ACBwX+//vBwnvBAIOK8EH/kBBwd+v/PSwIOB/fnjiWBBwXesHPLQIOB/2AgEvBwfgh0AFgf8gAuBLKQObgAANA=="))
|
||||
}
|
||||
}
|
||||
else if (img == "w7"){//falling portals
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YFE/H8BwtvBwvvvgOE/33Bwvf3gOE/v7Bxn5Bw2fHYv7/oOF3/cB118JQQOC4ODJQn8jEfLInBjBoE/0jO4pjD953CwCVF/EH5//+ykCwA8Cp4OB/MDz4DBEQUYjPzaIfn5k/74xC/l44f+BwePz1595ADDYPvv7vDMAN3Bwf4CAIOE4//BYIOB/0On47E8AFCBwcPTwYOCAgPAgE8Bwf8gEDBwOAGIJZDBwX9DofhUYRKDKIIOEAAQOD8EABwgcB+IODnoKB84OD37tCBwUzZ4QODZ4QdDnIFB/YODZwP+v47DJIIBBJQcAAwZyBABoA=="))
|
||||
}
|
||||
}
|
||||
else if (img == "w8"){//flying portals
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YFE/H8BwtvBwvvvgOE/33Bwvf3gdF/YOF/4OF/IOGgA7F8ENBwn8gHcBw/5AoOAg4OCh4sD/vD+AFB45KBBwfwv//BwMJgEIFAXcnvggF4kEBBwPMSIIYBz/8nAEBw5ZD4IhBO48AhpoG953FSo/2Ugv/p4OF/LCGaIyIBB34OH4EAngODbAMDBwnfDoqeCBy7RBBwnh//xBwc9BQPnBwe/AYO/BwUzFYQODGgYOCnIFB/YOD57WBv47Dj//AIJKDgAGDOQIANA"))
|
||||
}
|
||||
}
|
||||
else if (img == "w9"){//cake
|
||||
return {
|
||||
width : 60, height : 60, bpp : 1,
|
||||
buffer : require("heatshrink").decompress(atob("AB0//4AE4YGF/gOY/oOG94OF/1/Bwv3FgwKCBwfnFhn8HY0LAQPwvgOB8EP/5uBBwP2gF4j+PBwP+sEEj/x44OB90Ao/8Dodwg8/nkH4ZXBgHnx8ABwPv/k98+ABwZEB+EAJQPj/3+nkAv4OB5+fz0Aj4OB98Ag+Ah/nBwJXB4EDHYSTB/EA/wsCSoJfBwAODNIPgBwgcBHYQOCC4QODn8Ah4ODGgMH+47D8EB/A7KTYMf4A7Eg/wHYgcBHZx3DcAPggbRBFgQcBcAQOB/iUBBwgcBBwgcCd4V/HYL+D/YOBDgIOC8/+DgIOC/+HfwIOD/4cCBwYAEBwQADBz0AABoA="))
|
||||
}
|
||||
}
|
||||
else if (img == "butPress"){
|
||||
return {
|
||||
width : 176, height : 176, bpp : 4,
|
||||
transparent : 1,
|
||||
buffer : require("heatshrink").decompress(atob("iIA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFEc5kM5hD/ACXMAAJXB5nBI35WSK4ZY/AB8cK4/MJP5WRK4pY/ABhPD5e7he7A4fBJn5XNKwJXCLAZX/ABUcKwhXDLAZN/VxhSCK4m8WH5XNVwZXEWARX/ABEcK4sAgBYDXYRP/K5RQC2ACE3e8K/5XPVgYDCK/4AKJIPLVYoEEBoPBIWPd6ICPK46uDAohXzjvd7oCMCAJX/K7cAAAZXFBQkBK/6v/ABPd6ICPK/4AaK4mwKwYEDV+4ARjhKBVQoDD3gMBK2MdAIRXXVYSuDK/5XN5ZRCgEAWQYLBK/4AJK4u7KwZXC4JXxiPd6JXV5hXH3hX1ACscWApXDMQRN/WBpYCK4QICV35XOLARXBA4ZX/ABccKAfMhgFEJf5YRK4hJ/ABxXH4JI/LCRXCK34ASjhXCIf4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACIA=="))
|
||||
}
|
||||
}
|
||||
else if (img == "butUnpress"){
|
||||
return {
|
||||
width : 176, height : 176, bpp : 4,
|
||||
transparent : 1,
|
||||
buffer : require("heatshrink").decompress(atob("iIA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AFEc5kM5hD/ACXMAAJXB5nBI35WSK4ZY/AB8cK4/MJP5WRK4pY/ABhQEK43BJn5X/AEMcK5fMJv6uPK46w/K/4AgjhPFgEALAxP/K5vAAQhX/K6KsDWApP/AA6uHWA/BIWOIwICPK46qFAohXxjGIxACMCAJX/K7cAAAZXFBQkBK/6v/ABOIwICPK/4AaKInAAhCv2ACMcVRC0FK2MYAIRXXVYSuFK/5XO5kAgCuFK/4AJJwvMKw3BK+MRxGBK/4ArjhXMJv6wQK4qu/K/4AjjhXKJf5YRK4hJ/ABxXH4JI/LCRXCK34ASjhXCIf4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACI="))
|
||||
}
|
||||
}
|
||||
|
||||
else if (img == "apetureLaboratories"){
|
||||
return {
|
||||
width : 173, height : 43, bpp : 4,
|
||||
transparent : 0,
|
||||
buffer : require("heatshrink").decompress(atob("AA+IAAeAIv5UTK34APhABBKwpeBJX5VLJgJWGAwKv/ABL8EKomABIYA/KpJWHAoRW/KpZQCfwJSCAgRW/KphWFBgZW/KphNCAgSxEKP4ADJAatFKwWAL4oA/KpALGXQhS/Ko4MIwAOMAHREOKv5ZVVgcAh///4LDAwQAB+AIHCQYHMDQQYBDwQEGDIoaGTx4MCwAiCJgYhFBIYIHLw4HFBARjGESJUDehZVFVhJNLRI5VGDIIdEAgwiL+DzDJAJVJB4IMBCoKsHAYpFCDgr2IApYEIBpJFCKpqsCHgQbFIhBVHBhJoGTwyBRKp5mBDoY6GKoYqEXYg7JApKeHQJysEKpRmBDoasHQBAACM4qMGEAr0JAgZjGFYhVPgGAEIpVEAAaAEA4oyEW4qyKFZBoFKoisDJIJWLfIp3IQBJeJfgysMERpVCwEIVpijFBA6ZJdA4JHJYgEKQIx1EVgRVBVhj5IEIyZHHYoUGNw4MCQIwIKAARVDxBVRQo6ZJHZZoGU4yBGBAiBGVgRZBVhYlIfJBoFHZZoHfJRwGBAQrEVISvCKpwrEUZAqFVhzYKB4yKGQIpVDAYJVKEoYFDBIpVIb4ysMIgoPHOgoRFhGAVwKsLAFzQHACBVCVhQA/KpawBCJa76IhRWDVxIODAwTaFAocP///dhALGBQYJBCIYQBDYgKEBBAEDVgeIAgKgFBYZVEEYY+CH4YDBBgYFBBYoFEOYhmEDYgFFLIwEDKw2AKwhTFBoSsHIgglFAQo/HKIoDECBBZGc46eEAYa2EKox2Ga5LjLDoq8FKAgVDBAv/EghWGVgRWJKoaOFD4IgCViAWFVjKTGJIZOFKpKOHAQj3JAogCFPApcGKQziGLopKCU4hWFKojsEHYrXFCAJFId45CEDYh4EB4Y1DCYSzFJQT+FgAGCKooA/AAZMBfopWDKv5WLVgxT/AB+AKopW/ACBU/ABwA=="))
|
||||
}
|
||||
}
|
||||
|
||||
else if (img == "apetureLaboratoriesLight"){
|
||||
return {
|
||||
width : 173, height : 43, bpp : 4,
|
||||
transparent : 1,
|
||||
buffer : require("heatshrink").decompress(atob("iIAGxAADwINHAH5ULK34APjABBKwpeBJX5VLJgJWGAwKv/ABL8EKomBBIYA/KpJWHAoRW/KpZQCfwJSCAgRW/KphWFBgZW/KphNCAgSxEKP4ADJAatFKwWBL4oA/KpALGXQhS/Ko4MIwIOMAHREOKv5ZVVgcRiEAgALDAwQABgIIHCQYHMDQQYBDwQEGDIoaGTx4MCwIiCJgYhFBIYIHLw4HFBARjGESJUDehZVFVhJNLRI5VGDIIdEAgwiLgLzDJAJVJB4IMBCoKsHAYpFCDgr2IApYEIBpJFCKpqsCHgQbFIhBVHBhJoGTwyBRKp5mBDoY6GKoYqEXYg7JApKeHQJysEKpRmBDoasHQBAACM4qMGEAr0JAgZjGFYhVPiOBEIpVEAAaAEA4oyEW4qyKFZBoFKoisDJIJWLfIp3IQBJeJfgysMERpVCwMYVpijFBA6ZJdA4JHJYgEKQIx1EVgRVBVhj5IEIyZHHYoUGNw4MCQIwIKAARVDxBVRQo6ZJHZZoGU4yBGBAiBGVgRZBVhYlIfJBoFHZZoHfJRwGBAQrEVISvCKpwrEUZAqFVhzYKB4yKGQIpVDAYJVKEoYFDBIpVIb4ysMIgoPHOgoRFjGBVwKsLAFzQHACBVCVhQA/KpawBCJa76IhRWDVxIODAwTaFAocQgEAdhALGBQYJBCIYQBDYgKEBBAEDVgeIAgKgFBYZVEEYY+CH4YDBBgYFBBYoFEOYhmEDYgFFLIwEDKw2BKwhTFBoSsHIgglFAQo/HKIoDECBBZGc46eEAYa2EKox2Ga5LjLDoq8FKAgVDBAsAEghWGVgRWJKoaOFD4IgCViAWFVjKTGJIZOFKpKOHAQj3JAogCFPApcGKQziGLopKCU4hWFKojsEHYrXFCAJFId45CEDYh4EB4Y1DCYSzFJQT+FiIGCKooA/AAZMBfopWDKv5WLVgxT/AB+BKopW/ACBU/ABsQA="))
|
||||
}
|
||||
}
|
||||
|
||||
else if (img == "potato"){
|
||||
return {
|
||||
width : 54, height : 55, bpp : 4,
|
||||
transparent : 6,
|
||||
buffer : require("heatshrink").decompress(atob("swAEsEGA4oASEQ4ARGgNgDa8QsA3BKStowxTDDalikxTCRKsSNwY2BDSYvDGoI2TsoTDgwEBGx5IBs1VHIgaBGx4qCDQZOBUiUGstQDQ4FBKYwHGDQNWDRA3BCYsABotgJ4dkogABowcGAAUGOwogDDAVCAYScKOooFBooWCAAjqNAoQYHKYQ8ELQZzFsAMCiIeJAAdFMwiCEoIaNoICBoAaMiMUDA0ikMiigaIAAgaGDIIaBkcyoCHEMxqIBmdEkMzmcxDSVBkYXBGoMzmUQDSMSDIURiIbBkDBCDRtBiYwBDIMREAMiDR6qBiI0BkQEBKQKkBUJQAEoCfBC4MVgMSDYMkGwQaBeBMUiC3BGYNN8kSHgM0DQrsGAAMQQAMyCwIaBgK/BmIaKM4IGCiMTDQXd9waCmlENZIaEgESKAMhpxQEDQSiEoJTGiEimaGCqIaBmKABUQwyBUIzRBkIXBDoI8BkAaDGwgaFEIMCeQUSYAKNBmLYDGwJ/DDYlFqAaBGwILBGgLyBUIRSFQghrCgEjDYMiiTdCggaFDYgaFKIKIBmcTkciiA1GNwpsFgI4BmZsBkVEDRAbJiBTCNQMABIIZHfBCPBDQKSCoFEGhA2IKIMQgJ1CoCFHGxVBeASQDgAZJDQ8RQIMyDQkGDZQ1GDILtBAwNGsAaLs1hokECYNCaQMxDIVmDRoOBDQifCBggaNKgMRqUhiovFGxoMEsCADAQQaMsUWJBi+LsMRqtWDSwUBqvFqpVFQ54UCstVqEGsDbBQx4lFgAABotRAgQABT54ADqtRM5jjQAAQ"))
|
||||
}
|
||||
}
|
||||
|
||||
else if (img == "apetureWatch"){
|
||||
return {
|
||||
width : 176, height : 176, bpp : 4,
|
||||
transparent : 2,
|
||||
buffer : require("heatshrink").decompress(atob("kQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A+/4A/AH4A/AH4A/AD0RgAASgMQCqgrqiJX/K/5X/K/5XR7vQK/8IxBX/K/5X/K/5X/K/5X/K/5X/K/5X/K/5Xah////wK/5XTKwIABK4YAG//x7vRBhAA2jGIAYMfK4fxCRAOCK/5XFKwYABBgTBEh4LC7vQbabxLFYoVPFZMIxADBK4sAK4wLDK/5XSBYhX/K6MPBIQDBK/5XD+BXMBAQQCK/5XDKAJXKVwRWBAoJX/K4j3CUohXDL4YACK/5XF+CuEK4yuCK/5XHWAZOCK4cPKwhX/K45MFK4YAGK/5XGLApX/K6X/K5sPK/5XIWAZXJ/5X/K6sPK/5XPAoauDK/5XJ/5XDj5eEVwRX/K5y2FVwRX/K4/wK44GDVwRX/K50fWAi9DK/5XGUYRQCiKwCAwKuDK/5XGJgZXGMQJWDK/5XGAoKkCK4arEK/5XIVQRQDK/5XQAwZLC+BXBAwYACLwJX/K4auCWApXHCAJX/K5JYFAoi/Ch5X/K4ZHCAAZXEAoZnCK/5XLVQRXFBgZX/K4igCLAnxFYRdBBohX/K5cAiIrJK/5XEfIhX/K/5Xr+BX/K/5Xu/5X/K/5WKFhRXWl5XB+BX/K6ywFK9XxK4SMFK7JWCK58PK7sP/5XDGoxXr/5Xc//4xBXEHIKyPK54fFK5CPBK7cPxAyBK4oHBK7wKFK5AQBK7UP/GPD4JWCiMfK4IJBK7jOGFgYwEK4XRBg4AQ/CtFAAZXCBQ4AQjBXCCRxXcUoJLDjnMhnMBgTqCK7RzJYYxXC6DbTAAf4UYInB5gABK4PM4KBDdYwrQhBXBBQ5XIAAJXYh6GDKwRXDLASwCK7BxIK8auDjhXH5iwPK5gKIK8iuBKwhXFLAJX/AA0PxCuBKAhXG4KwCK/6uEx/xK5qwNK/KuBjhXL5kRWAJXrbapXEJ4pXH4JXXABRXhh+I+JXPiP//5X/K4WP+McJ4oLBLAxX/K5vAAQhX/ABBDBiJXFVgawFiMf//wK/4gBAAKuHWA/BCYQhIK8uIwACOK5CqFAohXxhGIxACMCAJX/K7YaEK4pKFK/6v/ABOIwACOK/4AMFZRXI4AEIV+wrO///iMcVRC0FiMf//wQaZXZhABCFZ0P//xK4qrCVwpXB/+PbahX15gLBVwpX/K5ERJwvMKw3BiP4K98AxGAFaH//5XPj/4+BXvFaRXCjhXMiMfxBXhGoIAcK4nxWAxXF4MR/GPK4Q4e+UiADcvK4UPWARXMj/4NwayKACUPK8KwDjhXKcQOIKYZX/eIkRLAhXEiKuBx5XEY4QAYDgJXiIAXxiJXH4KuC/4VDK/6wGLAZXCKwKuGK/6wIiMcK4QFBVw5X/WA2IwJSCAAYJBVwpX/WA2IJwJVDj///GPVwpX/LA34K4PxVoYHBKwxX/AAynCK4ZWC+BX/K5ixB/6vEVo5X/IxAABK4asHK/5XLgJXCBxRX/K/5XgLAQNLK/5X/K6r7CACxX/K/5XVfJcBiINLK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K5o6bK/YaZK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K6o6bK/cAABUBiINLK/5X/K/5X/K/5X/K/AMLACBX/K7DMaAAcRADA4eA=="))
|
||||
}
|
||||
}
|
||||
|
||||
else if (img == "apetureWatchLight"){
|
||||
return {
|
||||
width : 176, height : 176, bpp : 4,
|
||||
transparent : 2,
|
||||
buffer : require("heatshrink").decompress(atob("kQA/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4A+gAA/AH4A/AH4A/AD0R/4AS+MfCqgrqiJX/K/5X/K/5XR7vfK//4xBX/K/5X/K/5X/K/5X/K/5X/K/5X/K/5Xa+EAgEPK/5XTKwIABK4YAGgEB7vRBhAA2jGIAYMQK4cBCRAOCK/5XFKwYABBgTBE+ALC7vfbabxLFYoVPFZP4xADBK4v/K4wLDK/5XSBYhX/K6PwBIQDBK/5XDh5XMBAQQCK/5XDKAJXKVwRWBAoJX/K4j3CUohXDL4YACK/5XFh6uEK4yuCK/5XHWAZOCK4fwKwhX/K45MFK4YAGK/5XGLApX/K6UAK5vwK/5XIWAZXJgBX/K6vwK/5XPAoauDK/5XJgBXDiBeEVwRX/K5y2FVwRX/K48PK44GDVwRX/K50QWAi9DK/5XGUYRQCiKwCAwKuDK/5XGJgZXGMQJWDK/5XGAoKkCK4arEK/5XIVQRQDK/5XQAwZLCh5XBAwYACLwJX/K4auCWApXHCAJX/K5JYFAoi/C+BX/K4ZHCAAZXEAoZnCK/5XLVQRXFBgZX/K4igCLAkBFYRdBBohX/K5f/iIrJK/5XEfIhX/K/5Xrh5X/K/5XugBX/K/5WKFhRXWkBXBh5XvgJXCGgpXcWApXoF4KvD+COGK65WCK5/wK7gtCK4YmCLB5XfgBXbFgIzBK4mILCBXPDwpXIcIJXa+BPBKAJXFLARXdBQpXICAJXah/4x4qDAAMfK4IJBWBpXODgwsDAAcQK4XRBg4APgAwBVogADK4XwgInWjBXCCRxXbiD9BKwcc5kM5gNCRgXwK7JyJYYxXC77bTIwf4UYInB5gABK4PM4MRDQXwDpYrKawMABQ5XIAAJXYh6uDKwRXDLAQRDK64YIK8SuEjhXH5iwPK5gKIK8UP/APBKwhXFLAKwNK/ItBiJQEK43BDgRX/AAXw/GP+JXNGQXwK/5XDEgMcK5fMiIdBK9YAKK5cPK4RPFK4/BDoUPFagAJK8WI+JXPGYRX/IIWP+McJ4sAgBYGK/5XN4ACEK/4AH+AjCK4qsDWAsRDwIWCK/ogBAAKuHWA/BCYQhIK8uIx4COK5CqFAohXx/GIxACMCAJX/K7cAAAZXFBQkBK/6v/ABOIx4COK/4AMFZRXI4AEIV+wrN+AjCjiqIWgpUCCwRXr/ABCFZ0PBoJXFVYSuFK4P/x4VBK/5XI5kAgCuFK/5XIiJOF5hWG4MR/BXv/+Ix5XREgJXOj58BK94rR+AkCjhXMiMfxAUCK70AADpXE+KwGK4vBiP4x5XhgUiADcgEQTyCK5sf/ATDK/5DD+McK5UR/+IK/5XEeYcRLAhXEiKuBx4SDK/6wFiJXH4KuOK/SwELAZXCKwKuOK/ywBiMcK4QFBVwZX/K43/gACBxGBKQQADBIKuBh5X/K43/JAOIJwJVDIYP4x4NCK/5XHfAP4K4PxVoYHBBgRX/K5H/gCnCK4ZWDVxpX9LARABV4ZWQK/xYBgBXD+EAKx5X/AAMBK4RVQK/5ADK4RBSK/5YDIKZX/K/5XNfYQA1K/5X2eJkReKfxj4VTK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5XvgAAYh5X8DTJX/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5XVgAAYK/orL+MRIKZX/K/5X/K/5X/K/5X/K/5XmgAAdiIA3"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawStart(){
|
||||
g.clear();
|
||||
g.reset();
|
||||
if (g.theme.dark){apSciLab = getImg("apetureLaboratories");}
|
||||
else {apSciLab = getImg("apetureLaboratoriesLight");}
|
||||
g.drawImage(apSciLab, xyCenter-apSciLab.width/2, xyCenter-apSciLab.height/2);
|
||||
}
|
||||
|
||||
// Check settings for what type our clock should be
|
||||
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
|
||||
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
|
||||
//warnings
|
||||
var maxWarning = 9;
|
||||
var curWarning = Math.floor(Math.random() * (maxWarning+1));
|
||||
|
||||
function unPause(delay, quote){
|
||||
if (pause){
|
||||
setTimeout(function() {
|
||||
if (quote == undefined || quoteNum == quote){
|
||||
pause = false;
|
||||
draw();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
var quoteNum;
|
||||
|
||||
function quote(fontsize, width, height, specificQuote){
|
||||
pause = true;
|
||||
var finalString = "";
|
||||
var quotesFile;
|
||||
var finalFontSize;
|
||||
quotesFile = require("Storage").read("aptsciclkquotes.txt", 0, 0); //opens the quotes file
|
||||
//console.log(quotesFile);
|
||||
var quotes = quotesFile.split("^");
|
||||
var numQuotes = quotes.length;//number of quotes
|
||||
var curQuote;
|
||||
|
||||
if (specificQuote == undefined){
|
||||
quoteNum = Math.round(Math.random()*numQuotes)-1;
|
||||
curQuote = quotes[quoteNum]; //quote to be displayed
|
||||
}
|
||||
else{
|
||||
quoteNum = specificQuote;
|
||||
curQuote = quotes[quoteNum];
|
||||
}
|
||||
|
||||
unPause(10000, quoteNum);
|
||||
|
||||
var curWords = curQuote.split(" "); //individual words
|
||||
//console.log(numQuotes);
|
||||
|
||||
var maxChar = width/6/fontsize;
|
||||
var maxLines = height/10/fontsize;
|
||||
var curLines = 0;
|
||||
var curLength = 0;
|
||||
|
||||
|
||||
for (var i = 0; i < curWords.length; i++){
|
||||
//console.log(curLength+curWords[i].length);
|
||||
if (curLength + curWords[i].length <= maxChar){
|
||||
finalString += " "+curWords[i];
|
||||
curLength += curWords[i].length+1;
|
||||
//console.log("next");
|
||||
}
|
||||
else{
|
||||
//console.log("break");
|
||||
curLines++;
|
||||
if (curLines > maxLines){
|
||||
curLength = 0;
|
||||
finalString = "";
|
||||
i = -1;
|
||||
if (fontsize > 1){fontsize--;}
|
||||
maxChar = width/6/fontsize;
|
||||
maxLines = height/10/fontsize;
|
||||
console.log(maxLines);
|
||||
console.log(maxChar);
|
||||
|
||||
}
|
||||
else{
|
||||
curLength = 0;
|
||||
finalString += "\n";
|
||||
i--;
|
||||
}
|
||||
}
|
||||
finalFontSize = fontsize;
|
||||
}
|
||||
|
||||
|
||||
//drawing actual stuff
|
||||
g.setColor(g.getBgColor());
|
||||
g.fillRect(10, 10+28, g.getWidth()-10,g.getWidth()-10);
|
||||
g.reset();
|
||||
g.setFont(font, finalFontSize);
|
||||
g.setFontAlign(0, 0);
|
||||
g.drawString(finalString, xyCenter, xyCenter+14);
|
||||
//quote length*pixels per character = pixel width
|
||||
//height ~120 width ~160
|
||||
}
|
||||
|
||||
function buttonPressed(){
|
||||
if (curWarning < maxWarning) curWarning += 1;
|
||||
else curWarning = 0;
|
||||
g.reset();
|
||||
buttonImg = getImg("butPress");
|
||||
g.drawImage(buttonImg, 0, 0);
|
||||
|
||||
warningImg = getImg("w"+String(curWarning));
|
||||
g.drawImage(warningImg, 1, g.getWidth()-61);
|
||||
|
||||
setTimeout(buttonUnpressed, 500);
|
||||
}
|
||||
function buttonUnpressed(){
|
||||
if (!pause){
|
||||
buttonImg = getImg("butUnpress");
|
||||
g.drawImage(buttonImg, 0, 0);
|
||||
}
|
||||
else{
|
||||
setTimeout(buttonUnpressed, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
|
||||
function draw() {
|
||||
if (pause){}
|
||||
else{
|
||||
// get date
|
||||
var d = new Date();
|
||||
var da = d.toString().split(" ");
|
||||
|
||||
g.reset(); // default draw styles
|
||||
//draw watchface
|
||||
if (g.theme.dark){apSciWatch = getImg("apetureWatch");}
|
||||
else {apSciWatch = getImg("apetureWatchLight");}
|
||||
g.drawImage(apSciWatch, xyCenter-apSciWatch.width/2, xyCenter-apSciWatch.height/2);
|
||||
|
||||
potato = getImg("potato");
|
||||
g.drawImage(potato, 118, 118);
|
||||
|
||||
g.drawImage(warningImg, 1, g.getWidth()-61);//update warning
|
||||
|
||||
// drawString centered
|
||||
g.setFontAlign(0, 0);
|
||||
|
||||
// draw time
|
||||
var time = da[4].substr(0, 5).split(":");
|
||||
var hours = time[0],
|
||||
minutes = time[1];
|
||||
var meridian = "";
|
||||
if (is12Hour) {
|
||||
hours = parseInt(hours,10);
|
||||
meridian = "AM";
|
||||
if (hours == 0) {
|
||||
hours = 12;
|
||||
meridian = "AM";
|
||||
} else if (hours >= 12) {
|
||||
meridian = "PM";
|
||||
if (hours>12) hours -= 12;
|
||||
}
|
||||
hours = (" "+hours).substr(-2);
|
||||
}
|
||||
|
||||
g.setFont(font, timeFontSize);
|
||||
g.drawString(`${hours}:${minutes}`, xyCenter+2, yposTime, false);
|
||||
g.setFont(font, gmtFontSize);
|
||||
g.drawString(meridian, xyCenter + 102, yposTime + 10, true);
|
||||
|
||||
// draw Day, name of month, Date
|
||||
var date = [da[0], da[1], da[2]].join(" ");
|
||||
g.setFont(font, dateFontSize);
|
||||
g.drawString(String(date), xyCenter, yposDate, false);
|
||||
|
||||
|
||||
// draw year
|
||||
g.setFont(font, dateFontSize);
|
||||
g.drawString(d.getFullYear(), xyCenter+1, yposYear, true);
|
||||
}
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
|
||||
// Stop updates when LCD is off, restart when on
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (on) {
|
||||
draw(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on('touch',(n,e)=>{
|
||||
//button is 88 104
|
||||
if (!pause && buttonX-buttonTolerance < e.x && e.x < buttonX+buttonTolerance && buttonY-buttonTolerance < e.y && e.y < buttonY+buttonTolerance){
|
||||
buttonPressed();
|
||||
}
|
||||
//Potato GLaDOS
|
||||
else if (!pause && 117 < e.x && e.x < 172 && 117 < e.y && e.y < 172){
|
||||
quote(2, 150, 140);
|
||||
}
|
||||
else{
|
||||
unPause(0);
|
||||
}
|
||||
});
|
||||
|
||||
//show Apeture laboritories
|
||||
drawStart();
|
||||
|
||||
setTimeout(function() {
|
||||
// clean app screen
|
||||
g.clear();
|
||||
// Show launcher when button pressed
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
//update warning image
|
||||
buttonPressed();
|
||||
// draw now
|
||||
draw();
|
||||
}, 500);
|
After Width: | Height: | Size: 714 B |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "aptsciclk",
|
||||
"name": "Apeture Science Clock",
|
||||
"shortName":"AptSci Clock",
|
||||
"version": "0.08",
|
||||
"description": "A clock based on the portal series",
|
||||
"icon": "app.png",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": false,
|
||||
"readme":"README.md",
|
||||
"storage": [
|
||||
{"name":"aptsciclkquotes.txt","url":"quotes.txt"},
|
||||
{"name":"aptsciclk.app.js","url":"app.js"},
|
||||
{"name":"aptsciclk.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Well here we are again^You euthanized your faithful Companion Cube more quickly than any test subject on record. Congratulations.^So get comfortable while I warm up the neurotoxin emitters^This isn't brave. It's murder. What did I ever do to you?^The difference between us is that I can feel pain.^Who's gonna make the cake when I'm gone? You?^Oh... It's you.^I've been really busy being dead. You know, after you MURDERED ME.^So. How are you holding up? BECAUSE I'M A POTATO.^You really do have brain damage, don't you?^You like revenge, right? Everybody likes revenge. Well, let's go get some.^It's been fun. Don't come back.^And then you showed up. You dangerous, mute lunatic.^Unbelievable. You, [subject name here] must be the pride of [subject hometown here.]^You are not a good person. You know that, right? Good people don't get up here.^Cake, and grief counseling, will be available at the conclusion of the test.^This is your fault. I'm going to kill you. And all the cake is gone. You don't even care, do you?^Momentum, a function of mass and velocity, is conserved between portals. In layman's terms, speedy thing goes in, speedy thing comes out.
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))
|
||||
require("heatshrink").decompress(atob("kkkwIEBgf8AYMB//4AgN///ggEf4E/wED+EACQN8C4Pgh4TBh8BCYMAvEcEoWD4AEBnk4gFggPHwAXBj1wgIwB88An/Ah3gg/+gF+gH/+EH8Ef/+ABAPvuAIBgnyCIQjBBAMAJAIIEuAICFgIIBh14BAMB8eAg0Ajk8KAXBKAU4jwDBg+ADoIXBg4NBnxPBEgPAgP8gZaBg//KoKLBKAIEBMQMAA"))
|
||||
|
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY="))
|
||||
require("heatshrink").decompress(atob("kUw4MA///xP5gEH/AMBh//4AHBwF4gEDwEHgEB4fw8EAsf/jEAjPh80AhngjnAgcwAIMB5kA50A+cAmfAtnAhnYmc//8zhln/+c4YjBg0w440Bxk38EB/cP/0B//Dwf/+FxwEf8EGIAJGB2BkCnhiB4EPgF//EDFQIpB+HGgOMnkxwFjh8MsEY4YQHn/x//j//8n/wHYItBCAKFBhgKBKAIQBBgIQC4AQCmAQChkD/v8gcA/wCBBoMA7+39kAPwP/WIMP4aYBCAYhCCAkHAYOAA="))
|
||||
|
|
|
@ -3,3 +3,5 @@
|
|||
0.03: Add "Calculating" placeholder, update JSON save format
|
||||
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
|
||||
|
|
|
@ -33,7 +33,7 @@ Keep those copies safe and secure.
|
|||
* Swipe right to exit to the app launcher.
|
||||
* Swipe left on selected counter token to advance the counter to the next value.
|
||||
|
||||

|
||||
   
|
||||
|
||||
## Creator
|
||||
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
const tokenextraheight = 16;
|
||||
var tokendigitsheight = 30;
|
||||
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 = {
|
||||
|
@ -7,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;
|
||||
|
@ -45,192 +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;
|
||||
}
|
||||
} 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);
|
||||
g.setBgColor(g.theme.bgH);
|
||||
g.setFont("Vector", tokenextraheight);
|
||||
// center just below top line
|
||||
g.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
g.setColor(g.theme.fg);
|
||||
g.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);
|
||||
g.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);
|
||||
g.drawLine(x1, y1, x2, y1);
|
||||
g.drawLine(x1, y2, x2, y2);
|
||||
g.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 / (tokendigitsheight + tokenextraheight));
|
||||
var y = id * (tokendigitsheight + tokenextraheight) + 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:(tokendigitsheight + tokenextraheight)});
|
||||
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
||||
drewcur = true;
|
||||
}
|
||||
id += 1;
|
||||
y += (tokendigitsheight + tokenextraheight);
|
||||
}
|
||||
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);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
}
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
|
@ -238,93 +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)) / (tokendigitsheight + tokenextraheight));
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
if (id != -1) {
|
||||
var y = id * (tokendigitsheight + tokenextraheight) - 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 += (tokendigitsheight + tokenextraheight);
|
||||
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 * (tokendigitsheight + tokenextraheight) - 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);
|
||||
var fakee = {};
|
||||
fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - 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:"rising", debounce:50, repeat:true});
|
||||
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();
|
||||
|
|
|
@ -12,8 +12,8 @@ body.select div.select,body.export div.export{display:block}
|
|||
body.select div.export,body.export div.select{display:none}
|
||||
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#showqr,body.export div#tokens{display:block}
|
||||
#tokens th,#tokens td{padding:5px}
|
||||
#tokens tr:nth-child(odd){background-color:#ccc}
|
||||
#tokens tr:nth-child(even){background-color:#eee}
|
||||
#tokens tr:nth-child(odd){background-color:#f1f1fc}
|
||||
#tokens tr:nth-child(even){background-color:#fff}
|
||||
#qr-canvas{margin:auto;width:calc(100%-20px);max-width:400px}
|
||||
#advbtn,#scan,#tokenqr table{text-align:center}
|
||||
#edittoken tbody#adv{display:none}
|
||||
|
@ -54,8 +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('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, '');
|
||||
|
@ -80,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;
|
||||
|
@ -225,15 +226,18 @@ function editToken(id) {
|
|||
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
|
||||
markup += '</td></tr>';
|
||||
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
||||
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||
markup += '<button class="btn" type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||
markup += '</td></tr></table></form>';
|
||||
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||
markup += '<button class="btn" type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||
markup += ' ';
|
||||
if (tokens[id].isnew) {
|
||||
markup += '<button type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||
markup += '<button class="btn" type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||
} else {
|
||||
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>';
|
||||
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||
markup += '<button class="btn" type="button" onclick="showTokenQr()">Show QR</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||
}
|
||||
document.getElementById('edit').innerHTML = markup;
|
||||
document.body.className = 'editing';
|
||||
|
@ -303,9 +307,23 @@ function updateTokens() {
|
|||
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
|
||||
};
|
||||
const tokenButton = function(fn, id, label, dir) {
|
||||
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||
return '<button class="btn" type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||
};
|
||||
var markup = '<table><tr><th>';
|
||||
var markup = '';
|
||||
markup += '<div class="select">';
|
||||
markup += '<button class="btn" type="button" onclick="addToken()">Add Token</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="saveTokens()">Save to watch</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||
markup += '</div><div class="export">';
|
||||
markup += '<button class="btn" type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||
markup += '</div>';
|
||||
markup += '<table><tr><th>';
|
||||
markup += tokenSelect('all');
|
||||
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
|
||||
/* any tokens marked new are cancelled new additions and must be removed */
|
||||
|
@ -330,15 +348,6 @@ function updateTokens() {
|
|||
markup += '</td></tr>';
|
||||
}
|
||||
markup += '</table>';
|
||||
markup += '<div class="select">';
|
||||
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
||||
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
||||
markup += '<button type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||
markup += '<button type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||
markup += '</div><div class="export">';
|
||||
markup += '<button type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||
markup += '<button type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||
markup += '</div>';
|
||||
document.getElementById('tokens').innerHTML = markup;
|
||||
document.body.className = 'select';
|
||||
}
|
||||
|
@ -404,7 +413,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() {
|
||||
|
@ -486,7 +495,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;
|
||||
|
@ -603,7 +612,7 @@ function qrBack() {
|
|||
<div id="scan">
|
||||
<table>
|
||||
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
||||
<tr><td><button type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||
<tr><td><button class="btn" type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
@ -612,7 +621,7 @@ function qrBack() {
|
|||
|
||||
<div id="showqr">
|
||||
<table><tr><td id="qrcode"></td></tr><tr><td>
|
||||
<button type="button" onclick="qrBack()">Back</button>
|
||||
<button class="btn" type="button" onclick="qrBack()">Back</button>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
"name": "2FA Authenticator",
|
||||
"shortName": "AuthWatch",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version": "0.05",
|
||||
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
|
||||
"version": "0.07",
|
||||
"description": "Google Authenticator compatible tool.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
|
@ -1 +0,0 @@
|
|||
node_modules/
|
|
@ -1,13 +0,0 @@
|
|||
0.01: First release
|
||||
0.02: Bugfix time: Reset minutes to 0 when hitting 60
|
||||
0.03: Fix distance >=10 km (fix #529)
|
||||
0.04: Use offscreen buffer for flickerless updates
|
||||
0.05: Complete rewrite. New UI, GPS & HRM Kalman filters, activity logging
|
||||
0.06: Reading HDOP directly from the GPS event (needs Espruino 2v07 or above)
|
||||
0.07: Fixed GPS update, added guards against NaN values
|
||||
0.08: Fix issue with GPS coordinates being wrong after the first one
|
||||
0.09: Another GPS fix (log raw coordinates - not filtered ones)
|
||||
0.10: Removed kalman filtering to allow distance log to work
|
||||
Only log data every 5 seconds (not 1 sec)
|
||||
Don't create a file until the first log entry is ready
|
||||
Add labels for buttons
|
|
@ -1,25 +0,0 @@
|
|||
# BangleRun
|
||||
|
||||
An app for running sessions. Displays info and logs your run for later viewing.
|
||||
|
||||
## Compilation
|
||||
|
||||
The app is written in Typescript, and needs to be transpiled in order to be
|
||||
run on the BangleJS. The easiest way to perform this step is by using the
|
||||
ubiquitous [NPM package manager](https://www.npmjs.com/get-npm).
|
||||
|
||||
After having installed NPM for your platform, checkout the `BangleApps` repo,
|
||||
open a terminal, and navigate into the `apps/banglerun` folder. Then issue:
|
||||
|
||||
```
|
||||
npm i
|
||||
```
|
||||
|
||||
to install the project's build tools, and:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To build the app. The last command will generate the `app.js` file, containing
|
||||
the transpiled code for the BangleJS.
|
|
@ -1 +0,0 @@
|
|||
require("heatshrink").decompress(atob("mEwwIHEuAEDgP8ApMDAqAXBjAGD/E8AgUcgF8CAX/BgIFBn//wAFCv//8PwAoP///5Aon/8AcB+IFB4AFB8P/34FBgfj/8fwAFB4f+g4cBg/H/w/Cg+HKQcPx4FEh4/CAoMfAocOj4/CKYRwELIIFDLII6BAoZSBLIYeCgP+v4FD/k/GAQFBHgcD/ABBIIX4gIFBSYPwAoUPAog/B8AFEwAFDDQQCBQoQFCZYYFigCKEgFwgAA=="))
|
|
@ -1 +0,0 @@
|
|||
!function(){"use strict";var t;!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(t||(t={}));const n={STOP:63488,PAUSE:65504,RUN:2016};function e(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function i(i){var s;g.setFontVector(30),g.setFontAlign(0,-1,0),e((i.distance/1e3).toFixed(2),60,55),e(function(t){const n=Math.round(t),e=Math.floor(n/3600),i=Math.floor(n/60)%60,s=n%60;return(e?e+":":"")+("0"+i).substr(-2)+":"+("0"+s).substr(-2)}(i.duration),172,55),e(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),i=n%60;return("0"+e).substr(-2)+"'"+("0"+i).substr(-2)+'"'}(i.speed),60,115),e(i.hr.toFixed(0),172,115),e(i.steps.toFixed(0),60,175),e(i.cadence.toFixed(0),172,175),g.setFont("6x8",2),g.setColor(i.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(s=new Date).getHours()).substr(-2)+":"+("0"+s.getMinutes()).substr(-2),120,220),g.setColor(n[i.status]),g.fillRect(160,216,230,240),g.setColor(0),g.drawString(i.status,200,220),g.setFont("6x8").setFontAlign(0,0,1).setColor(-1),i.status===t.Paused?g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1):i.status===t.Running?g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1):g.drawString("START",236,60,1).drawString(" ",236,180,1)}function s(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),i(t),Bangle.drawWidgets()}function a(n){n.status===t.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),e=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(e,"w"),t.fileWritten=!1}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Running,i(n)}const r={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,t:NaN,timeSinceLog:0,hr:60,hrError:100,file:null,fileWritten:!1,drawing:!1,status:t.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var o;o=r,Bangle.on("GPS",n=>function(n,e){n.lat=e.lat,n.lon=e.lon,n.alt=e.alt,n.vel=e.speed/3.6,n.fix=e.fix,n.dop=e.hdop,n.gpsValid=n.fix>0,function(n){const e=Date.now();let i=(e-n.t)/1e3;if(isFinite(i)||(i=0),n.t=e,n.timeSinceLog+=i,n.status===t.Running&&(n.duration+=i),!n.gpsValid)return;const s=6371008.8+n.alt,a=n.lat*Math.PI/180,r=n.lon*Math.PI/180,o=s*Math.cos(a)*Math.cos(r),g=s*Math.cos(a)*Math.sin(r),d=s*Math.sin(a);if(!n.x)return n.x=o,n.y=g,void(n.z=d);const u=o-n.x,l=g-n.y,c=d-n.z,f=Math.sqrt(u*u+l*l+c*c);n.x=o,n.y=g,n.z=d,n.status===t.Running&&(n.distance+=f,n.speed=n.distance/n.duration||0,n.cadence=60*n.steps/n.duration||0)}(n),i(n),n.gpsValid&&n.status===t.Running&&n.timeSinceLog>5&&(n.timeSinceLog=0,function(t){t.fileWritten||(t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n"),t.fileWritten=!0),t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(n))}(o,n)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,i=Math.abs(e)+101-n.confidence,s=t.hrError/(t.hrError+i)||0;t.hr+=e*s,t.hrError+=(i-t.hrError)*s}(t,n)),Bangle.setHRMPower(1)}(r),function(n){Bangle.on("step",()=>function(n){n.status===t.Running&&(n.steps+=1)}(n))}(r),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&s(t)}),s(t)}(r),setWatch(()=>a(r),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(n){n.status===t.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Stopped,i(n)}(r),BTN3,{repeat:!0,edge:"falling"})}();
|
Before Width: | Height: | Size: 10 KiB |
|
@ -1,217 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="tracks"></div>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
/* TODO: Calculate cadence from step count */
|
||||
var domTracks = document.getElementById("tracks");
|
||||
|
||||
function saveKML(track,title) {
|
||||
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
|
||||
<Document>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="heartrate" type="int">
|
||||
<displayName>Heart Rate</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="steps" type="int">
|
||||
<displayName>Step Count</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="distance" type="float">
|
||||
<displayName>Distance</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="int">
|
||||
<displayName>Cadence</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name>Tracks</name>
|
||||
<Placemark>
|
||||
<name>${title}</name>
|
||||
<gx:Track>
|
||||
${track.map(pt=>` <when>${pt.date.toISOString()}</when>\n`).join("")}
|
||||
${track.map(pt=>` <gx:coord>${pt.lon} ${pt.lat} ${pt.alt}</gx:coord>\n`).join("")}
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="heartrate">
|
||||
${track.map(pt=>` <gx:value>${pt.heartrate}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="steps">
|
||||
${track.map(pt=>` <gx:value>${pt.steps}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="distance">
|
||||
${track.map(pt=>` <gx:value>${pt.distance}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
</Document>
|
||||
</kml>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([kml], {type: "application/vnd.google-earth.kml+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".kml";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function saveGPX(track, title) {
|
||||
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
|
||||
<metadata>
|
||||
<time>${track[0].date.toISOString()}</time>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>${title}</name>
|
||||
<trkseg>`;
|
||||
track.forEach(pt=>{
|
||||
gpx += `
|
||||
<trkpt lat="${pt.lat}" lon="${pt.lon}">
|
||||
<ele>${pt.alt}</ele>
|
||||
<time>${pt.date.toISOString()}</time>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:hr>${pt.heartrate}</gpxtpx:hr>
|
||||
<gpxtpx:distance>${pt.distance}</gpxtpx:distance>
|
||||
${/* <gpxtpx:cad>65</gpxtpx:cad> */""}
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>`;
|
||||
});
|
||||
gpx += `
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([gpx], {type: "application/gpx+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".gpx";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function trackLineToObject(l, hasFileName) {
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var t = l.trim().split(",");
|
||||
var n = hasFileName ? 1 : 0;
|
||||
var o = {
|
||||
invalid : t.length < 8,
|
||||
date : new Date(parseInt(t[n+0])),
|
||||
lat : parseFloat(t[n+1]),
|
||||
lon : parseFloat(t[n+2]),
|
||||
alt : parseFloat(t[n+3]),
|
||||
duration : parseFloat(t[n+4]),
|
||||
distance : parseFloat(t[n+5]),
|
||||
heartrate : parseInt(t[n+6]),
|
||||
steps : parseInt(t[n+7]),
|
||||
};
|
||||
if (hasFileName)
|
||||
o.filename = t[0];
|
||||
return o;
|
||||
}
|
||||
|
||||
function downloadTrack(trackid, callback) {
|
||||
Util.showModal("Downloading Track...");
|
||||
Util.readStorageFile(trackid, data=>{
|
||||
Util.hideModal();
|
||||
var trackLines = data.trim().split("\n");
|
||||
trackLines.shift(); // remove first line, which is column header
|
||||
// should be:
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var track = trackLines.map(l=>trackLineToObject(l,false));
|
||||
callback(track);
|
||||
});
|
||||
}
|
||||
function getTrackList() {
|
||||
Util.showModal("Loading Tracks...");
|
||||
domTracks.innerHTML = "";
|
||||
Puck.eval(`require("Storage").list(/banglerun_.*\\x01/).map(fn=>{fn=fn.slice(0,-1);var f=require("Storage").open(fn,"r");f.readLine();return fn+","+f.readLine()})`,trackLines=>{
|
||||
var html = `<div class="container">
|
||||
<div class="columns">\n`;
|
||||
trackLines.forEach(l => {
|
||||
var track = trackLineToObject(l, true /*has filename*/);
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">Track ${track.filename}</div>
|
||||
<div class="card-subtitle text-gray">${track.invalid ? "No Data":track.date.toString().substr(0,24)}</div>
|
||||
</div>
|
||||
${track.invalid?``:`<div class="card-image">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="250"
|
||||
frameborder="0" style="border:0"
|
||||
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4&q=${track.lat},${track.lon}&zoom=10" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="card-body"></div>`}
|
||||
<div class="card-footer">${track.invalid?``:`
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadkml">Download KML</button>
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadgpx">Download GPX</button>`}
|
||||
<button class="btn btn-default" trackid="${track.filename}" task="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (trackLines.length==0) {
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">No tracks</div>
|
||||
<div class="card-subtitle text-gray">No GPS tracks found</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
domTracks.innerHTML = html;
|
||||
Util.hideModal();
|
||||
var buttons = domTracks.querySelectorAll("button");
|
||||
for (var i=0;i<buttons.length;i++) {
|
||||
buttons[i].addEventListener("click",event => {
|
||||
var button = event.currentTarget;
|
||||
var trackid = button.getAttribute("trackid");
|
||||
var task = button.getAttribute("task");
|
||||
if (task=="delete") {
|
||||
Util.showModal("Deleting Track...");
|
||||
Util.eraseStorageFile(trackid,()=>{
|
||||
Util.hideModal();
|
||||
getTrackList();
|
||||
});
|
||||
}
|
||||
if (task=="downloadkml") {
|
||||
downloadTrack(trackid, track => saveKML(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
if (task=="downloadgpx") {
|
||||
downloadTrack(trackid, track => saveGPX(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
getTrackList();
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"spec_dir": "test",
|
||||
"spec_files": [
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"id": "banglerun",
|
||||
"name": "BangleRun",
|
||||
"shortName": "BangleRun",
|
||||
"version": "0.10",
|
||||
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
||||
"icon": "banglerun.png",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
"supports": ["BANGLEJS"],
|
||||
"interface": "interface.html",
|
||||
"allow_emulator": false,
|
||||
"storage": [
|
||||
{"name":"banglerun.app.js","url":"app.js"},
|
||||
{"name":"banglerun.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"name": "banglerun",
|
||||
"version": "0.5.0",
|
||||
"description": "Bangle.js app for running sessions",
|
||||
"main": "app.js",
|
||||
"types": "app.d.ts",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"test": "ts-node -P tsconfig.spec.json node_modules/jasmine/bin/jasmine --config=jasmine.json"
|
||||
},
|
||||
"author": {
|
||||
"name": "Stefano Baldan",
|
||||
"email": "singintime@gmail.com"
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "^4.1.1",
|
||||
"@types/jasmine": "^3.5.10",
|
||||
"jasmine": "^3.5.0",
|
||||
"rollup": "^2.10.2",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"terser": "^4.7.0",
|
||||
"ts-node": "^8.10.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.2"
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import typescript from '@rollup/plugin-typescript';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
export default {
|
||||
input: './src/app.ts',
|
||||
output: {
|
||||
dir: '.',
|
||||
format: 'iife',
|
||||
name: 'banglerun'
|
||||
},
|
||||
plugins: [
|
||||
typescript(),
|
||||
terser(),
|
||||
]
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
import { draw } from './display';
|
||||
import { initLog } from './log';
|
||||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
function startActivity(state: AppState): void {
|
||||
if (state.status === ActivityStatus.Stopped) {
|
||||
initLog(state);
|
||||
}
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.status = ActivityStatus.Paused;
|
||||
} else {
|
||||
state.status = ActivityStatus.Running;
|
||||
}
|
||||
|
||||
draw(state);
|
||||
}
|
||||
|
||||
function stopActivity(state: AppState): void {
|
||||
if (state.status === ActivityStatus.Paused) {
|
||||
clearActivity(state);
|
||||
}
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.status = ActivityStatus.Paused;
|
||||
} else {
|
||||
state.status = ActivityStatus.Stopped;
|
||||
}
|
||||
|
||||
draw(state);
|
||||
}
|
||||
|
||||
function clearActivity(state: AppState): void {
|
||||
state.duration = 0;
|
||||
state.distance = 0;
|
||||
state.speed = 0;
|
||||
state.steps = 0;
|
||||
state.cadence = 0;
|
||||
}
|
||||
|
||||
export { clearActivity, startActivity, stopActivity };
|
|
@ -1,20 +0,0 @@
|
|||
import { startActivity, stopActivity } from './activity';
|
||||
import { initDisplay } from './display';
|
||||
import { initGps } from './gps';
|
||||
import { initHrm } from './hrm';
|
||||
import { initState } from './state';
|
||||
import { initStep } from './step';
|
||||
|
||||
declare var BTN1: any;
|
||||
declare var BTN3: any;
|
||||
declare var setWatch: any;
|
||||
|
||||
const appState = initState();
|
||||
|
||||
initGps(appState);
|
||||
initHrm(appState);
|
||||
initStep(appState);
|
||||
initDisplay(appState);
|
||||
|
||||
setWatch(() => startActivity(appState), BTN1, { repeat: true, edge: 'falling' });
|
||||
setWatch(() => stopActivity(appState), BTN3, { repeat: true, edge: 'falling' });
|
|
@ -1,123 +0,0 @@
|
|||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
declare var g: any;
|
||||
|
||||
const STATUS_COLORS = {
|
||||
'STOP': 0xF800,
|
||||
'PAUSE': 0xFFE0,
|
||||
'RUN': 0x07E0,
|
||||
}
|
||||
|
||||
function initDisplay(state: AppState): void {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.on('lcdPower', (on: boolean) => {
|
||||
state.drawing = on;
|
||||
if (on) {
|
||||
drawAll(state);
|
||||
}
|
||||
});
|
||||
drawAll(state);
|
||||
}
|
||||
|
||||
function drawBackground(): void {
|
||||
g.clear();
|
||||
g.setColor(0xC618);
|
||||
g.setFont('6x8', 2);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
g.drawString('DIST (KM)', 60, 32);
|
||||
g.drawString('TIME', 172, 32);
|
||||
g.drawString('PACE', 60, 92);
|
||||
g.drawString('HEART', 172, 92);
|
||||
g.drawString('STEPS', 60, 152);
|
||||
g.drawString('CADENCE', 172, 152);
|
||||
}
|
||||
|
||||
function drawValue(value: string, x: number, y: number) {
|
||||
g.setColor(0x0000);
|
||||
g.fillRect(x - 60, y, x + 60, y + 30);
|
||||
g.setColor(0xFFFF);
|
||||
g.drawString(value, x, y);
|
||||
}
|
||||
|
||||
function draw(state: AppState): void {
|
||||
g.setFontVector(30);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
|
||||
drawValue(formatDistance(state.distance), 60, 55);
|
||||
drawValue(formatTime(state.duration), 172, 55);
|
||||
drawValue(formatPace(state.speed), 60, 115);
|
||||
drawValue(state.hr.toFixed(0), 172, 115);
|
||||
drawValue(state.steps.toFixed(0), 60, 175);
|
||||
drawValue(state.cadence.toFixed(0), 172, 175);
|
||||
|
||||
g.setFont('6x8', 2);
|
||||
|
||||
g.setColor(state.gpsValid ? 0x07E0 : 0xF800);
|
||||
g.fillRect(0, 216, 80, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString('GPS', 40, 220);
|
||||
|
||||
g.setColor(0xFFFF);
|
||||
g.fillRect(80, 216, 160, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString(formatClock(new Date()), 120, 220);
|
||||
|
||||
g.setColor(STATUS_COLORS[state.status]);
|
||||
g.fillRect(160, 216, 230, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString(state.status, 200, 220);
|
||||
|
||||
g.setFont("6x8").setFontAlign(0,0,1).setColor(-1);
|
||||
if (state.status === ActivityStatus.Paused) {
|
||||
g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1);
|
||||
} else if (state.status === ActivityStatus.Running) {
|
||||
g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1);
|
||||
} else {
|
||||
g.drawString("START",236,60,1).drawString(" ",236,180,1);
|
||||
}
|
||||
}
|
||||
|
||||
function drawAll(state: AppState) {
|
||||
drawBackground();
|
||||
draw(state);
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
function formatClock(date: Date): string {
|
||||
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
|
||||
}
|
||||
|
||||
function formatDistance(meters: number): string {
|
||||
return (meters / 1000).toFixed(2);
|
||||
}
|
||||
|
||||
function formatPace(speed: number): string {
|
||||
if (speed < 0.1667) {
|
||||
return `__'__"`;
|
||||
}
|
||||
const pace = Math.round(1000 / speed);
|
||||
const min = Math.floor(pace / 60);
|
||||
const sec = pace % 60;
|
||||
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
|
||||
}
|
||||
|
||||
function formatTime(time: number): string {
|
||||
const seconds = Math.round(time);
|
||||
const hrs = Math.floor(seconds / 3600);
|
||||
const min = Math.floor(seconds / 60) % 60;
|
||||
const sec = seconds % 60;
|
||||
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
|
||||
}
|
||||
|
||||
export {
|
||||
draw,
|
||||
drawAll,
|
||||
drawBackground,
|
||||
drawValue,
|
||||
formatClock,
|
||||
formatDistance,
|
||||
formatPace,
|
||||
formatTime,
|
||||
initDisplay,
|
||||
};
|
|
@ -1,90 +0,0 @@
|
|||
import { draw } from './display';
|
||||
import { updateLog } from './log';
|
||||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
|
||||
interface GpsEvent {
|
||||
lat: number;
|
||||
lon: number;
|
||||
alt: number;
|
||||
speed: number;
|
||||
hdop: number;
|
||||
fix: number;
|
||||
}
|
||||
|
||||
const EARTH_RADIUS = 6371008.8;
|
||||
|
||||
function initGps(state: AppState): void {
|
||||
Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps));
|
||||
Bangle.setGPSPower(1);
|
||||
}
|
||||
|
||||
function readGps(state: AppState, gps: GpsEvent): void {
|
||||
state.lat = gps.lat;
|
||||
state.lon = gps.lon;
|
||||
state.alt = gps.alt;
|
||||
state.vel = gps.speed / 3.6;
|
||||
state.fix = gps.fix;
|
||||
state.dop = gps.hdop;
|
||||
state.gpsValid = state.fix > 0;
|
||||
|
||||
updateGps(state);
|
||||
draw(state);
|
||||
|
||||
/* Only log GPS data every 5 secs if we
|
||||
have a fix and we're running. */
|
||||
if (state.gpsValid &&
|
||||
state.status === ActivityStatus.Running &&
|
||||
state.timeSinceLog > 5) {
|
||||
state.timeSinceLog = 0;
|
||||
updateLog(state);
|
||||
}
|
||||
}
|
||||
|
||||
function updateGps(state: AppState): void {
|
||||
const t = Date.now();
|
||||
let dt = (t - state.t) / 1000;
|
||||
if (!isFinite(dt)) dt=0;
|
||||
state.t = t;
|
||||
state.timeSinceLog += dt;
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.duration += dt;
|
||||
}
|
||||
|
||||
if (!state.gpsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = EARTH_RADIUS + state.alt;
|
||||
const lat = state.lat * Math.PI / 180;
|
||||
const lon = state.lon * Math.PI / 180;
|
||||
const x = r * Math.cos(lat) * Math.cos(lon);
|
||||
const y = r * Math.cos(lat) * Math.sin(lon);
|
||||
const z = r * Math.sin(lat);
|
||||
|
||||
if (!state.x) {
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
return;
|
||||
}
|
||||
|
||||
const dx = x - state.x;
|
||||
const dy = y - state.y;
|
||||
const dz = z - state.z;
|
||||
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.distance += dpMag;
|
||||
state.speed = (state.distance / state.duration) || 0;
|
||||
state.cadence = (60 * state.steps / state.duration) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
export { initGps, readGps, updateGps };
|
|
@ -1,29 +0,0 @@
|
|||
import { AppState } from './state';
|
||||
|
||||
interface HrmData {
|
||||
bpm: number;
|
||||
confidence: number;
|
||||
raw: string;
|
||||
}
|
||||
|
||||
declare var Bangle: any;
|
||||
|
||||
function initHrm(state: AppState) {
|
||||
Bangle.on('HRM', (hrm: HrmData) => updateHrm(state, hrm));
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
|
||||
function updateHrm(state: AppState, hrm: HrmData) {
|
||||
if (hrm.confidence === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dHr = hrm.bpm - state.hr;
|
||||
const hrError = Math.abs(dHr) + 101 - hrm.confidence;
|
||||
const hrGain = (state.hrError / (state.hrError + hrError)) || 0;
|
||||
|
||||
state.hr += dHr * hrGain;
|
||||
state.hrError += (hrError - state.hrError) * hrGain;
|
||||
}
|
||||
|
||||
export { initHrm, updateHrm };
|
|
@ -1,40 +0,0 @@
|
|||
import { AppState } from './state';
|
||||
|
||||
declare var require: any;
|
||||
|
||||
function initLog(state: AppState): void {
|
||||
const datetime = new Date().toISOString().replace(/[-:]/g, '');
|
||||
const date = datetime.substr(2, 6);
|
||||
const time = datetime.substr(9, 6);
|
||||
const filename = `banglerun_${date}_${time}`;
|
||||
state.file = require('Storage').open(filename, 'w');
|
||||
state.fileWritten = false;
|
||||
}
|
||||
|
||||
function updateLog(state: AppState): void {
|
||||
if (!state.fileWritten) {
|
||||
state.file.write([
|
||||
'timestamp',
|
||||
'latitude',
|
||||
'longitude',
|
||||
'altitude',
|
||||
'duration',
|
||||
'distance',
|
||||
'heartrate',
|
||||
'steps',
|
||||
].join(',') + '\n');
|
||||
state.fileWritten = true;
|
||||
}
|
||||
state.file.write([
|
||||
Date.now().toFixed(0),
|
||||
state.lat.toFixed(6),
|
||||
state.lon.toFixed(6),
|
||||
state.alt.toFixed(2),
|
||||
state.duration.toFixed(0),
|
||||
state.distance.toFixed(2),
|
||||
state.hr.toFixed(0),
|
||||
state.steps.toFixed(0),
|
||||
].join(',') + '\n');
|
||||
}
|
||||
|
||||
export { initLog, updateLog };
|
|
@ -1,85 +0,0 @@
|
|||
enum ActivityStatus {
|
||||
Stopped = 'STOP',
|
||||
Paused = 'PAUSE',
|
||||
Running = 'RUN',
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
// GPS NMEA data
|
||||
fix: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
alt: number;
|
||||
vel: number;
|
||||
dop: number;
|
||||
gpsValid: boolean;
|
||||
|
||||
// Absolute position data
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
// Last fix time
|
||||
t: number;
|
||||
// Last time we saved log info
|
||||
timeSinceLog : number;
|
||||
|
||||
// HRM data
|
||||
hr: number,
|
||||
hrError: number,
|
||||
|
||||
// Logger data
|
||||
file: File;
|
||||
fileWritten: boolean;
|
||||
|
||||
// Drawing data
|
||||
drawing: boolean;
|
||||
|
||||
// Activity data
|
||||
status: ActivityStatus;
|
||||
duration: number;
|
||||
distance: number;
|
||||
speed: number;
|
||||
steps: number;
|
||||
cadence: number;
|
||||
}
|
||||
|
||||
interface File {
|
||||
read: Function;
|
||||
write: Function;
|
||||
erase: Function;
|
||||
}
|
||||
|
||||
function initState(): AppState {
|
||||
return {
|
||||
fix: NaN,
|
||||
lat: NaN,
|
||||
lon: NaN,
|
||||
alt: NaN,
|
||||
vel: NaN,
|
||||
dop: NaN,
|
||||
gpsValid: false,
|
||||
|
||||
x: NaN,
|
||||
y: NaN,
|
||||
z: NaN,
|
||||
t: NaN,
|
||||
timeSinceLog : 0,
|
||||
|
||||
hr: 60,
|
||||
hrError: 100,
|
||||
|
||||
file: null,
|
||||
fileWritten: false,
|
||||
|
||||
drawing: false,
|
||||
|
||||
status: ActivityStatus.Stopped,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
speed: 0,
|
||||
steps: 0,
|
||||
cadence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export { ActivityStatus, AppState, File, initState };
|
|
@ -1,15 +0,0 @@
|
|||
import { ActivityStatus, AppState } from './state';
|
||||
|
||||
declare var Bangle: any;
|
||||
|
||||
function initStep(state: AppState) {
|
||||
Bangle.on('step', () => updateStep(state));
|
||||
}
|
||||
|
||||
function updateStep(state: AppState) {
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.steps += 1;
|
||||
}
|
||||
}
|
||||
|
||||
export { initStep, updateStep };
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "es2015",
|
||||
"noImplicitAny": true,
|
||||
"target": "es2015"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitAny": true,
|
||||
"target": "es2015"
|
||||
},
|
||||
"include": [
|
||||
"test"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Barometer altitude adjustment setting
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,16 @@
|
|||
## GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude...
|
||||
|
||||
...all taken from internal sources.
|
||||
|
||||
#### To speed-up GPS reception it is strongly recommended to upload AGPS data with ["Assisted GPS Update"](https://banglejs.com/apps/?id=assistedgps)
|
||||
|
||||
#### If "CALIB!" is shown on the display or the compass heading differs too much from GPS heading, compass calibration should be done with the ["Navigation Compass" App](https://banglejs.com/apps/?id=magnav)
|
||||
|
||||
Permanently diverging Barometer Altitude values can be compensated in the settings menu.
|
||||
|
||||
Please report bugs to https://github.com/espruino/BangleApps/issues/new?assignees=&labels=bug&template=bangle-bug-report-custom-form.yaml&title=%5BBike+Speedometer%5D+Short+description+of+bug
|
||||
|
||||
**Credits:**<br>
|
||||
Bike Speedometer App by <i>github.com/HilmarSt</i><br>
|
||||
Big parts of the software are based on <i>github.com/espruino/BangleApps/tree/master/apps/speedalt</i><br>
|
||||
Compass and Compass Calibration based on <i>github.com/espruino/BangleApps/tree/master/apps/magnav</i>
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgP/ABO/AokfAgf+r4FD3lPBQcZw4FC/nD+4FC/Pn+YFCBIP7GQ4aDEIMDAol/ApQRFuAFEv0/BoQXBx0HAoPgh/nn40C4fwEoP+n/4/BWC/weBBYP5BAM/C4Pz7/7z+f//n7/z5/f//vA4Pv5//AIPv8/n//d//Ou5yBDIOfu58Bz42B+Z8Bz/8AoPgv+/AoP7w0f3IFBnc/5+bL4Oyv/nEYP/+X/mYFC+n8mff8ln+v4vfd7tfsvzvfN7tPtv2vPn6H35vg/f36vX7vj/fz9vvznH+Z3B/0+5/3/l//iDBMwMf+KEBOAPBUoOCj///CNBUQQAEA="))
|
|
@ -0,0 +1,554 @@
|
|||
// Bike Speedometer by https://github.com/HilmarSt
|
||||
// Big parts of this software are based on https://github.com/espruino/BangleApps/tree/master/apps/speedalt
|
||||
// Compass and Compass Calibration based on https://github.com/espruino/BangleApps/tree/master/apps/magnav
|
||||
|
||||
const BANGLEJS2 = 1;
|
||||
const screenH = g.getHeight();
|
||||
const screenYstart = 24; // 0..23 for widgets
|
||||
const screenY_Half = screenH / 2 + screenYstart;
|
||||
const screenW = g.getWidth();
|
||||
const screenW_Half = screenW / 2;
|
||||
const fontFactorB2 = 2/3;
|
||||
const colfg=g.theme.fg, colbg=g.theme.bg;
|
||||
const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
|
||||
|
||||
var altiGPS=0, altiBaro=0;
|
||||
var hdngGPS=0, hdngCompass=0, calibrateCompass=false;
|
||||
|
||||
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
||||
var KalmanFilter = (function () {
|
||||
'use strict';
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
function _defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* KalmanFilter
|
||||
* @class
|
||||
* @author Wouter Bulten
|
||||
* @see {@link http://github.com/wouterbulten/kalmanjs}
|
||||
* @version Version: 1.0.0-beta
|
||||
* @copyright Copyright 2015-2018 Wouter Bulten
|
||||
* @license MIT License
|
||||
* @preserve
|
||||
*/
|
||||
var KalmanFilter =
|
||||
/*#__PURE__*/
|
||||
function () {
|
||||
/**
|
||||
* Create 1-dimensional kalman filter
|
||||
* @param {Number} options.R Process noise
|
||||
* @param {Number} options.Q Measurement noise
|
||||
* @param {Number} options.A State vector
|
||||
* @param {Number} options.B Control vector
|
||||
* @param {Number} options.C Measurement vector
|
||||
* @return {KalmanFilter}
|
||||
*/
|
||||
function KalmanFilter() {
|
||||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
||||
_ref$R = _ref.R,
|
||||
R = _ref$R === void 0 ? 1 : _ref$R,
|
||||
_ref$Q = _ref.Q,
|
||||
Q = _ref$Q === void 0 ? 1 : _ref$Q,
|
||||
_ref$A = _ref.A,
|
||||
A = _ref$A === void 0 ? 1 : _ref$A,
|
||||
_ref$B = _ref.B,
|
||||
B = _ref$B === void 0 ? 0 : _ref$B,
|
||||
_ref$C = _ref.C,
|
||||
C = _ref$C === void 0 ? 1 : _ref$C;
|
||||
|
||||
_classCallCheck(this, KalmanFilter);
|
||||
|
||||
this.R = R; // noise power desirable
|
||||
|
||||
this.Q = Q; // noise power estimated
|
||||
|
||||
this.A = A;
|
||||
this.C = C;
|
||||
this.B = B;
|
||||
this.cov = NaN;
|
||||
this.x = NaN; // estimated signal without noise
|
||||
}
|
||||
/**
|
||||
* Filter a new value
|
||||
* @param {Number} z Measurement
|
||||
* @param {Number} u Control
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
|
||||
_createClass(KalmanFilter, [{
|
||||
key: "filter",
|
||||
value: function filter(z) {
|
||||
var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
||||
|
||||
if (isNaN(this.x)) {
|
||||
this.x = 1 / this.C * z;
|
||||
this.cov = 1 / this.C * this.Q * (1 / this.C);
|
||||
} else {
|
||||
// Compute prediction
|
||||
var predX = this.predict(u);
|
||||
var predCov = this.uncertainty(); // Kalman gain
|
||||
|
||||
var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction
|
||||
|
||||
this.x = predX + K * (z - this.C * predX);
|
||||
this.cov = predCov - K * this.C * predCov;
|
||||
}
|
||||
|
||||
return this.x;
|
||||
}
|
||||
/**
|
||||
* Predict next value
|
||||
* @param {Number} [u] Control
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "predict",
|
||||
value: function predict() {
|
||||
var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
||||
return this.A * this.x + this.B * u;
|
||||
}
|
||||
/**
|
||||
* Return uncertainty of filter
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "uncertainty",
|
||||
value: function uncertainty() {
|
||||
return this.A * this.cov * this.A + this.R;
|
||||
}
|
||||
/**
|
||||
* Return the last filtered measurement
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "lastMeasurement",
|
||||
value: function lastMeasurement() {
|
||||
return this.x;
|
||||
}
|
||||
/**
|
||||
* Set measurement noise Q
|
||||
* @param {Number} noise
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "setMeasurementNoise",
|
||||
value: function setMeasurementNoise(noise) {
|
||||
this.Q = noise;
|
||||
}
|
||||
/**
|
||||
* Set the process noise R
|
||||
* @param {Number} noise
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "setProcessNoise",
|
||||
value: function setProcessNoise(noise) {
|
||||
this.R = noise;
|
||||
}
|
||||
}]);
|
||||
|
||||
return KalmanFilter;
|
||||
}();
|
||||
|
||||
return KalmanFilter;
|
||||
|
||||
}());
|
||||
|
||||
|
||||
//==================================== MAIN ====================================
|
||||
|
||||
var lf = {fix:0,satellites:0};
|
||||
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
||||
var canDraw = 1;
|
||||
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
|
||||
var sec; // actual seconds for testing purposes
|
||||
|
||||
var max = {};
|
||||
max.spd = 0;
|
||||
max.alt = 0;
|
||||
max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
|
||||
|
||||
var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
|
||||
|
||||
var wp = {}; // Waypoint to use for distance from cur position.
|
||||
var SATinView = 0;
|
||||
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function distance(a,b){
|
||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
|
||||
// Distance in selected units
|
||||
var d = Math.sqrt(x*x + y*y) * 6371000;
|
||||
d = (d/parseFloat(cfg.dist)).toFixed(2);
|
||||
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
|
||||
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
function drawFix(dat) {
|
||||
|
||||
if (!canDraw) return;
|
||||
|
||||
g.clearRect(0,screenYstart,screenW,screenH);
|
||||
|
||||
var v = '';
|
||||
var u='';
|
||||
|
||||
// Primary Display
|
||||
v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
|
||||
|
||||
// Primary Units
|
||||
u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units;
|
||||
|
||||
drawPrimary(v,u);
|
||||
|
||||
// Secondary Display
|
||||
v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString();
|
||||
|
||||
// Secondary Units
|
||||
u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit;
|
||||
|
||||
drawSecondary(v,u);
|
||||
|
||||
// Time
|
||||
drawTime();
|
||||
|
||||
//Sats
|
||||
if ( dat.age > 10 ) {
|
||||
if ( dat.age > 90 ) dat.age = '>90';
|
||||
drawSats('Age:'+dat.age);
|
||||
}
|
||||
else if (!BANGLEJS2) {
|
||||
drawSats('Sats:'+dat.sats);
|
||||
} else {
|
||||
if (lf.fix) {
|
||||
drawSats('Sats:'+dat.sats);
|
||||
} else {
|
||||
drawSats('View:' + SATinView);
|
||||
}
|
||||
}
|
||||
g.reset();
|
||||
}
|
||||
|
||||
|
||||
function drawClock() {
|
||||
if (!canDraw) return;
|
||||
g.clearRect(0,screenYstart,screenW,screenH);
|
||||
drawTime();
|
||||
g.reset();
|
||||
}
|
||||
|
||||
|
||||
function drawPrimary(n,u) {
|
||||
//if(emulator)console.log("\n1: " + n +" "+ u);
|
||||
var s=40; // Font size
|
||||
var l=n.length;
|
||||
|
||||
if ( l <= 7 ) s=48;
|
||||
if ( l <= 6 ) s=55;
|
||||
if ( l <= 5 ) s=66;
|
||||
if ( l <= 4 ) s=85;
|
||||
if ( l <= 3 ) s=110;
|
||||
|
||||
// X -1=left (default), 0=center, 1=right
|
||||
// Y -1=top (default), 0=center, 1=bottom
|
||||
g.setFontAlign(0,-1); // center, top
|
||||
if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
g.setFontVector(s);
|
||||
g.drawString(n, screenW_Half - 10, screenYstart);
|
||||
|
||||
// Primary Units
|
||||
s = 35; // Font size
|
||||
g.setFontAlign(1,-1,3); // right, top, rotate
|
||||
g.setColor(col1);
|
||||
if (BANGLEJS2) s = 20;
|
||||
g.setFontVector(s);
|
||||
g.drawString(u, screenW - 20, screenYstart + 2);
|
||||
}
|
||||
|
||||
|
||||
function drawSecondary(n,u) {
|
||||
//if(emulator)console.log("2: " + n +" "+ u);
|
||||
|
||||
if (calibrateCompass) hdngCompass = "CALIB!";
|
||||
else hdngCompass +="°";
|
||||
|
||||
g.setFontAlign(0,1);
|
||||
g.setColor(col1);
|
||||
|
||||
g.setFontVector(12).drawString("Altitude GPS / Barometer", screenW_Half - 5, screenY_Half - 10);
|
||||
g.setFontVector(20);
|
||||
g.drawString(n+" "+u+" / "+altiBaro+" "+u, screenW_Half, screenY_Half + 11);
|
||||
|
||||
g.setFontVector(12).drawString("Heading GPS / Compass", screenW_Half - 10, screenY_Half + 26);
|
||||
g.setFontVector(20);
|
||||
g.drawString(hdngGPS+"° / "+hdngCompass, screenW_Half, screenY_Half + 47);
|
||||
}
|
||||
|
||||
|
||||
function drawTime() {
|
||||
var x = 0, y = screenH;
|
||||
g.setFontAlign(-1,1); // left, bottom
|
||||
g.setFont("6x8", 2);
|
||||
|
||||
g.setColor(colbg);
|
||||
g.drawString(time,x+1,y); // clear old time
|
||||
|
||||
time = require("locale").time(new Date(),1);
|
||||
|
||||
g.setColor(colfg); // draw new time
|
||||
g.drawString(time,x+2,y);
|
||||
}
|
||||
|
||||
|
||||
function drawSats(sats) {
|
||||
|
||||
g.setColor(col1);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(1,1); //right, bottom
|
||||
g.drawString(sats,screenW,screenH);
|
||||
|
||||
g.setFontVector(18);
|
||||
g.setColor(col1);
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
if ( showMax ) {
|
||||
g.setFontAlign(0,1); //centre, bottom
|
||||
g.drawString('MAX',120,164);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onGPS(fix) {
|
||||
|
||||
if ( emulator ) {
|
||||
fix.fix = 1;
|
||||
fix.speed = Math.random()*30; // calmed by Kalman filter if cfg.spdFilt
|
||||
fix.alt = Math.random()*200 -20; // calmed by Kalman filter if cfg.altFilt
|
||||
fix.lat = 50.59; // google.de/maps/@50.59,8.53,17z
|
||||
fix.lon = 8.53;
|
||||
fix.course = 365;
|
||||
fix.satellites = sec;
|
||||
fix.time = new Date();
|
||||
fix.smoothed = 0;
|
||||
}
|
||||
|
||||
var m;
|
||||
|
||||
var sp = '---';
|
||||
var al = '---';
|
||||
var di = '---';
|
||||
var age = '---';
|
||||
|
||||
if (fix.fix) lf = fix;
|
||||
|
||||
hdngGPS = lf.course;
|
||||
if (isNaN(hdngGPS)) hdngGPS = "---";
|
||||
else if (0 == hdngGPS) hdngGPS = "0?";
|
||||
else hdngGPS = hdngGPS.toFixed(0);
|
||||
|
||||
if (emulator) hdngCompass = hdngGPS;
|
||||
if (emulator) altiBaro = lf.alt.toFixed(0);
|
||||
|
||||
if (lf.fix) {
|
||||
|
||||
if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw);
|
||||
|
||||
// Smooth data
|
||||
if ( lf.smoothed !== 1 ) {
|
||||
if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
|
||||
if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
|
||||
lf.smoothed = 1;
|
||||
if ( max.n <= 15 ) max.n++;
|
||||
}
|
||||
|
||||
|
||||
// Speed
|
||||
if ( cfg.spd == 0 ) {
|
||||
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
|
||||
sp = parseFloat(m[1]);
|
||||
cfg.spd_unit = m[2];
|
||||
}
|
||||
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
||||
|
||||
if ( sp < 10 ) sp = sp.toFixed(1);
|
||||
else sp = Math.round(sp);
|
||||
if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
|
||||
|
||||
// Altitude
|
||||
al = lf.alt;
|
||||
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
|
||||
if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al);
|
||||
|
||||
// Distance to waypoint
|
||||
di = distance(lf,wp);
|
||||
if (isNaN(di)) di = 0;
|
||||
|
||||
// Age of last fix (secs)
|
||||
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
if ( showMax )
|
||||
drawFix({
|
||||
speed:max.spd,
|
||||
sats:lf.satellites,
|
||||
alt:max.alt,
|
||||
alt_units:cfg.alt_unit,
|
||||
age:age,
|
||||
fix:lf.fix
|
||||
}); // Speed and alt maximums
|
||||
else
|
||||
drawFix({
|
||||
speed:sp,
|
||||
sats:lf.satellites,
|
||||
alt:al,
|
||||
alt_units:cfg.alt_unit,
|
||||
age:age,
|
||||
fix:lf.fix
|
||||
}); // Show speed/altitude
|
||||
}
|
||||
}
|
||||
|
||||
function setButtons(){
|
||||
setWatch(_=>load(), BTN1);
|
||||
|
||||
onGPS(lf);
|
||||
}
|
||||
|
||||
|
||||
function updateClock() {
|
||||
if (!canDraw) return;
|
||||
drawTime();
|
||||
g.reset();
|
||||
|
||||
if ( emulator ) {
|
||||
max.spd++; max.alt++;
|
||||
d=new Date(); sec=d.getSeconds();
|
||||
onGPS(lf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =Main Prog
|
||||
|
||||
// Read settings.
|
||||
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
|
||||
|
||||
cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd_unit = 'km/h'; // Displayed speed unit
|
||||
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
|
||||
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
|
||||
cfg.dist = 1000; // Multiplier for distnce unit conversions.
|
||||
cfg.dist_unit = 'km'; // Displayed distnce units
|
||||
cfg.modeA = 1;
|
||||
cfg.primSpd = 1; // 1 = Spd in primary, 0 = Spd in secondary
|
||||
|
||||
cfg.altDiff = cfg.altDiff==undefined?100:cfg.altDiff;
|
||||
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
||||
cfg.altFilt = cfg.altFilt==undefined?false:cfg.altFilt;
|
||||
// console.log("cfg.altDiff: " + cfg.altDiff);
|
||||
// console.log("cfg.spdFilt: " + cfg.spdFilt);
|
||||
// console.log("cfg.altFilt: " + cfg.altFilt);
|
||||
|
||||
if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
|
||||
if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
|
||||
|
||||
function onGPSraw(nmea) {
|
||||
var nofGP = 0, nofBD = 0, nofGL = 0;
|
||||
if (nmea.slice(3,6) == "GSV") {
|
||||
// console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
|
||||
SATinView = nofGP + nofBD + nofGL;
|
||||
} }
|
||||
if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
|
||||
|
||||
function onPressure(dat) {
|
||||
altiBaro = Number(dat.altitude.toFixed(0)) + Number(cfg.altDiff);
|
||||
}
|
||||
|
||||
Bangle.setBarometerPower(1); // needs some time...
|
||||
g.clearRect(0,screenYstart,screenW,screenH);
|
||||
onGPS(lf);
|
||||
Bangle.setGPSPower(1);
|
||||
Bangle.on('GPS', onGPS);
|
||||
Bangle.on('pressure', onPressure);
|
||||
|
||||
Bangle.setCompassPower(1);
|
||||
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
|
||||
if (!CALIBDATA) calibrateCompass = true;
|
||||
function Compass_tiltfixread(O,S){
|
||||
"ram";
|
||||
//console.log(O.x+" "+O.y+" "+O.z);
|
||||
var m = Bangle.getCompass();
|
||||
var g = Bangle.getAccel();
|
||||
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
|
||||
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
|
||||
if (d<0) d+=360;
|
||||
var phi = Math.atan(-g.x/-g.z);
|
||||
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
|
||||
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
|
||||
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
|
||||
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
|
||||
var yh = m.dz*sinphi - m.dx*cosphi;
|
||||
var psi = Math.atan2(yh,xh)*180/Math.PI;
|
||||
if (psi<0) psi+=360;
|
||||
return psi;
|
||||
}
|
||||
var Compass_heading = 0;
|
||||
function Compass_newHeading(m,h){
|
||||
var s = Math.abs(m - h);
|
||||
var delta = (m>h)?1:-1;
|
||||
if (s>=180){s=360-s; delta = -delta;}
|
||||
if (s<2) return h;
|
||||
var hd = h + delta*(1 + Math.round(s/5));
|
||||
if (hd<0) hd+=360;
|
||||
if (hd>360)hd-= 360;
|
||||
return hd;
|
||||
}
|
||||
function Compass_reading() {
|
||||
"ram";
|
||||
var d = Compass_tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
||||
Compass_heading = Compass_newHeading(d,Compass_heading);
|
||||
hdngCompass = Compass_heading.toFixed(0);
|
||||
}
|
||||
if (!calibrateCompass) setInterval(Compass_reading,200);
|
||||
|
||||
setButtons();
|
||||
if (emulator) setInterval(updateClock, 2000);
|
||||
else setInterval(updateClock, 10000);
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
After Width: | Height: | Size: 751 B |
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "bikespeedo",
|
||||
"name": "Bike Speedometer (beta)",
|
||||
"shortName": "Bike Speedometer",
|
||||
"version": "0.02",
|
||||
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"Screenshot.png"}],
|
||||
"type": "app",
|
||||
"tags": "tool,cycling,bicycle,outdoors,sport",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"bikespeedo.app.js","url":"app.js"},
|
||||
{"name":"bikespeedo.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"bikespeedo.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"bikespeedo.json"}]
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
(function(back) {
|
||||
|
||||
let settings = require('Storage').readJSON('bikespeedo.json',1)||{};
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').write('bikespeedo.json',settings);
|
||||
}
|
||||
|
||||
const appMenu = {
|
||||
'': {'title': 'Bike Speedometer'},
|
||||
'< Back': back,
|
||||
'< Load Bike Speedometer': ()=>{load('bikespeedo.app.js');},
|
||||
'Barometer Altitude adjustment' : function() { E.showMenu(altdiffMenu); },
|
||||
'Kalman Filters' : function() { E.showMenu(kalMenu); }
|
||||
};
|
||||
|
||||
const altdiffMenu = {
|
||||
'': { 'title': 'Altitude adjustment' },
|
||||
'< Back': function() { E.showMenu(appMenu); },
|
||||
'Altitude delta': {
|
||||
value: settings.altDiff || 100,
|
||||
min: -200,
|
||||
max: 200,
|
||||
step: 10,
|
||||
onchange: v => {
|
||||
settings.altDiff = v;
|
||||
writeSettings(); }
|
||||
}
|
||||
};
|
||||
|
||||
const kalMenu = {
|
||||
'': {'title': 'Kalman Filters'},
|
||||
'< Back': function() { E.showMenu(appMenu); },
|
||||
'Speed' : {
|
||||
value : settings.spdFilt,
|
||||
format : v => v?"On":"Off",
|
||||
onchange : () => { settings.spdFilt = !settings.spdFilt; writeSettings(); }
|
||||
},
|
||||
'Altitude' : {
|
||||
value : settings.altFilt,
|
||||
format : v => v?"On":"Off",
|
||||
onchange : () => { settings.altFilt = !settings.altFilt; writeSettings(); }
|
||||
}
|
||||
};
|
||||
|
||||
E.showMenu(appMenu);
|
||||
|
||||
});
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New game! BTN4- Hit card, BTN5- Stand
|
||||
0.02: ignore buttons on pauses
|
||||
0.02: Ignore buttons on pauses
|
||||
0.03: Support Bangle.js 2
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
var Clubs = require("heatshrink").decompress(atob("j0ewcBkmSpICipEAiQLHwA3BBY8gBQMEEA1AJwQgGyAKChILGBQUCFgxwDJpEAO5AVCII44CAQI1GAAg1GAAZQCWxCDEAAqJBQYQAFRIJWCAApcCR4YADPoRWCgQdBPopfCwAdBTw47BcBAvBU44vDfBDUIRIbUHATuQ"));
|
||||
|
||||
var Spades = require("heatshrink").decompress(atob("j0ewcBkmSpICuoALJIQILHpAKBJQ+QLIUJBYsgMoY1GBQcCBYmAPgkSEBEAgggIKApBDIg4KFHAZiCAAgsDBQw4DFitJFhQ4FTwplBgRoCSQoRBBYJ6EF4jgUwDUHAVOQA=="));
|
||||
|
||||
var Hearts = require("heatshrink").decompress(atob("j0ewY96gMkyAEByVIBQcSpILBhMkBYkEyQLBAQYKCCIQLEEwQgCBYuAEBFJkBBCBYw4CEA44CgQLHIYQsHLJsAEBJEHSQhxENwQADMQoAEKAdAWowLCYJESXggAFGowA/AAQ"));
|
||||
|
||||
var Diamonds = require("heatshrink").decompress(atob("j0ewY1ykgKJhIKJiVIEBOSoAKHpILBBQ+SBYOQBIsBCgILBwAKEgQgCAQIKEggICAQMgKwgUDAQI1GBY4IFLgoLGJpGSPoo4EMoxNIMoqSHiR6HLgizIPoLgfAFA"));
|
||||
|
||||
var deck = [];
|
||||
var player = {Hand:[]};
|
||||
var computer = {Hand:[]};
|
||||
var ctx = {ready:true};
|
||||
|
||||
function createDeck() {
|
||||
var suits = ["Spades", "Hearts", "Diamonds", "Clubs"];
|
||||
var values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];
|
||||
|
||||
var dck = [];
|
||||
for (var i = 0 ; i < values.length; i++) {
|
||||
for(var x = 0; x < suits.length; x++) {
|
||||
dck.push({ Value: values[i], Suit: suits[x] });
|
||||
}
|
||||
}
|
||||
return dck;
|
||||
}
|
||||
|
||||
function shuffle(a) {
|
||||
var j, x, i;
|
||||
for (i = a.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1));
|
||||
x = a[i];
|
||||
a[i] = a[j];
|
||||
a[j] = x;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function EndGameMessdage(msg){
|
||||
ctx.ready = false;
|
||||
g.clearRect(0,160,176,176);
|
||||
g.setColor(255,255,255);
|
||||
g.fillRect(0,160,176,176);
|
||||
g.setColor(0,0,0);
|
||||
g.drawString(msg, 12, 155);
|
||||
setTimeout(function(){
|
||||
startGame();
|
||||
}, 2500);
|
||||
|
||||
}
|
||||
|
||||
function hitMe() {
|
||||
if (!ctx.ready) return;
|
||||
player.Hand.push(deck.pop());
|
||||
renderOnScreen(1);
|
||||
var playerWeight = calcWeight(player.Hand, 0);
|
||||
|
||||
if(playerWeight == 21)
|
||||
EndGameMessdage('WINNER');
|
||||
else if(playerWeight > 21)
|
||||
EndGameMessdage('LOSER');
|
||||
}
|
||||
|
||||
function calcWeight(hand, hideCard) {
|
||||
if(hideCard === 1) {
|
||||
if (hand[0].Value == "J" || hand[0].Value == "Q" || hand[0].Value == "K")
|
||||
return "10 +";
|
||||
else if (hand[0].Value == "A")
|
||||
return "11 +";
|
||||
else
|
||||
return parseInt(hand[0].Value) +" +";
|
||||
}
|
||||
else {
|
||||
var weight = 0;
|
||||
for(i=0; i<hand.length; i++){
|
||||
if (hand[i].Value == "J" || hand[i].Value == "Q" || hand[i].Value == "K") {
|
||||
weight += 10;
|
||||
}
|
||||
else if (hand[i].Value == "A") {
|
||||
weight += 1;
|
||||
}
|
||||
else
|
||||
weight += parseInt(hand[i].Value);
|
||||
}
|
||||
|
||||
// Find count of aces because it may be 11 or 1
|
||||
var numOfAces = hand.filter(function(x){ return x.Value === "A"; }).length;
|
||||
for (var j = 0; j < numOfAces; j++) {
|
||||
if (weight + 10 <= 21) {
|
||||
weight +=10;
|
||||
}
|
||||
}
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
|
||||
function stand(){
|
||||
if (!ctx.ready) return;
|
||||
ctx.ready = false;
|
||||
function sleepFor( sleepDuration ){
|
||||
console.log("Sleeping...");
|
||||
var now = new Date().getTime();
|
||||
while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
|
||||
}
|
||||
|
||||
renderOnScreen(0);
|
||||
var playerWeight = calcWeight(player.Hand, 0);
|
||||
var bangleWeight = calcWeight(computer.Hand, 0);
|
||||
|
||||
while(bangleWeight<17){
|
||||
sleepFor(500);
|
||||
computer.Hand.push(deck.pop());
|
||||
renderOnScreen(0);
|
||||
bangleWeight = calcWeight(computer.Hand, 0);
|
||||
}
|
||||
|
||||
if (bangleWeight == playerWeight)
|
||||
EndGameMessdage('TIES');
|
||||
else if(playerWeight==21 || bangleWeight > 21 || bangleWeight < playerWeight)
|
||||
EndGameMessdage('WINNER');
|
||||
else if(bangleWeight > playerWeight)
|
||||
EndGameMessdage('LOOSER');
|
||||
}
|
||||
|
||||
function renderOnScreen(HideCard) {
|
||||
const fontName = "6x8";
|
||||
|
||||
g.clear(); // clear screen
|
||||
g.reset(); // default draw styles
|
||||
g.setFont(fontName, 1);
|
||||
|
||||
g.setColor(255,255,255);
|
||||
g.fillRect(Bangle.appRect);
|
||||
g.setColor(0,0,0);
|
||||
|
||||
g.drawString('Hit', 176/4-10, 160);
|
||||
g.drawString('Stand', 176/4+176/2-10, 160);
|
||||
|
||||
g.setFont(fontName, 3);
|
||||
for(i=0; i<computer.Hand.length; i++){
|
||||
g.drawImage(eval(computer.Hand[i].Suit), i*40, -1);
|
||||
if(i == 1 && HideCard == 1)
|
||||
g.drawString("?", i*40+8, 30);
|
||||
else
|
||||
g.drawString(computer.Hand[i].Value, i*40+8, 30);
|
||||
}
|
||||
g.setFont(fontName, 2);
|
||||
g.drawString('AI has '+ calcWeight(computer.Hand, HideCard), 5, 55);
|
||||
|
||||
g.setFont(fontName, 3);
|
||||
for(i=0; i<player.Hand.length; i++){
|
||||
g.drawImage(eval(player.Hand[i].Suit), i*40, 83);
|
||||
g.drawString(player.Hand[i].Value, i*40+8, 110);
|
||||
}
|
||||
g.setFont(fontName, 2);
|
||||
g.drawString('You have ' + calcWeight(player.Hand, 0), 5, 133);
|
||||
}
|
||||
|
||||
function dealHands() {
|
||||
player.Hand= [];
|
||||
computer.Hand= [];
|
||||
ctx.ready = false;
|
||||
|
||||
setTimeout(function(){
|
||||
player.Hand.push(deck.pop());
|
||||
renderOnScreen(0);
|
||||
}, 500);
|
||||
|
||||
setTimeout(function(){
|
||||
computer.Hand.push(deck.pop());
|
||||
renderOnScreen(1);
|
||||
}, 1000);
|
||||
|
||||
setTimeout(function(){
|
||||
player.Hand.push(deck.pop());
|
||||
renderOnScreen(1);
|
||||
}, 1500);
|
||||
|
||||
setTimeout(function(){
|
||||
computer.Hand.push(deck.pop());
|
||||
renderOnScreen(1);
|
||||
ctx.ready = true;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function startGame(){
|
||||
deck = createDeck();
|
||||
deck = shuffle(deck);
|
||||
dealHands();
|
||||
}
|
||||
|
||||
Bangle.on('touch', function(btn, e){
|
||||
var left = parseInt(g.getWidth() * 0.2);
|
||||
var right = g.getWidth() - left;
|
||||
|
||||
var is_left = e.x < left;
|
||||
var is_right = e.x > right;
|
||||
|
||||
if(is_left){
|
||||
hitMe();
|
||||
|
||||
} else if(is_right){
|
||||
stand();
|
||||
}
|
||||
});
|
||||
setWatch(startGame, BTN1, {repeat:true, edge:"falling"});
|
||||
|
||||
startGame();
|
|
@ -2,15 +2,16 @@
|
|||
"id": "blackjack",
|
||||
"name": "Black Jack game",
|
||||
"shortName": "Black Jack game",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "Simple implementation of card game Black Jack",
|
||||
"icon": "blackjack.png",
|
||||
"tags": "game",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"screenshots": [{"url":"bangle1-black-jack-game-screenshot.png"}],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"blackjack.app.js","url":"blackjack.app.js"},
|
||||
{"name":"blackjack.app.js","url":"blackjack.app.js","supports": ["BANGLEJS"]},
|
||||
{"name":"blackjack.app.js","url":"appb2.js","supports": ["BANGLEJS2"]},
|
||||
{"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fixed issue with wrong device informations
|
||||
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
||||
0.04: Set Bangle.js 2 compatible, show widgets
|
||||
|
|
|
@ -5,6 +5,7 @@ let menu = {
|
|||
|
||||
function showMainMenu() {
|
||||
menu["< Back"] = () => load();
|
||||
Bangle.drawWidgets();
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
|
@ -55,5 +56,6 @@ function waitMessage() {
|
|||
E.showMessage("scanning");
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
scan();
|
||||
waitMessage();
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"id": "bledetect",
|
||||
"name": "BLE Detector",
|
||||
"shortName": "BLE Detector",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Detect BLE devices and show some informations.",
|
||||
"icon": "bledetect.png",
|
||||
"tags": "app,bluetooth,tool",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"bledetect.app.js","url":"bledetect.js"},
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.03: Tweak for more efficient rendering, and firmware 2v06
|
||||
0.04: Work with themes, smaller screens
|
||||
0.05: Adjust hand lengths to be within 'tick' points
|
||||
0.06: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
|
||||
|
|
|
@ -129,14 +129,6 @@ Bangle.on('lcdPower', (on) => {
|
|||
clearTimers();
|
||||
}
|
||||
});
|
||||
Bangle.on('faceUp',function(up){
|
||||
//console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn());
|
||||
if (up && !Bangle.isLCDOn()) {
|
||||
//console.log("faceUp and LCD off");
|
||||
clearTimers();
|
||||
Bangle.setLCDPower(true);
|
||||
}
|
||||
});
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boldclk",
|
||||
"name": "Bold Clock",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Simple, readable and practical clock",
|
||||
"icon": "bold_clock.png",
|
||||
"screenshots": [{"url":"screenshot_bold.png"}],
|
||||
|
|
|
@ -46,3 +46,8 @@
|
|||
0.40: Bootloader now rebuilds for new firmware versions
|
||||
0.41: Add Keyboard and Mouse Bluetooth HID option
|
||||
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.boot.js
|
||||
0.43: Fix Gadgetbridge handling with Programmable:off
|
||||
0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)
|
||||
0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
|
||||
0.46: Fix no clock found error on Bangle.js 2
|
||||
0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -4,7 +4,7 @@ of the time. */
|
|||
E.showMessage("Updating boot0...");
|
||||
var s = require('Storage').readJSON('setting.json',1)||{};
|
||||
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
|
||||
var boot = "";
|
||||
var boot = "", bootPost = "";
|
||||
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
|
||||
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
|
||||
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
|
||||
|
@ -15,6 +15,7 @@ if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't chang
|
|||
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
|
||||
boot += `E.setFlags({pretokenise:1});\n`;
|
||||
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
|
||||
bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
|
||||
if (s.ble!==false) {
|
||||
if (s.HID) { // Human interface device
|
||||
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
|
||||
|
@ -38,7 +39,7 @@ LoopbackA.setConsole(true);\n`;
|
|||
boot += `
|
||||
Bluetooth.line="";
|
||||
Bluetooth.on('data',function(d) {
|
||||
var l = (Bluetooth.line + d).split("\n");
|
||||
var l = (Bluetooth.line + d).split(/[\\n\\r]/);
|
||||
Bluetooth.line = l.pop();
|
||||
l.forEach(n=>Bluetooth.emit("line",n));
|
||||
});
|
||||
|
@ -96,52 +97,20 @@ delete g.theme; // deleting stops us getting confused by our own decl. builtins
|
|||
if (!g.theme) {
|
||||
boot += `g.theme={fg:-1,bg:0,fg2:-1,bg2:7,fgH:-1,bgH:0x02F7,dark:true};\n`;
|
||||
}
|
||||
delete Bangle.setUI; // deleting stops us getting confused by our own decl. builtins can't be deleted
|
||||
if (!Bangle.setUI) { // assume this is just for F18 - Q3 should already have it
|
||||
boot += `Bangle.setUI=function(mode, cb) {
|
||||
if (Bangle.btnWatches) {
|
||||
Bangle.btnWatches.forEach(clearWatch);
|
||||
delete Bangle.btnWatches;
|
||||
}
|
||||
if (Bangle.swipeHandler) {
|
||||
Bangle.removeListener("swipe", Bangle.swipeHandler);
|
||||
delete Bangle.swipeHandler;
|
||||
}
|
||||
if (Bangle.touchHandler) {
|
||||
Bangle.removeListener("touch", Bangle.touchHandler);
|
||||
delete Bangle.touchHandler;
|
||||
}
|
||||
if (!mode) return;
|
||||
else if (mode=="updown") {
|
||||
Bangle.btnWatches = [
|
||||
setWatch(function() { cb(-1); }, BTN1, {repeat:1}),
|
||||
setWatch(function() { cb(1); }, BTN3, {repeat:1}),
|
||||
setWatch(function() { cb(); }, BTN2, {repeat:1})
|
||||
];
|
||||
} else if (mode=="leftright") {
|
||||
Bangle.btnWatches = [
|
||||
setWatch(function() { cb(-1); }, BTN1, {repeat:1}),
|
||||
setWatch(function() { cb(1); }, BTN3, {repeat:1}),
|
||||
setWatch(function() { cb(); }, BTN2, {repeat:1})
|
||||
];
|
||||
Bangle.swipeHandler = d => {cb(d);};
|
||||
Bangle.on("swipe", Bangle.swipeHandler);
|
||||
Bangle.touchHandler = d => {cb();};
|
||||
Bangle.on("touch", Bangle.touchHandler);
|
||||
} else if (mode=="clock") {
|
||||
Bangle.CLOCK=1;
|
||||
Bangle.btnWatches = [
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:1,edge:"falling"})
|
||||
];
|
||||
} else if (mode=="clockupdown") {
|
||||
Bangle.CLOCK=1;
|
||||
Bangle.btnWatches = [
|
||||
setWatch(function() { cb(-1); }, BTN1, {repeat:1}),
|
||||
setWatch(function() { cb(1); }, BTN3, {repeat:1}),
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:1,edge:"falling"})
|
||||
];
|
||||
} else
|
||||
throw new Error("Unknown UI mode");
|
||||
try {
|
||||
Bangle.setUI({}); // In 2v12.xx we added the option for mode to be an object - for 2v12 and earlier, add a fix if it fails with an object supplied
|
||||
} catch(e) {
|
||||
boot += `Bangle._setUI = Bangle.setUI;
|
||||
Bangle.setUI=function(mode, cb) {
|
||||
if (Bangle.uiRemove) {
|
||||
Bangle.uiRemove();
|
||||
delete Bangle.uiRemove;
|
||||
}
|
||||
if ("object"==typeof mode) {
|
||||
// TODO: handle mode.back?
|
||||
mode = mode.mode;
|
||||
}
|
||||
Bangle._setUI(mode, cb);
|
||||
};\n`;
|
||||
}
|
||||
delete E.showScroller; // deleting stops us getting confused by our own decl. builtins can't be deleted
|
||||
|
@ -195,8 +164,8 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
|
|||
|
||||
// Append *.boot.js files
|
||||
// These could change bleServices/bleServiceOptions if needed
|
||||
var getPriority = /.*\.(\d+)\.boot\.js$/;
|
||||
require('Storage').list(/\.boot\.js/).sort((a,b)=>{
|
||||
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
|
||||
var getPriority = /.*\.(\d+)\.boot\.js$/;
|
||||
var aPriority = a.match(getPriority);
|
||||
var bPriority = b.match(getPriority);
|
||||
if (aPriority && bPriority){
|
||||
|
@ -206,18 +175,40 @@ require('Storage').list(/\.boot\.js/).sort((a,b)=>{
|
|||
} else if (!aPriority && bPriority){
|
||||
return 1;
|
||||
}
|
||||
return a > b;
|
||||
}).forEach(bootFile=>{
|
||||
return a==b ? 0 : (a>b ? 1 : -1);
|
||||
});
|
||||
// precalculate file size
|
||||
var fileSize = boot.length + bootPost.length;
|
||||
bootFiles.forEach(bootFile=>{
|
||||
// match the size of data we're adding below in bootFiles.forEach
|
||||
fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2;
|
||||
});
|
||||
// write file in chunks (so as not to use up all RAM)
|
||||
require('Storage').write('.boot0',boot,0,fileSize);
|
||||
var fileOffset = boot.length;
|
||||
bootFiles.forEach(bootFile=>{
|
||||
// we add a semicolon so if the file is wrapped in (function(){ ... }()
|
||||
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
|
||||
// which would cause an error!
|
||||
boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
|
||||
// we write:
|
||||
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
|
||||
// but we need to do this without ever loading everything into RAM as some
|
||||
// boot files seem to be getting pretty big now.
|
||||
require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
|
||||
fileOffset+=2+bootFile.length+1;
|
||||
var bf = require('Storage').read(bootFile);
|
||||
require('Storage').write('.boot0',bf,fileOffset);
|
||||
fileOffset+=bf.length;
|
||||
require('Storage').write('.boot0',";\n",fileOffset);
|
||||
fileOffset+=2;
|
||||
});
|
||||
// update ble
|
||||
boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`;
|
||||
// write file
|
||||
require('Storage').write('.boot0',boot);
|
||||
require('Storage').write('.boot0',bootPost,fileOffset);
|
||||
|
||||
delete boot;
|
||||
delete bootPost;
|
||||
delete bootFiles;
|
||||
delete fileSize;
|
||||
delete fileOffset;
|
||||
E.showMessage("Reloading...");
|
||||
eval(require('Storage').read('.boot0'));
|
||||
// .bootcde should be run automatically after if required, since
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boot",
|
||||
"name": "Bootloader",
|
||||
"version": "0.42",
|
||||
"version": "0.47",
|
||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||
"icon": "bootloader.png",
|
||||
"type": "bootloader",
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Bordle
|
||||
|
||||
The Bangle version of a popular word guessing game. The goal is to guess a 5 letter word in 6 tries or less. After each guess, the letters in the guess are
|
||||
marked in colors: yellow for a letter that appears in the to-be-guessed word, but in a different location and green for a letter in the correct position.
|
||||
|
||||
Only words contained in the internal dictionary are allowed as valid guesses. At app launch, a target word is picked from the dictionary at random.
|
||||
|
||||
On startup, a grid of 6 lines with 5 (empty) letter boxes is displayed. Swiping left or right at any time switches between grid view and keyboard view.
|
||||
The keyboad was inspired by the 'Scribble' app (it is a simplified version using the layout library). The letter group "Z ..." contains the delete key and
|
||||
the enter key. Hitting enter after the 5th letter will add the guess to the grid view and color mark it.
|
||||
|
||||
The (English language) dictionary was derived from the the Unix ispell word list by filtering out plurals and past particples (and some hand editing) from all 5 letter words.
|
||||
It is contained in the file 'wordlencr.txt' which contains one long string (no newline characters) of all the words concatenated. It would not be too difficult to swap it
|
||||
out for a different language version. The keyboard currently only supports the 26 characters of the latin alphabet (no accents or umlauts).
|
||||
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AA/TADwoIFkYyOF0owIF04wGUSqvVBZQtZGJYJIFzomKF0onIF07EKF0owLF9wNEnwACE6oZILxovbMBov/F/4v/C54uWF/4vKBQQLLF/4YPFwYMLF7AZGF5Y5KF5xJIFwoMJD44vaBhwvcLQpgHF8gGRF6xYNBpQvTXBoNOF65QJBIgvjBywvUV5YOOF64OIB54v/cQwAKB5ov/F84wKADYuIF+AwkFIwwnE45hmExCSlEpTEiERr3KADw+PF0ownUSoseA=="))
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,159 @@
|
|||
var Layout = require("Layout");
|
||||
|
||||
var gameState = 0;
|
||||
var keyState = 0;
|
||||
var keyStateIdx = 0;
|
||||
|
||||
function buttonPushed(b) {
|
||||
if (keyState==0) {
|
||||
keyState++;
|
||||
keyStateIdx = b;
|
||||
if (b<6) {
|
||||
for (i=1; i<=5; ++i) {
|
||||
var c = String.fromCharCode(i+64+(b-1)*5);
|
||||
layout["bt"+i.toString()].label = c;
|
||||
layout["bt"+i.toString()].bgCol = wordle.keyColors[c]||g.theme.bg;
|
||||
}
|
||||
layout.bt6.label = "<";
|
||||
}
|
||||
else {
|
||||
layout.bt1.label = "Z";
|
||||
layout.bt1.bgCol = wordle.keyColors.Z||g.theme.bg;
|
||||
layout.bt2.label = "<del>";
|
||||
layout.bt4.label = "<ent>";
|
||||
layout.bt3.label = layout.bt5.label = " ";
|
||||
layout.bt6.label = "<";
|
||||
}
|
||||
}
|
||||
else { // actual button pushed
|
||||
inp = layout.input.label;
|
||||
if (b!=6) {
|
||||
if ((keyStateIdx<=5 || b<=1) && inp.length<5) inp += String.fromCharCode(b+(keyStateIdx-1)*5+64);
|
||||
else if (layout.input.label.length>0 && b==2) inp = inp.slice(0,-1);
|
||||
layout.input.label = inp;
|
||||
}
|
||||
layout = getKeyLayout(inp);
|
||||
keyState = 0;
|
||||
if (inp.length==5 && keyStateIdx==6 && b==4) {
|
||||
rc = wordle.addGuess(inp);
|
||||
layout.input.label = "";
|
||||
layout.update();
|
||||
gameState = 0;
|
||||
if (rc>0) return;
|
||||
g.clear();
|
||||
wordle.render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
layout.update();
|
||||
g.clear();
|
||||
layout.render();
|
||||
}
|
||||
|
||||
function getKeyLayout(text) {
|
||||
return new Layout( {
|
||||
type: "v", c: [
|
||||
{type:"txt", font:"6x8:2", id:"input", label:text, pad: 3},
|
||||
{type: "h", c: [
|
||||
{type:"btn", font:"6x8:2", id:"bt1", label:"ABCDE", cb: l=>buttonPushed(1), pad:4, filly:1, fillx:1 },
|
||||
{type:"btn", font:"6x8:2", id:"bt2", label:"FGHIJ", cb: l=>buttonPushed(2), pad:4, filly:1, fillx:1 },
|
||||
]},
|
||||
{type: "h", c: [
|
||||
{type:"btn", font:"6x8:2", id:"bt3", label:"KLMNO", cb: l=>buttonPushed(3), pad:4, filly:1, fillx:1 },
|
||||
{type:"btn", font:"6x8:2", id:"bt4", label:"PQRST", cb: l=>buttonPushed(4), pad:4, filly:1, fillx:1 },
|
||||
]},
|
||||
{type: "h", c: [
|
||||
{type:"btn", font:"6x8:2", id:"bt5", label:"UVWXY", cb: l=>buttonPushed(5), pad:4, filly:1, fillx:1 },
|
||||
{type:"btn", font:"6x8:2", id:"bt6", label:"Z ...", cb: l=>buttonPushed(6), pad:4, filly:1, fillx:1 },
|
||||
]}
|
||||
]});
|
||||
}
|
||||
|
||||
class Wordle {
|
||||
constructor(word) {
|
||||
this.word = word;
|
||||
this.guesses = [];
|
||||
this.guessColors = [];
|
||||
this.keyColors = [];
|
||||
this.nGuesses = -1;
|
||||
if (word == "rnd") {
|
||||
this.words = require("Storage").read("wordlencr.txt");
|
||||
i = Math.floor(Math.floor(this.words.length/5)*Math.random())*5;
|
||||
this.word = this.words.slice(i, i+5).toUpperCase();
|
||||
}
|
||||
console.log(this.word);
|
||||
}
|
||||
render(clear) {
|
||||
h = g.getHeight();
|
||||
bh = Math.floor(h/6);
|
||||
bbh = Math.floor(0.85*bh);
|
||||
w = g.getWidth();
|
||||
bw = Math.floor(w/5);
|
||||
bbw = Math.floor(0.85*bw);
|
||||
if (clear) g.clear();
|
||||
g.setFont("Vector", Math.floor(bbh*0.95)).setFontAlign(0,0);
|
||||
g.setColor(g.theme.fg);
|
||||
for (i=0; i<6; ++i) {
|
||||
for (j=0; j<5; ++j) {
|
||||
if (i<=this.nGuesses) {
|
||||
g.setColor(this.guessColors[i][j]).fillRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
|
||||
g.setColor(g.theme.fg).drawString(this.guesses[i][j], 2+j*bw+bw/2, 2+i*bh+bh/2);
|
||||
}
|
||||
g.setColor(g.theme.fg).drawRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
addGuess(w) {
|
||||
if ((this.words.indexOf(w.toLowerCase())%5)!=0) {
|
||||
E.showAlert(w+"\nis not a word", "Invalid word").then(function() {
|
||||
layout = getKeyLayout("");
|
||||
wordle.render(true);
|
||||
});
|
||||
return 3;
|
||||
}
|
||||
this.guesses.push(w);
|
||||
this.nGuesses++;
|
||||
this.guessColors.push([]);
|
||||
correct = 0;
|
||||
var sol = this.word;
|
||||
for (i=0; i<w.length; ++i) {
|
||||
c = w[i];
|
||||
col = g.theme.bg;
|
||||
if (sol[i]==c) {
|
||||
sol = sol.substr(0,i) + '?' + sol.substr(i+1);
|
||||
col = "#0f0";
|
||||
++correct;
|
||||
}
|
||||
else if (sol.includes(c)) col = "#ff0";
|
||||
if (col!=g.theme.bg) this.keyColors[c] = this.keyColors[c] || col;
|
||||
else this.keyColors[c] = "#00f";
|
||||
this.guessColors[this.nGuesses].push(col);
|
||||
}
|
||||
if (correct==5) {
|
||||
E.showAlert("The word is\n"+this.word, "You won in "+(this.nGuesses+1)+" guesses!").then(function(){load();});
|
||||
return 1;
|
||||
}
|
||||
if (this.nGuesses==5) {
|
||||
E.showAlert("The word was\n"+this.word, "You lost!").then(function(){load();});
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wordle = new Wordle("rnd");
|
||||
layout = getKeyLayout("");
|
||||
wordle.render(true);
|
||||
|
||||
Bangle.on('swipe', function (dir) {
|
||||
if (dir==1 || dir==-1) {
|
||||
g.clear();
|
||||
if (gameState==0) {
|
||||
layout.render();
|
||||
gameState = 1;
|
||||
}
|
||||
else if (gameState==1) {
|
||||
wordle.render();
|
||||
gameState = 0;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "bordle",
|
||||
"name": "Bordle",
|
||||
"shortName":"Bordle",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Bangle version of a popular word search game",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"tags": "game, text",
|
||||
"storage": [
|
||||
{"name":"bordle.app.js","url":"bordle.app.js"},
|
||||
{"name":"wordlencr.txt","url":"wordlencr.txt"},
|
||||
{"name":"bordle.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -20,3 +20,4 @@
|
|||
0.07: Recorder icon only blue if values actually arive
|
||||
Adds some preset modes and a custom one
|
||||
Restructure the settings menu
|
||||
0.08: Allow scanning for devices in settings
|
||||
|
|
|
@ -23,7 +23,10 @@
|
|||
}
|
||||
|
||||
function getCache(){
|
||||
return require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
|
||||
if (settings.btname && settings.btname == cache.name) return cache;
|
||||
clearCache();
|
||||
return {};
|
||||
}
|
||||
|
||||
function addNotificationHandler(characteristic){
|
||||
|
@ -361,7 +364,13 @@
|
|||
var promise;
|
||||
|
||||
if (!device){
|
||||
promise = NRF.requestDevice({ filters: serviceFilters });
|
||||
var filters = serviceFilters;
|
||||
if (settings.btname){
|
||||
log("Configured device name", settings.btname);
|
||||
filters = [{name: settings.btname}];
|
||||
}
|
||||
log("Requesting device with filters", filters);
|
||||
promise = NRF.requestDevice({ filters: filters });
|
||||
|
||||
if (settings.gracePeriodRequest){
|
||||
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
||||
|
@ -488,11 +497,15 @@
|
|||
if (gatt) {
|
||||
if (gatt.connected){
|
||||
log("Disconnect with gatt: ", gatt);
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect", e);
|
||||
});
|
||||
try{
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect promise", e);
|
||||
});
|
||||
} catch (e){
|
||||
log("Error during disconnect attempt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.07",
|
||||
"version": "0.08",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -17,54 +17,73 @@
|
|||
var settings;
|
||||
readSettings();
|
||||
|
||||
var mainmenu = {
|
||||
'': { 'title': 'Bluetooth HRM' },
|
||||
'< Back': back,
|
||||
'Mode': {
|
||||
value: 0 | settings.mode,
|
||||
min: 0,
|
||||
max: 3,
|
||||
format: v => ["Off", "Default", "Both", "Custom"][v],
|
||||
onchange: v => {
|
||||
settings.mode = v;
|
||||
switch (v){
|
||||
case 0:
|
||||
writeSettings("enabled",false);
|
||||
break;
|
||||
case 1:
|
||||
writeSettings("enabled",true);
|
||||
writeSettings("replace",true);
|
||||
writeSettings("debuglog",false);
|
||||
writeSettings("startWithHrm",true);
|
||||
writeSettings("allowFallback",true);
|
||||
writeSettings("fallbackTimeout",10);
|
||||
break;
|
||||
case 2:
|
||||
writeSettings("enabled",true);
|
||||
writeSettings("replace",false);
|
||||
writeSettings("debuglog",false);
|
||||
writeSettings("startWithHrm",false);
|
||||
writeSettings("allowFallback",false);
|
||||
break;
|
||||
case 3:
|
||||
writeSettings("enabled",true);
|
||||
writeSettings("replace",settings.custom_replace);
|
||||
writeSettings("debuglog",settings.custom_debuglog);
|
||||
writeSettings("startWithHrm",settings.custom_startWithHrm);
|
||||
writeSettings("allowFallback",settings.custom_allowFallback);
|
||||
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
|
||||
break;
|
||||
function buildMainMenu(){
|
||||
var mainmenu = {
|
||||
'': { 'title': 'Bluetooth HRM' },
|
||||
'< Back': back,
|
||||
'Mode': {
|
||||
value: 0 | settings.mode,
|
||||
min: 0,
|
||||
max: 3,
|
||||
format: v => ["Off", "Default", "Both", "Custom"][v],
|
||||
onchange: v => {
|
||||
settings.mode = v;
|
||||
switch (v){
|
||||
case 0:
|
||||
writeSettings("enabled",false);
|
||||
break;
|
||||
case 1:
|
||||
writeSettings("enabled",true);
|
||||
writeSettings("replace",true);
|
||||
writeSettings("debuglog",false);
|
||||
writeSettings("startWithHrm",true);
|
||||
writeSettings("allowFallback",true);
|
||||
writeSettings("fallbackTimeout",10);
|
||||
break;
|
||||
case 2:
|
||||
writeSettings("enabled",true);
|
||||
writeSettings("replace",false);
|
||||
writeSettings("debuglog",false);
|
||||
writeSettings("startWithHrm",false);
|
||||
writeSettings("allowFallback",false);
|
||||
break;
|
||||
case 3:
|
||||
writeSettings("enabled",true);
|
||||
writeSettings("replace",settings.custom_replace);
|
||||
writeSettings("debuglog",settings.custom_debuglog);
|
||||
writeSettings("startWithHrm",settings.custom_startWithHrm);
|
||||
writeSettings("allowFallback",settings.custom_allowFallback);
|
||||
writeSettings("fallbackTimeout",settings.custom_fallbackTimeout);
|
||||
break;
|
||||
}
|
||||
writeSettings("mode",v);
|
||||
}
|
||||
writeSettings("mode",v);
|
||||
}
|
||||
},
|
||||
'Custom Mode': function() { E.showMenu(submenu_custom); },
|
||||
'Debug': function() { E.showMenu(submenu_debug); }
|
||||
};
|
||||
};
|
||||
|
||||
if (settings.btname){
|
||||
var name = "Clear " + settings.btname;
|
||||
mainmenu[name] = function() {
|
||||
E.showPrompt("Clear current device name?").then((r)=>{
|
||||
if (r) {
|
||||
writeSettings("btname",undefined);
|
||||
}
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
mainmenu["BLE Scan"] = ()=> createMenuFromScan();
|
||||
mainmenu["Custom Mode"] = function() { E.showMenu(submenu_custom); };
|
||||
mainmenu.Debug = function() { E.showMenu(submenu_debug); };
|
||||
return mainmenu;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var submenu_debug = {
|
||||
'' : { title: "Debug"},
|
||||
'< Back': function() { E.showMenu(mainmenu); },
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||
'Alert on disconnect': {
|
||||
value: !!settings.warnDisconnect,
|
||||
format: v => settings.warnDisconnect ? "On" : "Off",
|
||||
|
@ -81,10 +100,41 @@
|
|||
},
|
||||
'Grace periods': function() { E.showMenu(submenu_grace); }
|
||||
};
|
||||
|
||||
function createMenuFromScan(){
|
||||
E.showMenu();
|
||||
E.showMessage("Scanning");
|
||||
|
||||
var submenu_scan = {
|
||||
'' : { title: "Scan"},
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); }
|
||||
};
|
||||
var packets=10;
|
||||
var scanStart=Date.now();
|
||||
NRF.setScan(function(d) {
|
||||
packets--;
|
||||
if (packets<=0 || Date.now() - scanStart > 5000){
|
||||
NRF.setScan();
|
||||
E.showMenu(submenu_scan);
|
||||
} else if (d.name){
|
||||
print("Found device", d);
|
||||
submenu_scan[d.name] = function(){
|
||||
E.showPrompt("Set "+d.name+"?").then((r)=>{
|
||||
if (r) {
|
||||
writeSettings("btname",d.name);
|
||||
}
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
||||
};
|
||||
}
|
||||
}, { filters: [{services: [ "180d" ]}]});
|
||||
}
|
||||
|
||||
|
||||
|
||||
var submenu_custom = {
|
||||
'' : { title: "Custom mode"},
|
||||
'< Back': function() { E.showMenu(mainmenu); },
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||
'Replace HRM': {
|
||||
value: !!settings.custom_replace,
|
||||
format: v => settings.custom_replace ? "On" : "Off",
|
||||
|
@ -165,7 +215,7 @@
|
|||
|
||||
var submenu = {
|
||||
'' : { title: "Grace periods"},
|
||||
'< Back': function() { E.showMenu(mainmenu); },
|
||||
'< Back': function() { E.showMenu(buildMainMenu()); },
|
||||
'Request': {
|
||||
value: settings.gracePeriodRequest,
|
||||
min: 0,
|
||||
|
@ -208,5 +258,5 @@
|
|||
}
|
||||
};
|
||||
|
||||
E.showMenu(mainmenu);
|
||||
E.showMenu(buildMainMenu());
|
||||
})
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Write available data on reset or kill
|
||||
0.03: Buzz short on every finished measurement and longer if all are done
|
||||
|
|