Merge branch 'espruino:master' into master

master
eleanor 2022-05-16 10:51:29 -05:00 committed by GitHub
commit 74bec38e18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
751 changed files with 96062 additions and 2693 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ _config.yml
tests/Layout/bin/tmp.* tests/Layout/bin/tmp.*
tests/Layout/testresult.bmp tests/Layout/testresult.bmp
apps.local.json apps.local.json
_site
.jekyll-cache

View File

@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer.
}, },
``` ```
### Screenshots
In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { "url":"screenshot.png" } ],`
To get a screenshot you can:
* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then
right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is
unable to read data back from the LCD controller).
* Run your code in the emulator and use the screenshot button in the bottom right of the window.
## Testing ## Testing
### Online ### Online
@ -214,10 +226,8 @@ and which gives information about the app for the Launcher.
"name":"Short Name", // for Bangle.js menu "name":"Short Name", // for Bangle.js menu
"icon":"*myappid", // for Bangle.js menu "icon":"*myappid", // for Bangle.js menu
"src":"-myappid", // source file "src":"-myappid", // source file
"type":"widget/clock/app/bootloader", // optional, default "app" "type":"widget/clock/app/bootloader/...", // optional, default "app"
// if this is 'widget' then it's not displayed in the menu // see 'type' in 'metadata.json format' below for more options/info
// if it's 'clock' then it'll be loaded by default at boot time
// if this is 'bootloader' then it's code that is run at boot time, but is not in a menu
"version":"1.23", "version":"1.23",
// added by BangleApps loader on upload based on metadata.json // added by BangleApps loader on upload based on metadata.json
"files:"file1,file2,file3", "files:"file1,file2,file3",
@ -240,16 +250,23 @@ and which gives information about the app for the Launcher.
"version": "0v01", // the version of this app "version": "0v01", // the version of this app
"description": "...", // long description (can contain markdown) "description": "...", // long description (can contain markdown)
"icon": "icon.png", // icon in apps/ "icon": "icon.png", // icon in apps/
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app "screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) - "type":"...", // optional(if app) -
// 'app' - an application // 'app' - an application
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget // 'widget' - a widget
// 'launch' - replacement launcher app // 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
// 'bootloader' - code that runs at startup only
// 'RAM' - code that runs and doesn't upload anything to storage // 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle
// 'scheduler' - provides 'sched' library and boot code for scheduling alarms/timers
// (currently only 'sched' app)
// 'notify' - provides 'notify' library for showing notifications
// 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching "tags": "", // comma separated tag list for searching
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID "dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify' // for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file

View File

@ -1 +1 @@
theme: jekyll-theme-minimal theme: jekyll-theme-slate

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

@ -0,0 +1,140 @@
class TwoK {
constructor() {
this.b = Array(4).fill().map(() => Array(4).fill(0));
this.score = 0;
this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
}
drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
g.setColor(c);
for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
}
render() {
const yo = 20;
const xo = yo/2;
h = g.getHeight()-yo;
w = g.getWidth()-yo;
bh = Math.floor(h/4);
bw = Math.floor(w/4);
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
for (y=0; y<4; ++y)
for (x=0; x<4; ++x) {
b = this.b[y][x];
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
if (b > 4) g.setColor(1, 1, 1);
else g.setColor(0, 0, 0);
g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
}
}
shift(d) { // +/-1: shift x, +/- 2: shift y
var crc = E.CRC32(this.b.toString());
if (d==-1) { // shift x left
for (y=0; y<4; ++y) {
for (x=2; x>=0; x--)
if (this.b[y][x]==0) {
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
this.b[y][3] = 0;
}
for (x=0; x<3; ++x)
if (this.b[y][x]==this.b[y][x+1]) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y][x+1];
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
this.b[y][3] = 0;
}
}
}
else if (d==1) { // shift x right
for (y=0; y<4; ++y) {
for (x=1; x<4; x++)
if (this.b[y][x]==0) {
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
this.b[y][0] = 0;
}
for (x=3; x>0; --x)
if (this.b[y][x]==this.b[y][x-1]) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y][x-1] ;
for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
this.b[y][0] = 0;
}
}
}
else if (d==-2) { // shift y down
for (x=0; x<4; ++x) {
for (y=1; y<4; y++)
if (this.b[y][x]==0) {
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
this.b[0][x] = 0;
}
for (y=3; y>0; y--)
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y-1][x];
for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
this.b[0][x] = 0;
}
}
}
else if (d==2) { // shift y up
for (x=0; x<4; ++x) {
for (y=2; y>=0; y--)
if (this.b[y][x]==0) {
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
this.b[3][x] = 0;
}
for (y=0; y<3; ++y)
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y+1][x];
for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
this.b[3][x] = 0;
}
}
}
return (E.CRC32(this.b.toString())!=crc);
}
addDigit() {
var d = Math.random()>0.9 ? 4 : 2;
var id = Math.floor(Math.random()*16);
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
this.b[Math.floor(id/4)][id%4] = d;
}
}
function dragHandler(e) {
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
var res = false;
if (Math.abs(e.dx)>Math.abs(e.dy)) {
if (e.dx>0) res = twok.shift(1);
if (e.dx<0) res = twok.shift(-1);
}
else {
if (e.dy>0) res = twok.shift(-2);
if (e.dy<0) res = twok.shift(2);
}
if (res) twok.addDigit();
twok.render();
}
}
function swipeHandler() {
}
function buttonHandler() {
}
var twok = new TwoK();
twok.addDigit(); twok.addDigit();
twok.render();
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
if (process.env.HWVERSION==1) {
Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

2
apps/2047pp/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Better support for watch themes

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

@ -0,0 +1,9 @@
# Game of 2047pp (2047++)
Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024.
Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using
the top/bottom button (Bangle 1).
![Screenshot](./2047pp_screenshot.png)

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

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

BIN
apps/2047pp/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

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

@ -0,0 +1,15 @@
{ "id": "2047pp",
"name": "2047pp",
"shortName":"2047pp",
"icon": "app.png",
"version":"0.02",
"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}
]
}

2
apps/90sclk/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Fullscreen settings.

13
apps/90sclk/README.md Normal file
View File

@ -0,0 +1,13 @@
# 90s Clock
A watch face in 90s style:
![](screenshot_2.png)
Fullscreen mode can be enabled in the settings:
![](screenshot.png)
## Creator
- [David Peer](https://github.com/peerdavid)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgc8+fAgEgwAMDvPnz99BYdl2weHtu27ft2AGBiEcuEAhAPDg4jGgECIRMN23fthUNgP374vBAB3gAgc/gAXNjlx4EDxwJEpAjG/6IBjkBL4UAjVgBAJuCgPHBQMFEIkkyQjFhwEClgXBEYNBwkQJoibCBwNFBAUCEAVAQZAjC/8euPHDon//hKB//xEYMP//jBYP/+ARDNYM///+EYIgBj1B/8fCIUhEYQRB//FUIM/EZU4EYMkEYP/8VhEYUH/gRBWAUfI4MD+AjBoAsBwEH8EB/EDwE4HwYjCuEHWAOHgExEYKbBCIZNB8fAEYQHByE/EwPABAY+BgRHDBANyJQXHNwIjD8CSBj/+BwMSTwOOBYK2D/4CCNYZQB/iJBQwYjCCIcAgeBSoOAWYQjEVoIRCNAIjKAQKJBgAFC8ZoCWwJbDABMHGQPAAoMQB5EDx/4A4gqBZwIGCWwIABuBWC4EBZwPgv/AcwS/EAAcIU4IRBVQIRKEwIjBv0ARIUDCJIjD//x/ARK/5HC/+BCJkcI45uDgECUgQjCWAM4WwUBWYanEAA8cTARWBEYUC5RAHw1YgEOFQXADQPHIIkAhgICuARBh0A23blhHBagIKBsOGjNswhHDEYUUAoTUBhkxEYMwKwU503bvuwXILmCEYMYsumWYYjB85lDEYovBEYXm7fs25EBI4kYtOWNwIjD4+8NYsw4YjGz9/2hrEoOGjVBwE4NYdzNYSwBuEDEYcxaIUA8+atugGogjBiVgWAI"))

144
apps/90sclk/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/90sclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
apps/90sclk/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

18
apps/90sclk/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"id": "90sclk",
"name": "90s Clock",
"version": "0.02",
"description": "A 90s style watch-face",
"readme": "README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_2.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"90sclk.app.js","url":"app.js"},
{"name":"90sclk.img","url":"app-icon.js","evaluate":true},
{"name":"90sclk.settings.js","url":"settings.js"}
]
}

BIN
apps/90sclk/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

31
apps/90sclk/settings.js Normal file
View File

@ -0,0 +1,31 @@
(function(back) {
const SETTINGS_FILE = "90sclk.setting.json";
// initialize with default settings...
const storage = require('Storage')
let settings = {
fullscreen: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
function save() {
storage.write(SETTINGS_FILE, settings)
}
E.showMenu({
'': { 'title': '90s Clock' },
'< Back': back,
'Full Screen': {
value: settings.fullscreen,
format: () => (settings.fullscreen ? 'Yes' : 'No'),
onchange: () => {
settings.fullscreen = !settings.fullscreen;
save();
},
}
});
})

View File

@ -1,12 +1,34 @@
// place your const, vars, functions or classes here // place your const, vars, functions or classes here
// special function to handle display switch on // clear the screen
Bangle.on('lcdPower', (on) => { g.clear();
if (on) {
// call your app function here var n = 0;
// If you clear the screen, do Bangle.drawWidgets();
// redraw the screen
function draw() {
g.reset().clearRect(Bangle.appRect);
g.setFont("6x8").setFontAlign(0,0).drawString("Up / Down",g.getWidth()/2,g.getHeight()/2 - 20);
g.setFont("Vector",60).setFontAlign(0,0).drawString(n,g.getWidth()/2,g.getHeight()/2 + 30);
}
// Respond to user input
Bangle.setUI({mode: "updown"}, function(dir) {
if (dir<0) {
n--;
draw();
} else if (dir>0) {
n++;
draw();
} else {
n = 0;
draw();
} }
}); });
g.clear(); // First draw...
// call your app function here draw();
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Use the new multiplatform 'Layout' library 0.02: Use the new multiplatform 'Layout' library
0.03: Exit as first menu option, dont show decimal places for seconds 0.03: Exit as first menu option, dont show decimal places for seconds
0.04: Localisation, change Exit->Back to allow back-arrow to appear on 2v13 firmware

View File

@ -7,21 +7,21 @@ function getFileName(n) {
function showMenu() { function showMenu() {
var menu = { var menu = {
"" : { title : "Accel Logger" }, "" : { title : /*LANG*/"Accel Logger" },
"Exit" : function() { "< Back" : function() {
load(); load();
}, },
"File No" : { /*LANG*/"File No" : {
value : fileNumber, value : fileNumber,
min : 0, min : 0,
max : MAXLOGS, max : MAXLOGS,
onchange : v => { fileNumber=v; } onchange : v => { fileNumber=v; }
}, },
"Start" : function() { /*LANG*/"Start" : function() {
E.showMenu(); E.showMenu();
startRecord(); startRecord();
}, },
"View Logs" : function() { /*LANG*/"View Logs" : function() {
viewLogs(); viewLogs();
}, },
}; };
@ -29,7 +29,7 @@ function showMenu() {
} }
function viewLog(n) { function viewLog(n) {
E.showMessage("Loading..."); E.showMessage(/*LANG*/"Loading...");
var f = require("Storage").open(getFileName(n), "r"); var f = require("Storage").open(getFileName(n), "r");
var records = 0, l = "", ll=""; var records = 0, l = "", ll="";
while ((l=f.readLine())!==undefined) {records++;ll=l;} while ((l=f.readLine())!==undefined) {records++;ll=l;}
@ -37,29 +37,29 @@ function viewLog(n) {
if (ll) length = Math.round( (ll.split(",")[0]|0)/1000 ); if (ll) length = Math.round( (ll.split(",")[0]|0)/1000 );
var menu = { var menu = {
"" : { title : "Log "+n } "" : { title : "Log "+n },
"< Back" : () => { viewLogs(); }
}; };
menu[records+" Records"] = ""; menu[records+/*LANG*/" Records"] = "";
menu[length+" Seconds"] = ""; menu[length+/*LANG*/" Seconds"] = "";
menu["DELETE"] = function() { menu[/*LANG*/"DELETE"] = function() {
E.showPrompt("Delete Log "+n).then(ok=>{ E.showPrompt(/*LANG*/"Delete Log "+n).then(ok=>{
if (ok) { if (ok) {
E.showMessage("Erasing..."); E.showMessage(/*LANG*/"Erasing...");
f.erase(); f.erase();
viewLogs(); viewLogs();
} else viewLog(n); } else viewLog(n);
}); });
}; };
menu["< Back"] = function() {
viewLogs();
};
E.showMenu(menu); E.showMenu(menu);
} }
function viewLogs() { function viewLogs() {
var menu = { var menu = {
"" : { title : "Logs" } "" : { title : /*LANG*/"Logs" },
"< Back" : () => { showMenu(); }
}; };
var hadLogs = false; var hadLogs = false;
@ -67,14 +67,13 @@ function viewLogs() {
var f = require("Storage").open(getFileName(i), "r"); var f = require("Storage").open(getFileName(i), "r");
if (f.readLine()!==undefined) { if (f.readLine()!==undefined) {
(function(i) { (function(i) {
menu["Log "+i] = () => viewLog(i); menu[/*LANG*/"Log "+i] = () => viewLog(i);
})(i); })(i);
hadLogs = true; hadLogs = true;
} }
} }
if (!hadLogs) if (!hadLogs)
menu["No Logs Found"] = function(){}; menu[/*LANG*/"No Logs Found"] = function(){};
menu["< Back"] = function() { showMenu(); };
E.showMenu(menu); E.showMenu(menu);
} }
@ -83,7 +82,7 @@ function startRecord(force) {
// check for existing file // check for existing file
var f = require("Storage").open(getFileName(fileNumber), "r"); var f = require("Storage").open(getFileName(fileNumber), "r");
if (f.readLine()!==undefined) if (f.readLine()!==undefined)
return E.showPrompt("Overwrite Log "+fileNumber+"?").then(ok=>{ return E.showPrompt(/*LANG*/"Overwrite Log "+fileNumber+"?").then(ok=>{
if (ok) startRecord(true); else showMenu(); if (ok) startRecord(true); else showMenu();
}); });
} }
@ -93,14 +92,14 @@ function startRecord(force) {
var Layout = require("Layout"); var Layout = require("Layout");
var layout = new Layout({ type: "v", c: [ var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:"Samples", pad:2}, {type:"txt", font:"6x8", label:/*LANG*/"Samples", pad:2},
{type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, {type:"txt", id:"samples", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8", label:"Time", pad:2}, {type:"txt", font:"6x8", label:/*LANG*/"Time", pad:2},
{type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg}, {type:"txt", id:"time", font:"6x8:2", label:" - ", pad:5, bgCol:g.theme.bg},
{type:"txt", font:"6x8:2", label:"RECORDING", bgCol:"#f00", pad:5, fillx:1}, {type:"txt", font:"6x8:2", label:/*LANG*/"RECORDING", bgCol:"#f00", pad:5, fillx:1},
] ]
},{btns:[ // Buttons... },{btns:[ // Buttons...
{label:"STOP", cb:()=>{ {label:/*LANG*/"STOP", cb:()=>{
Bangle.removeListener('accel', accelHandler); Bangle.removeListener('accel', accelHandler);
showMenu(); showMenu();
}} }}

View File

@ -2,7 +2,7 @@
"id": "accellog", "id": "accellog",
"name": "Acceleration Logger", "name": "Acceleration Logger",
"shortName": "Accel Log", "shortName": "Accel Log",
"version": "0.03", "version": "0.04",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC", "description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png", "icon": "app.png",
"tags": "outdoor", "tags": "outdoor",

View File

@ -8,6 +8,7 @@
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true, "allow_emulator": true,
"storage": [ "storage": [
{"name":"aclock.app.js","url":"clock-analog.js"}, {"name":"aclock.app.js","url":"clock-analog.js"},

View File

@ -0,0 +1,5 @@
0.01: New App!
0.02: Fix the settings bug and some tweaking
0.03: Do not alarm while charging
0.04: Obey system quiet mode
0.05: Battery optimisation, add the pause option, bug fixes

View File

@ -0,0 +1,14 @@
# Activity reminder
A reminder to take short walks for the ones with a sedentary lifestyle.
The alert will popup only if you didn't take your short walk yet.
Different settings can be personalized:
- Enable : Enable/Disable the app
- Start hour: Hour to start the reminder
- End hour: Hour to end the reminder
- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 120 min
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min
- Pause delay: Same as Dismiss delay but longer (usefull for meetings and such). From 30 to 240 min
- Min steps: Minimal amount of steps to count as an activity

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwYda7dtwAQNmwRB2wQMgO2CIXACJcNCIfYCJYOCCgQRNJQYRM2ADBgwR/CKprRWAKPQWZ0DCIjXLjYREGpYODAQVgCBB3Btj+EAoQAGO4IdCgImDCAwLCAoo4IF4J3DCIPDCIQ4FO4VtwARCAoIRGRgQCBa4IRCKAQRERgOwIIIRDAoOACIoIBwwRHLIqMCFgIRCGQQRIWAYRLYQoREWwTmHO4IRCFgLXHPoi/CbogAFEAIRCWwTpKEwZBCHwK5BCJZEBCJZcCGQTLDCJK/BAQIRKMoaSDOIYAFeQYRMcYRWBXIUAWYPACIq8DagfACJQLCCIYsBU4QRF7B9CAogRGI4QLCAoprIMoZKER5C/DAoShMAo4AGfAQFIACQ="))

View File

@ -0,0 +1,42 @@
function drawAlert() {
E.showPrompt("Inactivity detected", {
title: "Activity reminder",
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
}).then(function (v) {
if (v == 1) {
activityreminder_data.okDate = new Date();
}
if (v == 2) {
activityreminder_data.dismissDate = new Date();
}
if (v == 3) {
activityreminder_data.pauseDate = new Date();
}
activityreminder.saveData(activityreminder_data);
load();
});
// Obey system quiet mode:
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
Bangle.buzz(400);
}
setTimeout(load, 20000);
}
function run() {
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
drawAlert();
} else {
eval(storage.read("activityreminder.settings.js"))(() => load());
}
}
const activityreminder = require("activityreminder");
const storage = require("Storage");
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
const activityreminder_settings = activityreminder.loadSettings();
const activityreminder_data = activityreminder.loadData();
run();

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,45 @@
function run() {
if (isNotWorn()) return;
let now = new Date();
let h = now.getHours();
let health = Bangle.getHealthStatus("day");
if (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour) {
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
activityreminder_data.stepsOnDate = health.steps;
activityreminder_data.stepsDate = now;
activityreminder.saveData(activityreminder_data);
/* todo in a futur release
add settimer to trigger like 10 secs after the stepsDate + minSteps
cancel all other timers of this app
*/
}
if(activityreminder.mustAlert(activityreminder_data, activityreminder_settings)){
load('activityreminder.app.js');
}
}
}
function isNotWorn() {
// todo in a futur release check temperature and mouvement in a futur release
return Bangle.isCharging();
}
const activityreminder = require("activityreminder");
const activityreminder_settings = activityreminder.loadSettings();
if (activityreminder_settings.enabled) {
const activityreminder_data = activityreminder.loadData();
if(activityreminder_data.firstLoad){
activityreminder_data.firstLoad =false;
activityreminder.saveData(activityreminder_data);
}
setInterval(run, 60000);
/* todo in a futur release
increase setInterval time to something that is still sensible (5 mins ?)
add settimer to trigger like 10 secs after the stepsDate + minSteps
cancel all other timers of this app
*/
}

View File

@ -0,0 +1,57 @@
const storage = require("Storage");
exports.loadSettings = function () {
return Object.assign({
enabled: true,
startHour: 9,
endHour: 20,
maxInnactivityMin: 30,
dismissDelayMin: 15,
pauseDelayMin: 120,
minSteps: 50
}, storage.readJSON("activityreminder.s.json", true) || {});
};
exports.writeSettings = function (settings) {
storage.writeJSON("activityreminder.s.json", settings);
};
exports.saveData = function (data) {
storage.writeJSON("activityreminder.data.json", data);
};
exports.loadData = function () {
let health = Bangle.getHealthStatus("day");
const data = Object.assign({
firstLoad: true,
stepsDate: new Date(),
stepsOnDate: health.steps,
okDate: new Date(1970),
dismissDate: new Date(1970),
pauseDate: new Date(1970),
},
storage.readJSON("activityreminder.data.json") || {});
if(typeof(data.stepsDate) == "string")
data.stepsDate = new Date(data.stepsDate);
if(typeof(data.okDate) == "string")
data.okDate = new Date(data.okDate);
if(typeof(data.dismissDate) == "string")
data.dismissDate = new Date(data.dismissDate);
if(typeof(data.pauseDate) == "string")
data.pauseDate = new Date(data.pauseDate);
return data;
};
exports.mustAlert = function(activityreminder_data, activityreminder_settings) {
let now = new Date();
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
return true;
}
}
return false;
}

View File

@ -0,0 +1,23 @@
{
"id": "activityreminder",
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.05",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name": "activityreminder.app.js", "url":"app.js"},
{"name": "activityreminder.boot.js", "url": "boot.js"},
{"name": "activityreminder.settings.js", "url": "settings.js"},
{"name": "activityreminder", "url": "lib.js"},
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
],
"data": [
{"name": "activityreminder.s.json"},
{"name": "activityreminder.data.json"}
]
}

View File

@ -0,0 +1,76 @@
(function (back) {
// Load settings
const activityreminder = require("activityreminder");
const settings = activityreminder.loadSettings();
// Show the menu
E.showMenu({
"": { "title": "Activity Reminder" },
"< Back": () => back(),
'Enable': {
value: settings.enabled,
format: v => v ? "Yes" : "No",
onchange: v => {
settings.enabled = v;
activityreminder.writeSettings(settings);
}
},
'Start hour': {
value: settings.startHour,
min: 0, max: 24,
onchange: v => {
settings.startHour = v;
activityreminder.writeSettings(settings);
}
},
'End hour': {
value: settings.endHour,
min: 0, max: 24,
onchange: v => {
settings.endHour = v;
activityreminder.writeSettings(settings);
}
},
'Max inactivity': {
value: settings.maxInnactivityMin,
min: 15, max: 120,
onchange: v => {
settings.maxInnactivityMin = v;
activityreminder.writeSettings(settings);
},
format: x => {
return x + " min";
}
},
'Dismiss delay': {
value: settings.dismissDelayMin,
min: 5, max: 60,
onchange: v => {
settings.dismissDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => {
return x + " min";
}
},
'Pause delay': {
value: settings.pauseDelayMin,
min: 30, max: 240,
onchange: v => {
settings.pauseDelayMin = v;
activityreminder.writeSettings(settings);
},
format: x => {
return x + " min";
}
},
'Min steps': {
value: settings.minSteps,
min: 10, max: 500,
onchange: v => {
settings.minSteps = v;
activityreminder.writeSettings(settings);
}
}
});
})

View File

@ -14,3 +14,16 @@
0.13: Alarm widget state now updates when setting/resetting an alarm 0.13: Alarm widget state now updates when setting/resetting an alarm
0.14: Order of 'back' menu item 0.14: Order of 'back' menu item
0.15: Fix hour/minute wrapping code for new menu system 0.15: Fix hour/minute wrapping code for new menu system
0.16: Adding alarm library
0.17: Moving alarm internals to 'sched' library
0.18: Cope with >1 identical alarm at once (#1667)
0.19: Ensure rescheduled alarms that already fired have 'last' reset
0.20: Use the new 'sched' factories to initialize new alarms/timers
0.21: Fix time reset after a day of week change (#1676)
0.22: Refactor some methods to scheduling library
0.23: Fix regression with Days of Week (#1735)
0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow
Add "Enable All", "Disable All" and "Remove All" actions
0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu
0.26: Add support for Monday as first day of the week (#1780)
0.27: New UI!

31
apps/alarm/README.md Normal file
View File

@ -0,0 +1,31 @@
# Alarms & Timers
This app allows you to add/modify any alarms and timers.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.
## Menu overview
- `New...`
- `New Alarm` &rarr; Configure a new alarm
- `Repeat` &rarr; Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
- `New Timer` &rarr; Configure a new timer
- `Advanced`
- `Scheduler settings` &rarr; Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
- `Enable All` &rarr; Enable _all_ disabled alarms & timers
- `Disable All` &rarr; Disable _all_ enabled alarms & timers
- `Delete All` &rarr; Delete _all_ alarms & timers
## Creator
- [Gordon Williams](https://github.com/gfwilliams)
## Main Contributors
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
- [storm64](https://github.com/storm64) - Fix redrawing in submenus
## Attributions
All icons used in this app are from [icons8](https://icons8.com).

View File

@ -1,72 +0,0 @@
// Chances are boot0.js got run already and scheduled *another*
// 'load(alarm.js)' - so let's remove it first!
clearInterval();
function formatTime(t) {
var hrs = 0|t;
var mins = Math.round((t-hrs)*60);
return hrs+":"+("0"+mins).substr(-2);
}
function getCurrentHr() {
var time = new Date();
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
}
function showAlarm(alarm) {
var msg = formatTime(alarm.hr);
var buzzCount = 10;
if (alarm.msg)
msg += "\n"+alarm.msg;
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showPrompt(msg,{
title:alarm.timer ? /*LANG*/"TIMER!" : /*LANG*/"ALARM!",
buttons : {/*LANG*/"Sleep":true,/*LANG*/"Ok":false} // default is sleep so it'll come back in 10 mins
}).then(function(sleep) {
buzzCount = 0;
if (sleep) {
if(alarm.ohr===undefined) alarm.ohr = alarm.hr;
alarm.hr += 10/60; // 10 minutes
} else {
alarm.last = (new Date()).getDate();
if (alarm.ohr!==undefined) {
alarm.hr = alarm.ohr;
delete alarm.ohr;
}
if (!alarm.rp) alarm.on = false;
}
require("Storage").write("alarm.json",JSON.stringify(alarms));
load();
});
function buzz() {
if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence
Bangle.buzz(100).then(()=>{
setTimeout(()=>{
Bangle.buzz(100).then(function() {
if (buzzCount--)
setTimeout(buzz, 3000);
else if(alarm.as) { // auto-snooze
buzzCount = 10;
setTimeout(buzz, 600000);
}
});
},100);
});
}
buzz();
}
// Check for alarms
var day = (new Date()).getDate();
var hr = getCurrentHr()+10000; // get current time - 10s in future to ensure we alarm if we've started the app a tad early
var alarms = require("Storage").readJSON("alarm.json",1)||[];
var active = alarms.filter(a=>a.on&&(a.hr<hr)&&(a.last!=day));
if (active.length) {
// if there's an alarm, show it
active = active.sort((a,b)=>a.hr-b.hr);
showAlarm(active[0]);
} else {
// otherwise just go back to default app
setTimeout(load, 100);
}

View File

@ -1,181 +1,349 @@
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
var alarms = require("Storage").readJSON("alarm.json",1)||[]; // 0 = Sunday (default), 1 = Monday
/*alarms = [ const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
{ on : true, const WORKDAYS = 62
hr : 6.5, // hours + minutes/60 const WEEKEND = firstDayOfWeek ? 192 : 65;
msg : "Eat chocolate", const EVERY_DAY = firstDayOfWeek ? 254 : 127;
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
rp : true, // repeat const iconAlarmOn = "\0" + atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA==");
as : false, // auto snooze const iconAlarmOff = "\0" + (g.theme.dark
timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes ? atob("GBjBAP////8AAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg=")
: atob("GBjBAP//AAAAAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg="));
const iconTimerOn = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA=")
: atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA="));
const iconTimerOff = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg=")
: atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg="));
// An array of alarm objects (see sched/README.md)
var alarms = require("sched").getAlarms();
function handleFirstDayOfWeek(dow) {
if (firstDayOfWeek == 1) {
if ((dow & 1) == 1) {
// In the scheduler API Sunday is 1.
// Here the week starts on Monday and Sunday is ON so
// when I read the dow I need to move Sunday to 128...
dow += 127;
} else if ((dow & 128) == 128) {
// ... and then when I write the dow I need to move Sunday back to 1.
dow -= 127;
}
} }
];*/ return dow;
function formatTime(t) {
var hrs = 0|t;
var mins = Math.round((t-hrs)*60);
return hrs+":"+("0"+mins).substr(-2);
} }
function formatMins(t) { // Check the first day of week and update the dow field accordingly.
mins = (0|t)%60; alarms.forEach(alarm => alarm.dow = handleFirstDayOfWeek(alarm.dow));
hrs = 0|(t/60);
return hrs+":"+("0"+mins).substr(-2);
}
function getCurrentHr() {
var time = new Date();
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
}
function showMainMenu() { function showMainMenu() {
const menu = { const menu = {
'': { 'title': 'Alarm/Timer' }, "": { "title": /*LANG*/"Alarms & Timers" },
/*LANG*/'< Back' : ()=>{load();}, "< Back": () => load(),
/*LANG*/'New Alarm': ()=>editAlarm(-1), /*LANG*/"New...": () => showNewMenu()
/*LANG*/'New Timer': ()=>editTimer(-1)
}; };
alarms.forEach((alarm,idx)=>{
if (alarm.timer) { alarms.forEach((e, index) => {
txt = /*LANG*/"TIMER "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatMins(alarm.timer); var label = e.timer
} else { ? require("time_utils").formatDuration(e.timer)
txt = /*LANG*/"ALARM "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatTime(alarm.hr); : require("time_utils").formatTime(e.t) + (e.dow > 0 ? (" " + decodeDOW(e)) : "");
if (alarm.rp) txt += /*LANG*/" (repeat)"; menu[label] = {
} value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
menu[txt] = function() { onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
if (alarm.timer) editTimer(idx);
else editAlarm(idx);
}; };
}); });
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload(); menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
return E.showMenu(menu);
E.showMenu(menu);
} }
function editAlarm(alarmIndex) { function showNewMenu() {
var newAlarm = alarmIndex<0; E.showMenu({
var hrs = 12; "": { "title": /*LANG*/"New..." },
var mins = 0; "< Back": () => showMainMenu(),
var en = true; /*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
var repeat = true; /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined)
var as = false; });
if (!newAlarm) {
var a = alarms[alarmIndex];
hrs = 0|a.hr;
mins = Math.round((a.hr-hrs)*60);
en = a.on;
repeat = a.rp;
as = a.as;
}
const menu = {
'': { 'title': /*LANG*/'Alarm' },
/*LANG*/'< Back' : showMainMenu,
/*LANG*/'Hours': {
value: hrs, min : 0, max : 23, wrap : true,
onchange: v => hrs=v
},
/*LANG*/'Minutes': {
value: mins, min : 0, max : 59, wrap : true,
onchange: v => mins=v
},
/*LANG*/'Enabled': {
value: en,
format: v=>v?"On":"Off",
onchange: v=>en=v
},
/*LANG*/'Repeat': {
value: en,
format: v=>v?"Yes":"No",
onchange: v=>repeat=v
},
/*LANG*/'Auto snooze': {
value: as,
format: v=>v?"Yes":"No",
onchange: v=>as=v
}
};
function getAlarm() {
var hr = hrs+(mins/60);
var day = 0;
// If alarm is for tomorrow not today (eg, in the past), set day
if (hr < getCurrentHr())
day = (new Date()).getDate();
// Save alarm
return {
on : en, hr : hr,
last : day, rp : repeat, as: as
};
}
menu[/*LANG*/"> Save"] = function() {
if (newAlarm) alarms.push(getAlarm());
else alarms[alarmIndex] = getAlarm();
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
if (!newAlarm) {
menu[/*LANG*/"> Delete"] = function() {
alarms.splice(alarmIndex,1);
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
}
return E.showMenu(menu);
} }
function editTimer(alarmIndex) { function showEditAlarmMenu(selectedAlarm, alarmIndex) {
var newAlarm = alarmIndex<0; var isNew = alarmIndex === undefined;
var hrs = 0;
var mins = 5; var alarm = require("sched").newDefaultAlarm();
var en = true; alarm.dow = handleFirstDayOfWeek(alarm.dow);
if (!newAlarm) {
var a = alarms[alarmIndex]; if (selectedAlarm) {
mins = (0|a.timer)%60; Object.assign(alarm, selectedAlarm);
hrs = 0|(a.timer/60);
en = a.on;
} }
var time = require("time_utils").decodeTime(alarm.t);
const menu = { const menu = {
'': { 'title': /*LANG*/'Timer' }, "": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
/*LANG*/'Hours': { "< Back": () => {
value: hrs, min : 0, max : 23, wrap : true, saveAlarm(alarm, alarmIndex, time);
onchange: v => hrs=v showMainMenu();
}, },
/*LANG*/'Minutes': { /*LANG*/"Hour": {
value: mins, min : 0, max : 59, wrap : true, value: time.h,
onchange: v => mins=v format: v => ("0" + v).substr(-2),
min: 0,
max: 23,
wrap: true,
onchange: v => time.h = v
}, },
/*LANG*/'Enabled': { /*LANG*/"Minute": {
value: en, value: time.m,
format: v=>v?/*LANG*/"On":/*LANG*/"Off", format: v => ("0" + v).substr(-2),
onchange: v=>en=v min: 0,
max: 59,
wrap: true,
onchange: v => time.m = v
},
/*LANG*/"Enabled": {
value: alarm.on,
onchange: v => alarm.on = v
},
/*LANG*/"Repeat": {
value: decodeDOW(alarm),
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.dow, dow => {
alarm.rp = dow > 0;
alarm.dow = dow;
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
})
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
/*LANG*/"Auto Snooze": {
value: alarm.as,
onchange: v => alarm.as = v
},
/*LANG*/"Cancel": () => showMainMenu()
};
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
if (confirm) {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
} else {
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
}
});
};
}
E.showMenu(menu);
}
function saveAlarm(alarm, alarmIndex, time) {
alarm.t = require("time_utils").encodeTime(time);
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
if (alarmIndex === undefined) {
alarms.push(alarm);
} else {
alarms[alarmIndex] = alarm;
}
saveAndReload();
}
function saveAndReload() {
// Before saving revert the dow to the standard format
alarms.forEach(a => a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek));
require("sched").setAlarms(alarms);
require("sched").reload();
// Fix after save
alarms.forEach(a => a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek));
}
function decodeDOW(alarm) {
return alarm.rp
? require("date_utils")
.dows(firstDayOfWeek, 2)
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("")
.toLowerCase()
: "Once"
}
function showEditRepeatMenu(dow, dowChangeCallback) {
var originalDow = dow;
var isCustom = dow > 0 && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY;
const menu = {
"": { "title": /*LANG*/"Repeat Alarm" },
"< Back": () => dowChangeCallback(dow),
/*LANG*/"Once": { // No days set: the alarm will fire once
value: dow == 0,
onchange: () => dowChangeCallback(0)
},
/*LANG*/"Workdays": {
value: dow == WORKDAYS,
onchange: () => dowChangeCallback(WORKDAYS)
},
/*LANG*/"Weekends": {
value: dow == WEEKEND,
onchange: () => dowChangeCallback(WEEKEND)
},
/*LANG*/"Every Day": {
value: dow == EVERY_DAY,
onchange: () => dowChangeCallback(EVERY_DAY)
},
/*LANG*/"Custom": {
value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false,
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalDow)
} }
}; };
function getTimer() {
var d = new Date(Date.now() + ((hrs*60)+mins)*60000); E.showMenu(menu);
var hr = d.getHours() + (d.getMinutes()/60) + (d.getSeconds()/3600); }
// Save alarm
return { function showCustomDaysMenu(dow, dowChangeCallback, originalDow) {
on : en, const menu = {
timer : (hrs*60)+mins, "": { "title": /*LANG*/"Custom Days" },
hr : hr, "< Back": () => dowChangeCallback(dow),
rp : false, as: false
};
}
menu["> Save"] = function() {
if (newAlarm) alarms.push(getTimer());
else alarms[alarmIndex] = getTimer();
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
}; };
if (!newAlarm) {
menu["> Delete"] = function() { require("date_utils").dows(firstDayOfWeek).forEach((day, i) => {
alarms.splice(alarmIndex,1); menu[day] = {
require("Storage").write("alarm.json",JSON.stringify(alarms)); value: !!(dow & (1 << (i + firstDayOfWeek))),
onchange: v => v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek)))
};
});
menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalDow, dowChangeCallback)
E.showMenu(menu);
}
function showEditTimerMenu(selectedTimer, timerIndex) {
var isNew = timerIndex === undefined;
var timer = require("sched").newDefaultTimer();
if (selectedTimer) {
Object.assign(timer, selectedTimer);
}
var time = require("time_utils").decodeTime(timer.timer);
const menu = {
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
"< Back": () => {
saveTimer(timer, timerIndex, time);
showMainMenu(); showMainMenu();
},
/*LANG*/"Hours": {
value: time.h,
min: 0,
max: 23,
wrap: true,
onchange: v => time.h = v
},
/*LANG*/"Minutes": {
value: time.m,
min: 0,
max: 59,
wrap: true,
onchange: v => time.m = v
},
/*LANG*/"Enabled": {
value: timer.on,
onchange: v => timer.on = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
};
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
if (confirm) {
alarms.splice(timerIndex, 1);
saveAndReload();
showMainMenu();
} else {
timer.timer = require("time_utils").encodeTime(time);
setTimeout(showEditTimerMenu, 10, timer, timerIndex)
}
});
}; };
} }
return E.showMenu(menu);
E.showMenu(menu);
}
function saveTimer(timer, timerIndex, time) {
timer.timer = require("time_utils").encodeTime(time);
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
timer.last = 0;
if (timerIndex === undefined) {
alarms.push(timer);
} else {
alarms[timerIndex] = timer;
}
saveAndReload();
}
function showAdvancedMenu() {
E.showMenu({
"": { "title": /*LANG*/"Advanced" },
"< Back": () => showMainMenu(),
/*LANG*/"Scheduler Settings": () => eval(require("Storage").read("sched.settings.js"))(() => showAdvancedMenu()),
/*LANG*/"Enable All": () => enableAll(true),
/*LANG*/"Disable All": () => enableAll(false),
/*LANG*/"Delete All": () => deleteAll()
});
}
function enableAll(on) {
if (alarms.filter(e => e.on == !on).length == 0) {
E.showPrompt(on ? /*LANG*/"Nothing to Enable" : /*LANG*/"Nothing to Disable", {
title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All",
buttons: { /*LANG*/"Ok": true }
}).then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", { title: on ? "/*LANG*/Enable All" : /*LANG*/"Disable All" }).then((confirm) => {
if (confirm) {
alarms.forEach(alarm => alarm.on = on);
saveAndReload();
showMainMenu();
} else {
showAdvancedMenu();
}
});
}
}
function deleteAll() {
if (alarms.length == 0) {
E.showPrompt(/*LANG*/"Nothing to delete", { title: /*LANG*/"Delete All", buttons: { /*LANG*/"Ok": true } }).then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", {
title: /*LANG*/"Delete All"
}).then((confirm) => {
if (confirm) {
alarms = [];
saveAndReload();
showMainMenu();
} else {
showAdvancedMenu();
}
});
}
} }
showMainMenu(); showMainMenu();

View File

@ -1,25 +0,0 @@
// check for alarms
(function() {
var alarms = require('Storage').readJSON('alarm.json',1)||[];
var time = new Date();
var active = alarms.filter(a=>a.on);
if (active.length) {
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
if (!require('Storage').read("alarm.js")) {
console.log("No alarm app!");
require('Storage').write('alarm.json',"[]");
} else {
var t = 3600000*(active[0].hr-hr);
if (active[0].last == time.getDate() || t < 0) t += 86400000;
if (t<1000) t=1000;
/* execute alarm at the correct time. We avoid execing immediately
since this code will get called AGAIN when alarm.js is loaded. alarm.js
will then clearInterval() to get rid of this call so it can proceed
normally. */
setTimeout(function() {
load("alarm.js");
},t);
}
}
})();

View File

@ -1,18 +1,30 @@
{ {
"id": "alarm", "id": "alarm",
"name": "Default Alarm & Timer", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.15", "version": "0.27",
"description": "Set and respond to alarms and timers", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm,widget", "tags": "tool,alarm,widget",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": [ "BANGLEJS", "BANGLEJS2" ],
"readme": "README.md",
"dependencies": { "scheduler":"type" },
"storage": [ "storage": [
{"name":"alarm.app.js","url":"app.js"}, { "name": "alarm.app.js", "url": "app.js" },
{"name":"alarm.boot.js","url":"boot.js"}, { "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
{"name":"alarm.js","url":"alarm.js"}, { "name": "alarm.wid.js", "url": "widget.js" }
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"}
], ],
"data": [{"name":"alarm.json"}] "screenshots": [
{ "url": "screenshot-1.png" },
{ "url": "screenshot-2.png" },
{ "url": "screenshot-3.png" },
{ "url": "screenshot-4.png" },
{ "url": "screenshot-5.png" },
{ "url": "screenshot-6.png" },
{ "url": "screenshot-7.png" },
{ "url": "screenshot-8.png" },
{ "url": "screenshot-9.png" },
{ "url": "screenshot-10.png" },
{ "url": "screenshot-11.png" }
]
} }

BIN
apps/alarm/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
apps/alarm/screenshot-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
apps/alarm/screenshot-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
apps/alarm/screenshot-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
apps/alarm/screenshot-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
apps/alarm/screenshot-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,7 +1,8 @@
WIDGETS["alarm"]={area:"tl",width:0,draw:function() { WIDGETS["alarm"]={area:"tl",width:0,draw:function() {
if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
},reload:function() { },reload:function() {
WIDGETS["alarm"].width = (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? 24 : 0; // don't include library here as we're trying to use as little RAM as possible
WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0;
} }
}; };
WIDGETS["alarm"].reload(); WIDGETS["alarm"].reload();

View File

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

2
apps/altimeter/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Actually upload correct code

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///t9TmuV3+GJf4AN+ALVgf8BasP/4LVn//4ALUWgJUJBZUDBYJUIBZcP3/nKhEOt/WBZE5r+VKg0KgEVr9V3wLHqtaqt9sALElWAqoABt1QBZNeBYuq0ILCrVUBYulBYVWBYkCBYgABBZ8K1WVBYlABZegKQWqBQlVqALKqWoKQWpBYtWBZeqKRAAB1WABZZSHAANq0ALLKQ6qC1ALLKQ5UEAH4AG"))

30
apps/altimeter/app.js Normal file
View File

@ -0,0 +1,30 @@
Bangle.setBarometerPower(true, "app");
g.clear(1);
Bangle.loadWidgets();
Bangle.drawWidgets();
var zero = 0;
var R = Bangle.appRect;
var y = R.y + R.h/2;
var MEDIANLENGTH = 20;
var avr = [], median;
var value = 0;
Bangle.on('pressure', function(e) {
while (avr.length>MEDIANLENGTH) avr.pop();
avr.unshift(e.altitude);
median = avr.slice().sort();
g.reset().clearRect(0,y-30,g.getWidth()-10,y+30);
if (median.length>10) {
var mid = median.length>>1;
value = E.sum(median.slice(mid-4,mid+5)) / 9;
g.setFont("Vector",50).setFontAlign(0,0).drawString((value-zero).toFixed(1), g.getWidth()/2, y);
}
});
g.reset();
g.setFont("6x8").setFontAlign(0,0).drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40);
g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"ZERO", g.getWidth()-5, g.getHeight()/2);
setWatch(function() {
zero = value;
}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true});

BIN
apps/altimeter/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,12 @@
{ "id": "altimeter",
"name": "Altimeter",
"version":"0.02",
"description": "Simple altimeter that can display height changed using Bangle.js 2's built in pressure sensor.",
"icon": "app.png",
"tags": "tool,outdoors",
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"altimeter.app.js","url":"app.js"},
{"name":"altimeter.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -6,3 +6,4 @@
0.05: Fix handling of message actions 0.05: Fix handling of message actions
0.06: Option to keep messages after a disconnect (default false) (fix #1186) 0.06: Option to keep messages after a disconnect (default false) (fix #1186)
0.07: Include charging state in battery updates to phone 0.07: Include charging state in battery updates to phone
0.08: Handling of alarms

View File

@ -21,6 +21,7 @@ of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js * `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them? keep any messages it has received, or should it delete them?
* `Messages` - launches the messages app, showing a list of messages * `Messages` - launches the messages app, showing a list of messages
* `Alarms` - opens a submenu where you can set default settings for alarms such as vibration pattern, repeat, and auto snooze
## How it works ## How it works

View File

@ -5,6 +5,11 @@
} }
var settings = require("Storage").readJSON("android.settings.json",1)||{}; var settings = require("Storage").readJSON("android.settings.json",1)||{};
//default alarm settings
if (settings.rp == undefined) settings.rp = true;
if (settings.as == undefined) settings.as = true;
if (settings.vibrate == undefined) settings.vibrate = "..";
require('Storage').writeJSON("android.settings.json", settings);
var _GB = global.GB; var _GB = global.GB;
global.GB = (event) => { global.GB = (event) => {
// feed a copy to other handlers if there were any // feed a copy to other handlers if there were any
@ -44,6 +49,40 @@
title:event.name||"Call", body:"Incoming call\n"+event.number}); title:event.name||"Call", body:"Incoming call\n"+event.number});
require("messages").pushMessage(event); require("messages").pushMessage(event);
}, },
"alarm" : function() {
//wipe existing GB alarms
var sched;
try { sched = require("sched"); } catch (e) {}
if (!sched) return; // alarms may not be installed
var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
for (var i = 0; i < gbalarms.length; i++)
sched.setAlarm(gbalarms[i].id, undefined);
var alarms = sched.getAlarms();
var time = new Date();
var currentTime = time.getHours() * 3600000 +
time.getMinutes() * 60000 +
time.getSeconds() * 1000;
for (var j = 0; j < event.d.length; j++) {
// prevents all alarms from going off at once??
var dow = event.d[j].rep;
if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = {
id : "gb"+j,
appid : "gbalarms",
on : true,
t : event.d[j].h * 3600000 + event.d[j].m * 60000,
dow : ((dow&63)<<1) | (dow>>6), // Gadgetbridge sends DOW in a different format
last : last,
rp : settings.rp,
as : settings.as,
vibrate : settings.vibrate
};
alarms.push(a);
}
sched.setAlarms(alarms);
sched.reload();
},
}; };
var h = HANDLERS[event.t]; var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event); if (h) h(); else console.log("GB Unknown",event);

View File

@ -2,7 +2,7 @@
"id": "android", "id": "android",
"name": "Android Integration", "name": "Android Integration",
"shortName": "Android", "shortName": "Android",
"version": "0.07", "version": "0.08",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge", "tags": "tool,system,messages,notifications,gadgetbridge",

View File

@ -10,8 +10,8 @@
"" : { "title" : "Android" }, "" : { "title" : "Android" },
"< Back" : back, "< Back" : back,
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
"Find Phone" : () => E.showMenu({ /*LANG*/"Find Phone" : () => E.showMenu({
"" : { "title" : "Find Phone" }, "" : { "title" : /*LANG*/"Find Phone" },
"< Back" : ()=>E.showMenu(mainmenu), "< Back" : ()=>E.showMenu(mainmenu),
/*LANG*/"On" : _=>gb({t:"findPhone",n:true}), /*LANG*/"On" : _=>gb({t:"findPhone",n:true}),
/*LANG*/"Off" : _=>gb({t:"findPhone",n:false}), /*LANG*/"Off" : _=>gb({t:"findPhone",n:false}),
@ -24,7 +24,28 @@
updateSettings(); updateSettings();
} }
}, },
/*LANG*/"Messages" : ()=>load("messages.app.js") /*LANG*/"Messages" : ()=>load("messages.app.js"),
/*LANG*/"Alarms" : () => E.showMenu({
"" : { "title" : /*LANG*/"Alarms" },
"< Back" : ()=>E.showMenu(mainmenu),
/*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => {settings.vibrate = v; updateSettings();}),
/*LANG*/"Repeat": {
value: settings.rp,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.rp = v;
updateSettings();
}
},
/*LANG*/"Auto snooze": {
value: settings.as,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.as = v;
updateSettings();
}
},
})
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
}) })

View File

@ -9,3 +9,4 @@
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek> 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) 0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits

View File

@ -99,7 +99,7 @@ function updateState() {
} }
function isoStr(date) { function isoStr(date) {
return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).substr(-2) + "-" + ("0" + date.getDate()).substr(-2); return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2);
} }
var calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested) var calWeekBuffer = [false,false,false]; //buffer tz, date, week no (once calculated until other tz or date is requested)
@ -140,7 +140,7 @@ function draw() {
g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time
if (secondsScreen) { if (secondsScreen) {
y += 65; y += 65;
var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).substr(-2); var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2);
if (doColor()) if (doColor())
g.setColor(0, 0, 1); g.setColor(0, 0, 1);
g.setFont("AntonSmall"); g.setFont("AntonSmall");
@ -193,7 +193,7 @@ function draw() {
if (calWeek || weekDay) { if (calWeek || weekDay) {
var dowcwStr = ""; var dowcwStr = "";
if (calWeek) if (calWeek)
dowcwStr = " #" + ("0" + ISO8601calWeek(date)).substring(-2); dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2);
if (weekDay) if (weekDay)
dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort #<calWeek> e.g. Mon #01 dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort #<calWeek> e.g. Mon #01
else //week #01 else //week #01

View File

@ -1,7 +1,7 @@
{ {
"id": "antonclk", "id": "antonclk",
"name": "Anton Clock", "name": "Anton Clock",
"version": "0.07", "version": "0.08",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.", "description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md", "readme":"README.md",
"icon": "app.png", "icon": "app.png",

View File

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

View File

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

View File

@ -4,3 +4,4 @@
0.04: Fix tapping at very bottom of list, exit on inactivity 0.04: Fix tapping at very bottom of list, exit on inactivity
0.05: Add support for bulk importing and exporting tokens 0.05: Add support for bulk importing and exporting tokens
0.06: Add spaces to codes for improved readability (thanks @BartS23) 0.06: Add spaces to codes for improved readability (thanks @BartS23)
0.07: Bangle 2: Improve drag responsiveness and exit on button press

View File

@ -1,6 +1,10 @@
const tokenextraheight = 16; const COUNTER_TRIANGLE_SIZE = 10;
var tokendigitsheight = 30; const TOKEN_EXTRA_HEIGHT = 16;
var tokenheight = tokendigitsheight + tokenextraheight; var TOKEN_DIGITS_HEIGHT = 30;
var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT;
const PROGRESSBAR_HEIGHT = 3;
const IDLE_REPEATS = 1; // when idle, the number of extra timed periods to show before hiding
const SETTINGS = "authentiwatch.json";
// Hash functions // Hash functions
const crypto = require("crypto"); const crypto = require("crypto");
const algos = { const algos = {
@ -8,33 +12,24 @@ const algos = {
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 }, "SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 }, "SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
}; };
const calculating = "Calculating"; const CALCULATING = /*LANG*/"Calculating";
const notokens = "No tokens"; const NO_TOKENS = /*LANG*/"No tokens";
const notsupported = "Not supported"; const NOT_SUPPORTED = /*LANG*/"Not supported";
// sample settings: // sample settings:
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} // {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}}; var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[], misc:{}};
if (settings.data ) tokens = settings.data ; /* v0.02 settings */ if (settings.data ) tokens = settings.data ; /* v0.02 settings */
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
// QR Code Text
//
// Example:
//
// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret}
//
// ${algorithm} : one of SHA1 / SHA256 / SHA512
// ${digits} : one of 6 / 8
// ${period} : one of 30 / 60
// ${url} : a domain name "example.com"
// ${secret} : the seed code
function b32decode(seedstr) { function b32decode(seedstr) {
// RFC4648 // RFC4648 Base16/32/64 Data Encodings
var i, buf = 0, bitcount = 0, retstr = ""; let buf = 0, bitcount = 0, retstr = "";
for (i in seedstr) { for (let c of seedstr.toUpperCase()) {
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0); if (c == '0') c = 'O';
if (c == '1') c = 'I';
if (c == '8') c = 'B';
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
if (c != -1) { if (c != -1) {
buf <<= 5; buf <<= 5;
buf |= c; buf |= c;
@ -46,195 +41,127 @@ function b32decode(seedstr) {
} }
} }
} }
var retbuf = new Uint8Array(retstr.length); let retbuf = new Uint8Array(retstr.length);
for (i in retstr) { for (let i in retstr) {
retbuf[i] = retstr.charCodeAt(i); retbuf[i] = retstr.charCodeAt(i);
} }
return retbuf; return retbuf;
} }
function do_hmac(key, message, algo) {
var a = algos[algo]; function hmac(key, message, algo) {
// RFC2104 let a = algos[algo.toUpperCase()];
// RFC2104 HMAC
if (key.length > a.blksz) { if (key.length > a.blksz) {
key = a.sha(key); key = a.sha(key);
} }
var istr = new Uint8Array(a.blksz + message.length); let istr = new Uint8Array(a.blksz + message.length);
var ostr = new Uint8Array(a.blksz + a.retsz); let ostr = new Uint8Array(a.blksz + a.retsz);
for (var i = 0; i < a.blksz; ++i) { for (let i = 0; i < a.blksz; ++i) {
var c = (i < key.length) ? key[i] : 0; let c = (i < key.length) ? key[i] : 0;
istr[i] = c ^ 0x36; istr[i] = c ^ 0x36;
ostr[i] = c ^ 0x5C; ostr[i] = c ^ 0x5C;
} }
istr.set(message, a.blksz); istr.set(message, a.blksz);
ostr.set(a.sha(istr), a.blksz); ostr.set(a.sha(istr), a.blksz);
var ret = a.sha(ostr); let ret = a.sha(ostr);
// RFC4226 dynamic truncation // RFC4226 HOTP (dynamic truncation)
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
return v.getUint32(0) & 0x7FFFFFFF; return v.getUint32(0) & 0x7FFFFFFF;
} }
function hotp(d, token, dohmac) {
var tick; function formatOtp(otp, digits) {
// 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) { if (token.period > 0) {
// RFC6238 - timed // RFC6238 - timed
var seconds = Math.floor(d.getTime() / 1000); tick = Math.floor(Math.floor(d / 1000) / token.period);
tick = Math.floor(seconds / token.period); next = (tick + 1) * token.period * 1000;
} else { } else {
// RFC4226 - counter // RFC4226 - counter
tick = -token.period; tick = -token.period;
next = d + 30000;
} }
var msg = new Uint8Array(8); let msg = new Uint8Array(8);
var v = new DataView(msg.buffer); let v = new DataView(msg.buffer);
v.setUint32(0, tick >> 16 >> 16); v.setUint32(0, tick >> 16 >> 16);
v.setUint32(4, tick & 0xFFFFFFFF); v.setUint32(4, tick & 0xFFFFFFFF);
var ret = calculating; let ret;
if (dohmac) { try {
try { ret = hmac(b32decode(token.secret), msg, token.algorithm);
var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); ret = formatOtp(ret, token.digits);
ret = "" + hash % Math.pow(10, token.digits); } catch(err) {
while (ret.length < token.digits) { ret = NOT_SUPPORTED;
ret = "0" + ret;
}
// add a space after every 3rd or 4th digit
var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : ".";
ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
} catch(err) {
ret = notsupported;
}
} }
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 = { var state = {
listy: 0, listy:0, // list scroll position
prevcur:0, id:-1, // current token ID
curtoken:-1, hotp:{hotp:"",next:0}
nextTime:0,
otp:"",
rem:0,
hide:0
}; };
function drawToken(id, r) { function sizeFont(id, txt, w) {
var x1 = r.x; let sz = fontszCache[id];
var y1 = r.y; if (!sz) {
var x2 = r.x + r.w - 1; sz = TOKEN_DIGITS_HEIGHT;
var y2 = r.y + r.h - 1;
var adj, lbl, sz;
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
lbl = tokens[id].label.substr(0, 10);
if (id == state.curtoken) {
// current token
g.setColor(g.theme.fgH)
.setBgColor(g.theme.bgH)
.setFont("Vector", tokenextraheight)
// center just below top line
.setFontAlign(0, -1, 0);
adj = y1;
} else {
g.setColor(g.theme.fg)
.setBgColor(g.theme.bg);
sz = tokendigitsheight;
do { do {
g.setFont("Vector", sz--); g.setFont("Vector", sz--);
} while (g.stringWidth(lbl) > r.w); } while (g.stringWidth(txt) > w);
// center in box fontszCache[id] = ++sz;
g.setFontAlign(0, 0, 0);
adj = (y1 + y2) / 2;
} }
g.clearRect(x1, y1, x2, y2) g.setFont("Vector", sz);
.drawString(lbl, (x1 + x2) / 2, adj, false);
if (id == state.curtoken) {
if (tokens[id].period > 0) {
// timed - draw progress bar
let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period);
g.fillRect(x1, y2 - 4, xr, y2 - 1);
adj = 0;
} else {
// counter - draw triangle as swipe hint
let yc = (y1 + y2) / 2;
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
adj = 12;
}
// digits just below label
sz = tokendigitsheight;
do {
g.setFont("Vector", sz--);
} while (g.stringWidth(state.otp) > (r.w - adj));
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
}
// shaded lines top and bottom
g.setColor(0.5, 0.5, 0.5)
.drawLine(x1, y1, x2, y1)
.drawLine(x1, y2, x2, y2)
.setClipRect(0, 0, g.getWidth(), g.getHeight());
} }
function draw() { tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
var timerfn = exitApp; half = n => Math.floor(n / 2);
var timerdly = 10000;
var d = new Date(); function timerCalc() {
if (state.curtoken != -1) { let timerfn = exitApp;
var t = tokens[state.curtoken]; let timerdly = 10000;
if (state.otp == calculating) { if (state.id >= 0 && state.hotp.hotp != "") {
state.otp = hotp(d, t, true).hotp; if (tokens[state.id].period > 0) {
} // timed HOTP
if (d.getTime() > state.nextTime) { if (state.hotp.next < Date.now()) {
if (state.hide == 0) { if (state.cnt > 0) {
// auto-hide the current token state.cnt--;
if (state.curtoken != -1) { state.hotp = hotp(tokens[state.id]);
state.prevcur = state.curtoken; } else {
state.curtoken = -1; state.hotp.hotp = "";
} }
state.nextTime = 0; timerdly = 1;
timerfn = updateCurrentToken;
} else { } else {
// time to generate a new token timerdly = 1000;
var r = hotp(d, t, state.otp != ""); timerfn = updateProgressBar;
state.nextTime = r.next;
state.otp = r.hotp;
if (t.period <= 0) {
state.hide = 1;
}
state.hide--;
}
}
state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000));
}
if (tokens.length > 0) {
var drewcur = false;
var id = Math.floor(state.listy / tokenheight);
var y = id * tokenheight + Bangle.appRect.y - state.listy;
while (id < tokens.length && y < Bangle.appRect.y2) {
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight});
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
drewcur = true;
}
id += 1;
y += tokenheight;
}
if (drewcur) {
// the current token has been drawn - schedule a redraw
if (tokens[state.curtoken].period > 0) {
timerdly = (state.otp == calculating) ? 1 : 1000; // timed
} else {
timerdly = state.nexttime - d.getTime(); // counter
}
timerfn = draw;
if (tokens[state.curtoken].period <= 0) {
state.hide = 0;
} }
} else { } else {
// de-select the current token if it is scrolled out of view // counter HOTP
if (state.curtoken != -1) { if (state.cnt > 0) {
state.prevcur = state.curtoken; state.cnt--;
state.curtoken = -1; timerdly = 30000;
} else {
state.hotp.hotp = "";
timerdly = 1;
} }
state.nexttime = 0; timerfn = updateCurrentToken;
} }
} else {
g.setFont("Vector", tokendigitsheight)
.setFontAlign(0, 0, 0)
.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
} }
if (state.drawtimer) { if (state.drawtimer) {
clearTimeout(state.drawtimer); clearTimeout(state.drawtimer);
@ -242,97 +169,236 @@ function draw() {
state.drawtimer = setTimeout(timerfn, timerdly); state.drawtimer = setTimeout(timerfn, timerdly);
} }
function onTouch(zone, e) { function updateCurrentToken() {
if (e) { drawToken(state.id);
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight); timerCalc();
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { }
id = -1;
} function updateProgressBar() {
if (state.curtoken != id) { drawProgressBar();
if (id != -1) { timerCalc();
var y = id * tokenheight - state.listy; }
if (y < 0) {
state.listy += y; function drawProgressBar() {
y = 0; let id = state.id;
if (id >= 0 && tokens[id].period > 0) {
let rem = Math.min(tokens[id].period, Math.floor((state.hotp.next - Date.now()) / 1000));
if (rem >= 0) {
let y1 = tokenY(id);
let y2 = y1 + TOKEN_HEIGHT - 1;
if (y2 >= AR.y && y1 <= AR.y2) {
// token visible
y1 = y2 - PROGRESSBAR_HEIGHT;
if (y1 <= AR.y2)
{
// progress bar visible
y2 = Math.min(y2, AR.y2);
let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x;
g.setColor(g.theme.fgH)
.setBgColor(g.theme.bgH)
.fillRect(AR.x, y1, xr, y2)
.clearRect(xr + 1, y1, AR.x2, y2);
} }
y += tokenheight; } else {
if (y > Bangle.appRect.h) { // token not visible
state.listy += (y - Bangle.appRect.h); state.id = -1;
}
state.otp = "";
} }
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) { function onDrag(e) {
if (e.x > g.getWidth() || e.y > g.getHeight()) return; state.cnt = IDLE_REPEATS;
if (e.dx == 0 && e.dy == 0) return; if (e.b != 0 && e.dy != 0) {
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h); let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
state.listy = Math.max(0, newy); if (state.listy != y) {
draw(); let id, dy = state.listy - y;
state.listy = y;
g.setClipRect(AR.x, AR.y, AR.x2, AR.y2)
.scroll(0, dy);
if (dy > 0) {
id = Math.floor((state.listy + dy) / TOKEN_HEIGHT);
y = tokenY(id + 1);
do {
drawToken(id);
id--;
y -= TOKEN_HEIGHT;
} while (y > AR.y);
}
if (dy < 0) {
id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT);
y = tokenY(id);
while (y < AR.y2) {
drawToken(id);
id++;
y += TOKEN_HEIGHT;
}
}
}
}
if (e.b == 0) {
timerCalc();
}
}
function onTouch(zone, e) {
state.cnt = IDLE_REPEATS;
if (e) {
let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT);
if (id == state.id || tokens.length == 0 || id >= tokens.length) {
id = -1;
}
if (state.id != id) {
if (id >= 0) {
// scroll token into view if necessary
let dy = 0;
let y = id * TOKEN_HEIGHT - state.listy;
if (y < 0) {
dy -= y;
y = 0;
}
y += TOKEN_HEIGHT;
if (y > AR.h) {
dy -= (y - AR.h);
}
onDrag({b:1, dy:dy});
}
changeId(id);
}
}
timerCalc();
} }
function onSwipe(e) { function onSwipe(e) {
if (e == 1) { state.cnt = IDLE_REPEATS;
switch (e) {
case 1:
exitApp(); exitApp();
break;
case -1:
if (state.id >= 0 && tokens[state.id].period <= 0) {
tokens[state.id].period--;
require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc});
state.hotp.hotp = CALCULATING;
drawToken(state.id);
}
break;
} }
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { timerCalc();
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();
} }
function bangle1Btn(e) { function bangleBtn(e) {
state.cnt = IDLE_REPEATS;
if (tokens.length > 0) { if (tokens.length > 0) {
if (state.curtoken == -1) { let id = E.clip(state.id + e, 0, tokens.length - 1);
state.curtoken = state.prevcur; onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))});
} else { changeId(id);
switch (e) { drawProgressBar();
case -1: state.curtoken--; break;
case 1: state.curtoken++; break;
}
}
state.curtoken = Math.max(state.curtoken, 0);
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
state.listy = state.curtoken * tokenheight;
state.listy -= (Bangle.appRect.h - tokenheight) / 2;
state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h);
state.listy = Math.max(state.listy, 0);
var fakee = {};
fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y;
state.curtoken = -1;
state.nextTime = 0;
onTouch(0, fakee);
} else {
draw(); // resets idle timer
} }
timerCalc();
} }
function exitApp() { function exitApp() {
if (state.drawtimer) {
clearTimeout(state.drawtimer);
}
Bangle.showLauncher(); Bangle.showLauncher();
} }
Bangle.on('touch', onTouch); Bangle.on('touch', onTouch);
Bangle.on('drag' , onDrag ); Bangle.on('drag' , onDrag );
Bangle.on('swipe', onSwipe); Bangle.on('swipe', onSwipe);
if (typeof BTN2 == 'number') { if (typeof BTN1 == 'number') {
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true}); if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50}); setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true});
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true}); setWatch(()=>exitApp() , BTN2, {edge:"falling", debounce:50});
setWatch(()=>bangleBtn( 1), BTN3, {edge:"rising" , debounce:50, repeat:true});
} else {
setWatch(()=>exitApp() , BTN1, {edge:"falling", debounce:50});
}
} }
Bangle.loadWidgets(); Bangle.loadWidgets();
const AR = Bangle.appRect;
// Clear the screen once, at startup // draw the initial display
g.clear(); g.clear();
draw(); if (tokens.length > 0) {
state.listy = AR.h;
onDrag({b:1, dy:AR.h});
} else {
g.setFont("Vector", TOKEN_DIGITS_HEIGHT)
.setFontAlign(0, 0, 0)
.drawString(NO_TOKENS, AR.x + half(AR.w), AR.y + half(AR.h), false);
}
timerCalc();
Bangle.drawWidgets(); Bangle.drawWidgets();

View File

@ -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.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} 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 th,#tokens td{padding:5px}
#tokens tr:nth-child(odd){background-color:#ccc} #tokens tr:nth-child(odd){background-color:#f1f1fc}
#tokens tr:nth-child(even){background-color:#eee} #tokens tr:nth-child(even){background-color:#fff}
#qr-canvas{margin:auto;width:calc(100%-20px);max-width:400px} #qr-canvas{margin:auto;width:calc(100%-20px);max-width:400px}
#advbtn,#scan,#tokenqr table{text-align:center} #advbtn,#scan,#tokenqr table{text-align:center}
#edittoken tbody#adv{display:none} #edittoken tbody#adv{display:none}
@ -54,9 +54,9 @@ var tokens = settings.tokens;
*/ */
function base32clean(val, nows) { function base32clean(val, nows) {
var ret = val.replaceAll(/\s+/g, ' '); var ret = val.replaceAll(/\s+/g, ' ');
ret = ret.replaceAll(/0/g, 'O'); ret = ret.replaceAll('0', 'O');
ret = ret.replaceAll(/1/g, 'I'); ret = ret.replaceAll('1', 'I');
ret = ret.replaceAll(/8/g, 'B'); ret = ret.replaceAll('8', 'B');
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, ''); ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
if (nows) { if (nows) {
ret = ret.replaceAll(/\s+/g, ''); ret = ret.replaceAll(/\s+/g, '');
@ -81,9 +81,9 @@ function b32encode(str) {
function b32decode(seedstr) { function b32decode(seedstr) {
// RFC4648 // RFC4648
var i, buf = 0, bitcount = 0, ret = ''; var buf = 0, bitcount = 0, ret = '';
for (i in seedstr) { for (var c of seedstr.toUpperCase()) {
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0); c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
if (c != -1) { if (c != -1) {
buf <<= 5; buf <<= 5;
buf |= c; buf |= c;
@ -226,15 +226,18 @@ function editToken(id) {
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm); markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
markup += '</td></tr>'; markup += '</td></tr>';
markup += '</tbody><tr><td id="advbtn" colspan="2">'; 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 += '</td></tr></table></form>';
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>'; markup += '<button class="btn" type="button" onclick="updateTokens()">Cancel Edit</button>';
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>'; markup += '&nbsp;';
markup += '<button class="btn" type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
markup += '&nbsp;';
if (tokens[id].isnew) { 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 { } else {
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>'; markup += '<button class="btn" type="button" onclick="showTokenQr()">Show QR</button>';
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>'; markup += '&nbsp;';
markup += '<button class="btn" type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
} }
document.getElementById('edit').innerHTML = markup; document.getElementById('edit').innerHTML = markup;
document.body.className = 'editing'; document.body.className = 'editing';
@ -304,9 +307,23 @@ function updateTokens() {
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">'; return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
}; };
const tokenButton = function(fn, id, label, dir) { 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 += '&nbsp;';
markup += '<button class="btn" type="button" onclick="saveTokens()">Save to watch</button>';
markup += '&nbsp;';
markup += '<button class="btn" type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
markup += '&nbsp;';
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 += '&nbsp;';
markup += '<button class="btn" type="button" onclick="exportTokens(true, null)">Show QR</button>';
markup += '</div>';
markup += '<table><tr><th>';
markup += tokenSelect('all'); markup += tokenSelect('all');
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>'; markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
/* any tokens marked new are cancelled new additions and must be removed */ /* any tokens marked new are cancelled new additions and must be removed */
@ -331,15 +348,6 @@ function updateTokens() {
markup += '</td></tr>'; markup += '</td></tr>';
} }
markup += '</table>'; 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.getElementById('tokens').innerHTML = markup;
document.body.className = 'select'; document.body.className = 'select';
} }
@ -405,7 +413,7 @@ class proto3decoder {
constructor(str) { constructor(str) {
this.buf = []; this.buf = [];
for (let i in str) { for (let i in str) {
this.buf = this.buf.concat(str.charCodeAt(i)); this.buf.push(str.charCodeAt(i));
} }
} }
getVarint() { getVarint() {
@ -487,7 +495,7 @@ function startScan(handler,cancel) {
document.body.className = 'scanning'; document.body.className = 'scanning';
navigator.mediaDevices navigator.mediaDevices
.getUserMedia({video:{facingMode:'environment'}}) .getUserMedia({video:{facingMode:'environment'}})
.then(function(stream){ .then(stream => {
scanning=true; scanning=true;
video.setAttribute('playsinline',true); video.setAttribute('playsinline',true);
video.srcObject = stream; video.srcObject = stream;
@ -604,7 +612,7 @@ function qrBack() {
<div id="scan"> <div id="scan">
<table> <table>
<tr><td><canvas id="qr-canvas"></canvas></td></tr> <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> </table>
</div> </div>
@ -613,7 +621,7 @@ function qrBack() {
<div id="showqr"> <div id="showqr">
<table><tr><td id="qrcode"></td></tr><tr><td> <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> </td></tr></table>
</div> </div>

View File

@ -4,7 +4,7 @@
"shortName": "AuthWatch", "shortName": "AuthWatch",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}], "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
"version": "0.06", "version": "0.07",
"description": "Google Authenticator compatible tool.", "description": "Google Authenticator compatible tool.",
"tags": "tool", "tags": "tool",
"interface": "interface.html", "interface": "interface.html",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -7,3 +7,4 @@
0.07: Update to use Bangle.setUI instead of setWatch 0.07: Update to use Bangle.setUI instead of setWatch
0.08: Use theme colors, Layout library 0.08: Use theme colors, Layout library
0.09: Fix time/date disappearing after fullscreen notification 0.09: Fix time/date disappearing after fullscreen notification
0.10: Use ClockFace library

View File

@ -11,13 +11,9 @@ let locale = require("locale");
date.setMonth(1, 3); // februari: months are zero-indexed date.setMonth(1, 3); // februari: months are zero-indexed
const localized = locale.date(date, true); const localized = locale.date(date, true);
locale.dayFirst = /3.*2/.test(localized); locale.dayFirst = /3.*2/.test(localized);
locale.hasMeridian = (locale.meridian(date)!=="");
locale.hasMeridian = false;
if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed
locale.hasMeridian = (locale.meridian(date)!=="");
}
} }
Bangle.loadWidgets();
function renderBar(l) { function renderBar(l) {
if (!this.fraction) { if (!this.fraction) {
// zero-size fillRect stills draws one line of pixels, we don't want that // zero-size fillRect stills draws one line of pixels, we don't want that
@ -27,32 +23,6 @@ function renderBar(l) {
g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1);
} }
const Layout = require("Layout");
const layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((g.getWidth()-24)/(5*6));
} else {
layout.ampm.label = "";
thickness = Math.floor(g.getWidth()/(5*6));
}
layout.bar.height = thickness+1;
layout.time.font = "6x8:"+thickness;
layout.update();
function timeText(date) { function timeText(date) {
if (!is12Hour) { if (!is12Hour) {
@ -78,31 +48,48 @@ function dateText(date) {
return `${dayName} ${dayMonth}`; return `${dayName} ${dayMonth}`;
} }
draw = function draw(force) {
if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled
const date = new Date();
layout.time.label = timeText(date);
layout.ampm.label = ampmText(date);
layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
if (force) {
Bangle.drawWidgets();
layout.forgetLazyState();
}
layout.render();
// schedule update at start of next second
const millis = date.getMilliseconds();
setTimeout(draw, 1000-millis);
};
// Show launcher when button pressed const ClockFace = require("ClockFace"),
Bangle.setUI("clock"); clock = new ClockFace({
Bangle.on("lcdPower", function(on) { precision:1,
if (on) { init: function() {
draw(true); const Layout = require("Layout");
} this.layout = new Layout({
}); type: "v", c: [
g.reset().clear(); {
Bangle.drawWidgets(); type: "h", c: [
draw(); {id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
this.layout.bar.height = thickness+1;
this.layout.time.font = "6x8:"+thickness;
this.layout.update();
},
update: function(date, c) {
if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d) this.layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
this.layout.render();
},
resume: function() {
this.layout.forgetLazyState();
},
});
clock.start();

View File

@ -1,7 +1,7 @@
{ {
"id": "barclock", "id": "barclock",
"name": "Bar Clock", "name": "Bar Clock",
"version": "0.09", "version": "0.10",
"description": "A simple digital clock showing seconds as a bar", "description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png", "icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

3
apps/bee/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New app!
0.02: Fix bug with regenerating index, fix bug in word lookups
0.03: Improve word search performance

33
apps/bee/README.md Normal file
View File

@ -0,0 +1,33 @@
# Spelling bee game
Word finding game inspired by the NYT spelling bee. Find as many words with 4 or more letters (must include the
letter at the center of the 'hive') as you can.
## Usage
- tap on letters to type out word
- swipe left to delete last letter
- swipe right to enter; the word will turn blue while it is being checked against the internal dictionary; once
checked, it will turn red if the word is invalid, does not contain the central letter or has been guessed before or
will turn green if it is a valid word; in the latter case, points will be awarded
- swipe down to shuffle the 6 outer letters
- swipe up to view a list of already guessed words; tap on any of them to return to the regular game.
## Scoring
The number of correctly guessed words is displayed on the bottom left, the score on the bottom right. A single point
is awarded for a 4 letter word, or the number of letters if longer. A pangram is a word that contains all 7 letters at
least once and yields an additional 7 points. Each game contains at least one pangram.
## Technical remarks
The game uses an internal dictionary consisting of a newline separated list of English words ('bee.words', using the '2of12inf' word list).
The dictionary is fairly large (~700kB of flash space) and thus requires appropriate space on the watch and will make installing the app somewhat
slow. Because of its size it cannot be compressed (heatshrink needs to hold the compressed/uncompressed data in memory).
This file can be replaced with a custom dictionary, an ASCII file containing a newline-separated (single "\n", not DOS-style "\r\n") alphabetically
sorted (sorting is important for the word lookup algorithm) list of words.
![Screenshot](./bee_screenshot.png)

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AE2JAAIKHnc7DyNPp4vRGAwuBGB4sBAAQvSGIovPFqYvHGAYvDGBYsGGhwvGGIQvEGBQnDMYhkNGBAvOvQABqyRTF5GJr4wLFwQACX6IwLsowJLYMrldVGAQvTsoADGBITD0YvDldPF6n+F4gyGGAdP5nMF4KKBGDJZDGI7EBcoOiGAK7DGAQvYRogxEr1Pp9VMAiSBBILBWeJIxCromBMAQwDAAZfTGBQyCxOCGAIvBGIV/F7AwMAAOIp95GAYACFqoyQMAIwGF7QADEQd5FgIADqvGF8DnEAAIvFGIWjF8CFE0QwHAAQudAAK0EGBQuecw3GqpemYIxiCGIa8cF4wwHdTwvJp9/F82jGA9VMQovf5jkHGIwvg4wvIAAgvg5miF9wwNF8QABF9QwF0YuoF4oxCqoulGBAAB42i0QvjGBPMF0gwIFswwHF1IA/AH4A/AH4AL"))

BIN
apps/bee/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

181
apps/bee/bee.app.js Normal file
View File

@ -0,0 +1,181 @@
const S = require("Storage");
const words = S.read("bee.words");
var letters = [];
var centers = [];
var word = '';
var foundWords = [];
var score = 0;
var intervalID = -1;
function biSearch(w, ws, start, end, count) {
"compile"
if (start>end-w.legnth || count--<=0) return ws.substr(start, end-start).indexOf("\n"+w+"\n");
var mid = (end+start)>>1;
if (ws[mid-1]==="\n") --mid;
else while (mid<end && ws[mid]!=="\n") mid++;
var i = 0;
while (i<w.length && ws[mid+i+1]==w[i]) ++i;
if (i==w.length && ws[mid+i+1]==="\n") return mid+1;
if (i==w.length || w[i]<ws[mid+i+1]) return biSearch(w, ws, start, mid+1, count);
if (w[i]>ws[mid+i+1]) return biSearch(w, ws, mid+1, end, count);
}
function isPangram(w) {
var ltrs = '';
for (var i=0; i<w.length; ++i) if (ltrs.indexOf(w[i])===-1) ltrs += w[i];
return ltrs.length==7;
}
function checkWord (w) {
if (w.indexOf(String.fromCharCode(97+letters[0]))==-1) return false; // does it contain central letter?
if (foundWords.indexOf(w)>=0) return false; // already found
if (biSearch(w, words, 0, words.length, 20)>-1) {
foundWords.push(w);
foundWords.sort();
if (w.length==4) score++;
else score += w.length;
if (isPangram(w)) score += 7;
return true;
}
return false;
}
function getHexPoly(cx, cy, r, a) {
var p = [];
for (var i=0; i<6; ++i) p.push(cx+r*Math.sin((i+a)/3*Math.PI), cy+r*Math.cos((i+a)/3*Math.PI));
return p;
}
function drawHive() {
w = g.getWidth();
h = g.getHeight();
const R = w/3.3;
centers = getHexPoly(w/2, h/2+10, R, 0);
centers.push(w/2, h/2+10);
g.clear();
g.setFont("Vector", w/7).setFontAlign(0, 0, 0);
g.setColor(g.theme.fg);
for (var i=0; i<6; ++i) {
g.drawPoly(getHexPoly(centers[2*i], centers[2*i+1], 0.9*R/Math.sqrt(3), 0.5), {closed:true});
g.drawString(String.fromCharCode(65+letters[i+1]), centers[2*i]+2, centers[2*i+1]+2);
}
g.setColor(1, 1, 0).fillPoly(getHexPoly(w/2, h/2+10, 0.9*R/Math.sqrt(3), 0.5));
g.setColor(0).drawString(String.fromCharCode(65+letters[0]), w/2+2, h/2+10+2);
}
function shuffleLetters(qAll) {
for (var i=letters.length-1; i > 0; i--) {
var j = (1-qAll) + Math.floor(Math.random()*(i+qAll));
var temp = letters[i];
letters[i] = letters[j];
letters[j] = temp;
}
}
function pickLetters() {
var ltrs = "";
while (ltrs.length!==7) {
ltrs = [];
var i = Math.floor((words.length-10)*Math.random());
while (words[i]!="\n" && i<words.length) ++i;
if (i<words.length-1) {
++i;
while (words[i]!=="\n") {
var c = words[i];
if (ltrs.indexOf(c)===-1) ltrs += c;
++i;
}
}
}
for (var i=0; i<7; ++i) letters.push(ltrs.charCodeAt(i)-97);
shuffleLetters(1);
}
function drawWord(c) {
g.clearRect(0, 0, g.getWidth()-1, 19);
g.setColor(c).setFont("Vector", 20).setFontAlign(0, 0, 0).drawString(word, g.getWidth()/2, 11).flip();
}
function touchHandler(e, x) {
var hex = 0;
var hex_d = 1e6;
for (var i=0; i<7; ++i) {
var d = (x.x-centers[2*i])*(x.x-centers[2*i]) + (x.y-centers[2*i+1])*(x.y-centers[2*i+1]);
if (d < hex_d) {
hex_d = d;
hex = i+1;
}
}
hex = hex%7;
if (word.length <= 15) word += String.fromCharCode(letters[hex]+65);
drawWord(g.theme.fg);
}
function drawScore() {
g.setColor(g.theme.fg).setFont("Vector", 20).setFontAlign(0, 0, 0);
g.clearRect(0, g.getHeight()-22, 60, g.getHeight()-1);
g.clearRect(g.getWidth()-60, g.getHeight()-22, g.getWidth(), g.getHeight()-1);
g.drawString(foundWords.length.toString(), 30, g.getHeight()-11);
g.drawString(score.toString(), g.getWidth()-30, g.getHeight()-11);
}
function wordFound (c) {
word = "";
drawWord(g.theme.fg);
drawScore();
clearInterval(intervalID);
intervalID = -1;
}
function swipeHandler(d, e) {
if (d==-1 && word.length>0) {
word = word.slice(0, -1);
drawWord(g.theme.fg);
}
if (d==1 && word.length>=4) {
drawWord("#00f");
drawWord((checkWord(word.toLowerCase()) ? "#0f0" : "#f00"));
if (intervalID===-1) intervalID = setInterval(wordFound, 800);
}
if (e===1) {
shuffleLetters(0);
drawHive();
drawScore();
drawWord(g.theme.fg);
}
if (e===-1 && foundWords.length>0) showWordList();
}
function showWordList() {
Bangle.removeListener("touch", touchHandler);
Bangle.removeListener("swipe", swipeHandler);
E.showScroller({
h : 20, c : foundWords.length,
draw : (idx, r) => {
g.clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1).setFont("6x8:2");
g.setColor(isPangram(foundWords[idx])?'#0f0':g.theme.fg).drawString(foundWords[idx].toUpperCase(),r.x+10,r.y+4);
},
select : (idx) => {
setInterval(()=> {
E.showScroller();
drawHive();
drawScore();
drawWord(g.theme.fg);
Bangle.on("touch", touchHandler);
Bangle.on("swipe", swipeHandler);
clearInterval();
}, 100);
}
});
}
pickLetters();
drawHive();
drawScore();
Bangle.on("touch", touchHandler);
Bangle.on("swipe", swipeHandler);

BIN
apps/bee/bee_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

74578
apps/bee/bee_words_2of12 Normal file

File diff suppressed because it is too large Load Diff

15
apps/bee/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "bee",
"name": "Bee",
"shortName":"Bee",
"icon": "app.png",
"version":"0.03",
"description": "Spelling bee",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"tags": "game,text",
"storage": [
{"name":"bee.app.js","url":"bee.app.js"},
{"name":"bee.words","url":"bee_words_2of12"},
{"name":"bee.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -7,6 +7,7 @@
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true, "allow_emulator": true,
"screenshots": [{"url":"berlin-clock-screenshot.png"}], "screenshots": [{"url":"berlin-clock-screenshot.png"}],
"storage": [ "storage": [

View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Barometer altitude adjustment setting

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

16
apps/bikespeedo/README.md Normal file
View File

@ -0,0 +1,16 @@
## GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude...
![](Hochrad120px.png)...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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

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

554
apps/bikespeedo/app.js Normal file
View File

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

BIN
apps/bikespeedo/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

View File

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

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Fixed issue with wrong device informations 0.02: Fixed issue with wrong device informations
0.03: Ensure manufacturer:undefined doesn't overflow screen 0.03: Ensure manufacturer:undefined doesn't overflow screen
0.04: Set Bangle.js 2 compatible, show widgets

View File

@ -5,6 +5,7 @@ let menu = {
function showMainMenu() { function showMainMenu() {
menu["< Back"] = () => load(); menu["< Back"] = () => load();
Bangle.drawWidgets();
return E.showMenu(menu); return E.showMenu(menu);
} }
@ -55,5 +56,6 @@ function waitMessage() {
E.showMessage("scanning"); E.showMessage("scanning");
} }
Bangle.loadWidgets();
scan(); scan();
waitMessage(); waitMessage();

Some files were not shown because too many files have changed in this diff Show More