Merge branch 'espruino:master' into master
|
|
@ -10,3 +10,5 @@ _config.yml
|
|||
tests/Layout/bin/tmp.*
|
||||
tests/Layout/testresult.bmp
|
||||
apps.local.json
|
||||
_site
|
||||
.jekyll-cache
|
||||
|
|
|
|||
33
README.md
|
|
@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer.
|
|||
},
|
||||
```
|
||||
|
||||
### Screenshots
|
||||
|
||||
In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { "url":"screenshot.png" } ],`
|
||||
|
||||
To get a screenshot you can:
|
||||
|
||||
* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then
|
||||
right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is
|
||||
unable to read data back from the LCD controller).
|
||||
* Run your code in the emulator and use the screenshot button in the bottom right of the window.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
### Online
|
||||
|
|
@ -214,10 +226,8 @@ and which gives information about the app for the Launcher.
|
|||
"name":"Short Name", // for Bangle.js menu
|
||||
"icon":"*myappid", // for Bangle.js menu
|
||||
"src":"-myappid", // source file
|
||||
"type":"widget/clock/app/bootloader", // optional, default "app"
|
||||
// if this is 'widget' then it's not displayed in the menu
|
||||
// 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
|
||||
"type":"widget/clock/app/bootloader/...", // optional, default "app"
|
||||
// see 'type' in 'metadata.json format' below for more options/info
|
||||
"version":"1.23",
|
||||
// added by BangleApps loader on upload based on metadata.json
|
||||
"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
|
||||
"description": "...", // long description (can contain markdown)
|
||||
"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) -
|
||||
// 'app' - an application
|
||||
// 'clock' - a clock - required for clocks to automatically start
|
||||
// 'widget' - a widget
|
||||
// 'launch' - replacement launcher app
|
||||
// 'bootloader' - code that runs at startup only
|
||||
// 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
|
||||
// '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
|
||||
"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
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
theme: jekyll-theme-minimal
|
||||
theme: jekyll-theme-slate
|
||||
|
|
@ -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});
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Better support for watch themes
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
# Game of 2047pp (2047++)
|
||||
|
||||
Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024.
|
||||
|
||||
Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using
|
||||
the top/bottom button (Bangle 1).
|
||||
|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI="))
|
||||
|
After Width: | Height: | Size: 759 B |
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "2047pp",
|
||||
"name": "2047pp",
|
||||
"shortName":"2047pp",
|
||||
"icon": "app.png",
|
||||
"version":"0.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}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Fullscreen settings.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# 90s Clock
|
||||
|
||||
A watch face in 90s style:
|
||||
|
||||

|
||||
|
||||
Fullscreen mode can be enabled in the settings:
|
||||
|
||||

|
||||
|
||||
|
||||
## Creator
|
||||
- [David Peer](https://github.com/peerdavid)
|
||||
|
|
@ -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"))
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -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();
|
||||
},
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
@ -1,12 +1,34 @@
|
|||
// place your const, vars, functions or classes here
|
||||
|
||||
// special function to handle display switch on
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
if (on) {
|
||||
// call your app function here
|
||||
// If you clear the screen, do Bangle.drawWidgets();
|
||||
// clear the screen
|
||||
g.clear();
|
||||
|
||||
var n = 0;
|
||||
|
||||
// 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();
|
||||
// call your app function here
|
||||
// First draw...
|
||||
draw();
|
||||
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Use the new multiplatform 'Layout' library
|
||||
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
|
||||
|
|
|
|||
|
|
@ -7,21 +7,21 @@ function getFileName(n) {
|
|||
|
||||
function showMenu() {
|
||||
var menu = {
|
||||
"" : { title : "Accel Logger" },
|
||||
"Exit" : function() {
|
||||
"" : { title : /*LANG*/"Accel Logger" },
|
||||
"< Back" : function() {
|
||||
load();
|
||||
},
|
||||
"File No" : {
|
||||
/*LANG*/"File No" : {
|
||||
value : fileNumber,
|
||||
min : 0,
|
||||
max : MAXLOGS,
|
||||
onchange : v => { fileNumber=v; }
|
||||
},
|
||||
"Start" : function() {
|
||||
/*LANG*/"Start" : function() {
|
||||
E.showMenu();
|
||||
startRecord();
|
||||
},
|
||||
"View Logs" : function() {
|
||||
/*LANG*/"View Logs" : function() {
|
||||
viewLogs();
|
||||
},
|
||||
};
|
||||
|
|
@ -29,7 +29,7 @@ function showMenu() {
|
|||
}
|
||||
|
||||
function viewLog(n) {
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
var f = require("Storage").open(getFileName(n), "r");
|
||||
var records = 0, l = "", ll="";
|
||||
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 );
|
||||
|
||||
var menu = {
|
||||
"" : { title : "Log "+n }
|
||||
"" : { title : "Log "+n },
|
||||
"< Back" : () => { viewLogs(); }
|
||||
};
|
||||
menu[records+" Records"] = "";
|
||||
menu[length+" Seconds"] = "";
|
||||
menu["DELETE"] = function() {
|
||||
E.showPrompt("Delete Log "+n).then(ok=>{
|
||||
menu[records+/*LANG*/" Records"] = "";
|
||||
menu[length+/*LANG*/" Seconds"] = "";
|
||||
menu[/*LANG*/"DELETE"] = function() {
|
||||
E.showPrompt(/*LANG*/"Delete Log "+n).then(ok=>{
|
||||
if (ok) {
|
||||
E.showMessage("Erasing...");
|
||||
E.showMessage(/*LANG*/"Erasing...");
|
||||
f.erase();
|
||||
viewLogs();
|
||||
} else viewLog(n);
|
||||
});
|
||||
};
|
||||
menu["< Back"] = function() {
|
||||
viewLogs();
|
||||
};
|
||||
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function viewLogs() {
|
||||
var menu = {
|
||||
"" : { title : "Logs" }
|
||||
"" : { title : /*LANG*/"Logs" },
|
||||
"< Back" : () => { showMenu(); }
|
||||
};
|
||||
|
||||
var hadLogs = false;
|
||||
|
|
@ -67,14 +67,13 @@ function viewLogs() {
|
|||
var f = require("Storage").open(getFileName(i), "r");
|
||||
if (f.readLine()!==undefined) {
|
||||
(function(i) {
|
||||
menu["Log "+i] = () => viewLog(i);
|
||||
menu[/*LANG*/"Log "+i] = () => viewLog(i);
|
||||
})(i);
|
||||
hadLogs = true;
|
||||
}
|
||||
}
|
||||
if (!hadLogs)
|
||||
menu["No Logs Found"] = function(){};
|
||||
menu["< Back"] = function() { showMenu(); };
|
||||
menu[/*LANG*/"No Logs Found"] = function(){};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +82,7 @@ function startRecord(force) {
|
|||
// check for existing file
|
||||
var f = require("Storage").open(getFileName(fileNumber), "r");
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
|
@ -93,14 +92,14 @@ function startRecord(force) {
|
|||
|
||||
var Layout = require("Layout");
|
||||
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", 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", 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...
|
||||
{label:"STOP", cb:()=>{
|
||||
{label:/*LANG*/"STOP", cb:()=>{
|
||||
Bangle.removeListener('accel', accelHandler);
|
||||
showMenu();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "accellog",
|
||||
"name": "Acceleration Logger",
|
||||
"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",
|
||||
"icon": "app.png",
|
||||
"tags": "outdoor",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"aclock.app.js","url":"clock-analog.js"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwYda7dtwAQNmwRB2wQMgO2CIXACJcNCIfYCJYOCCgQRNJQYRM2ADBgwR/CKprRWAKPQWZ0DCIjXLjYREGpYODAQVgCBB3Btj+EAoQAGO4IdCgImDCAwLCAoo4IF4J3DCIPDCIQ4FO4VtwARCAoIRGRgQCBa4IRCKAQRERgOwIIIRDAoOACIoIBwwRHLIqMCFgIRCGQQRIWAYRLYQoREWwTmHO4IRCFgLXHPoi/CbogAFEAIRCWwTpKEwZBCHwK5BCJZEBCJZcCGQTLDCJK/BAQIRKMoaSDOIYAFeQYRMcYRWBXIUAWYPACIq8DagfACJQLCCIYsBU4QRF7B9CAogRGI4QLCAoprIMoZKER5C/DAoShMAo4AGfAQFIACQ="))
|
||||
|
|
@ -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();
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -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
|
||||
*/
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
@ -14,3 +14,16 @@
|
|||
0.13: Alarm widget state now updates when setting/resetting an alarm
|
||||
0.14: Order of 'back' menu item
|
||||
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!
|
||||
|
|
|
|||
|
|
@ -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` → Configure a new alarm
|
||||
- `Repeat` → 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` → Configure a new timer
|
||||
- `Advanced`
|
||||
- `Scheduler settings` → 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` → Enable _all_ disabled alarms & timers
|
||||
- `Disable All` → Disable _all_ enabled alarms & timers
|
||||
- `Delete All` → 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).
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,181 +1,349 @@
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
var alarms = require("Storage").readJSON("alarm.json",1)||[];
|
||||
/*alarms = [
|
||||
{ on : true,
|
||||
hr : 6.5, // hours + minutes/60
|
||||
msg : "Eat chocolate",
|
||||
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
|
||||
rp : true, // repeat
|
||||
as : false, // auto snooze
|
||||
timer : 5, // OPTIONAL - if set, this is a timer and it's the time in minutes
|
||||
// 0 = Sunday (default), 1 = Monday
|
||||
const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
|
||||
const WORKDAYS = 62
|
||||
const WEEKEND = firstDayOfWeek ? 192 : 65;
|
||||
const EVERY_DAY = firstDayOfWeek ? 254 : 127;
|
||||
|
||||
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==");
|
||||
const iconAlarmOff = "\0" + (g.theme.dark
|
||||
? 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;
|
||||
}
|
||||
}
|
||||
];*/
|
||||
|
||||
function formatTime(t) {
|
||||
var hrs = 0|t;
|
||||
var mins = Math.round((t-hrs)*60);
|
||||
return hrs+":"+("0"+mins).substr(-2);
|
||||
return dow;
|
||||
}
|
||||
|
||||
function formatMins(t) {
|
||||
mins = (0|t)%60;
|
||||
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);
|
||||
}
|
||||
// Check the first day of week and update the dow field accordingly.
|
||||
alarms.forEach(alarm => alarm.dow = handleFirstDayOfWeek(alarm.dow));
|
||||
|
||||
function showMainMenu() {
|
||||
const menu = {
|
||||
'': { 'title': 'Alarm/Timer' },
|
||||
/*LANG*/'< Back' : ()=>{load();},
|
||||
/*LANG*/'New Alarm': ()=>editAlarm(-1),
|
||||
/*LANG*/'New Timer': ()=>editTimer(-1)
|
||||
"": { "title": /*LANG*/"Alarms & Timers" },
|
||||
"< Back": () => load(),
|
||||
/*LANG*/"New...": () => showNewMenu()
|
||||
};
|
||||
alarms.forEach((alarm,idx)=>{
|
||||
if (alarm.timer) {
|
||||
txt = /*LANG*/"TIMER "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatMins(alarm.timer);
|
||||
} else {
|
||||
txt = /*LANG*/"ALARM "+(alarm.on?/*LANG*/"on ":/*LANG*/"off ")+formatTime(alarm.hr);
|
||||
if (alarm.rp) txt += /*LANG*/" (repeat)";
|
||||
}
|
||||
menu[txt] = function() {
|
||||
if (alarm.timer) editTimer(idx);
|
||||
else editAlarm(idx);
|
||||
|
||||
alarms.forEach((e, index) => {
|
||||
var label = e.timer
|
||||
? require("time_utils").formatDuration(e.timer)
|
||||
: require("time_utils").formatTime(e.t) + (e.dow > 0 ? (" " + decodeDOW(e)) : "");
|
||||
menu[label] = {
|
||||
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
|
||||
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
|
||||
};
|
||||
});
|
||||
|
||||
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload();
|
||||
return E.showMenu(menu);
|
||||
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editAlarm(alarmIndex) {
|
||||
var newAlarm = alarmIndex<0;
|
||||
var hrs = 12;
|
||||
var mins = 0;
|
||||
var en = true;
|
||||
var repeat = true;
|
||||
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 showNewMenu() {
|
||||
E.showMenu({
|
||||
"": { "title": /*LANG*/"New..." },
|
||||
"< Back": () => showMainMenu(),
|
||||
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
|
||||
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined)
|
||||
});
|
||||
}
|
||||
|
||||
function editTimer(alarmIndex) {
|
||||
var newAlarm = alarmIndex<0;
|
||||
var hrs = 0;
|
||||
var mins = 5;
|
||||
var en = true;
|
||||
if (!newAlarm) {
|
||||
var a = alarms[alarmIndex];
|
||||
mins = (0|a.timer)%60;
|
||||
hrs = 0|(a.timer/60);
|
||||
en = a.on;
|
||||
function showEditAlarmMenu(selectedAlarm, alarmIndex) {
|
||||
var isNew = alarmIndex === undefined;
|
||||
|
||||
var alarm = require("sched").newDefaultAlarm();
|
||||
alarm.dow = handleFirstDayOfWeek(alarm.dow);
|
||||
|
||||
if (selectedAlarm) {
|
||||
Object.assign(alarm, selectedAlarm);
|
||||
}
|
||||
|
||||
var time = require("time_utils").decodeTime(alarm.t);
|
||||
|
||||
const menu = {
|
||||
'': { 'title': /*LANG*/'Timer' },
|
||||
/*LANG*/'Hours': {
|
||||
value: hrs, min : 0, max : 23, wrap : true,
|
||||
onchange: v => hrs=v
|
||||
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
|
||||
"< Back": () => {
|
||||
saveAlarm(alarm, alarmIndex, time);
|
||||
showMainMenu();
|
||||
},
|
||||
/*LANG*/'Minutes': {
|
||||
value: mins, min : 0, max : 59, wrap : true,
|
||||
onchange: v => mins=v
|
||||
/*LANG*/"Hour": {
|
||||
value: time.h,
|
||||
format: v => ("0" + v).substr(-2),
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: v => time.h = v
|
||||
},
|
||||
/*LANG*/'Enabled': {
|
||||
value: en,
|
||||
format: v=>v?/*LANG*/"On":/*LANG*/"Off",
|
||||
onchange: v=>en=v
|
||||
/*LANG*/"Minute": {
|
||||
value: time.m,
|
||||
format: v => ("0" + v).substr(-2),
|
||||
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);
|
||||
var hr = d.getHours() + (d.getMinutes()/60) + (d.getSeconds()/3600);
|
||||
// Save alarm
|
||||
return {
|
||||
on : en,
|
||||
timer : (hrs*60)+mins,
|
||||
hr : hr,
|
||||
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();
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showCustomDaysMenu(dow, dowChangeCallback, originalDow) {
|
||||
const menu = {
|
||||
"": { "title": /*LANG*/"Custom Days" },
|
||||
"< Back": () => dowChangeCallback(dow),
|
||||
};
|
||||
if (!newAlarm) {
|
||||
menu["> Delete"] = function() {
|
||||
alarms.splice(alarmIndex,1);
|
||||
require("Storage").write("alarm.json",JSON.stringify(alarms));
|
||||
|
||||
require("date_utils").dows(firstDayOfWeek).forEach((day, i) => {
|
||||
menu[day] = {
|
||||
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();
|
||||
},
|
||||
/*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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
@ -1,18 +1,30 @@
|
|||
{
|
||||
"id": "alarm",
|
||||
"name": "Default Alarm & Timer",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.15",
|
||||
"description": "Set and respond to alarms and timers",
|
||||
"version": "0.27",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm,widget",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"supports": [ "BANGLEJS", "BANGLEJS2" ],
|
||||
"readme": "README.md",
|
||||
"dependencies": { "scheduler":"type" },
|
||||
"storage": [
|
||||
{"name":"alarm.app.js","url":"app.js"},
|
||||
{"name":"alarm.boot.js","url":"boot.js"},
|
||||
{"name":"alarm.js","url":"alarm.js"},
|
||||
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"alarm.wid.js","url":"widget.js"}
|
||||
{ "name": "alarm.app.js", "url": "app.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" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -1,7 +1,8 @@
|
|||
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);
|
||||
},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();
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Actually upload correct code
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA///t9TmuV3+GJf4AN+ALVgf8BasP/4LVn//4ALUWgJUJBZUDBYJUIBZcP3/nKhEOt/WBZE5r+VKg0KgEVr9V3wLHqtaqt9sALElWAqoABt1QBZNeBYuq0ILCrVUBYulBYVWBYkCBYgABBZ8K1WVBYlABZegKQWqBQlVqALKqWoKQWpBYtWBZeqKRAAB1WABZZSHAANq0ALLKQ6qC1ALLKQ5UEAH4AG"))
|
||||
|
|
@ -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});
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,3 +6,4 @@
|
|||
0.05: Fix handling of message actions
|
||||
0.06: Option to keep messages after a disconnect (default false) (fix #1186)
|
||||
0.07: Include charging state in battery updates to phone
|
||||
0.08: Handling of alarms
|
||||
|
|
|
|||
|
|
@ -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 any messages it has received, or should it delete them?
|
||||
* `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
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
}
|
||||
|
||||
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;
|
||||
global.GB = (event) => {
|
||||
// feed a copy to other handlers if there were any
|
||||
|
|
@ -44,6 +49,40 @@
|
|||
title:event.name||"Call", body:"Incoming call\n"+event.number});
|
||||
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];
|
||||
if (h) h(); else console.log("GB Unknown",event);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"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.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
"" : { "title" : "Android" },
|
||||
"< Back" : back,
|
||||
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
|
||||
"Find Phone" : () => E.showMenu({
|
||||
"" : { "title" : "Find Phone" },
|
||||
/*LANG*/"Find Phone" : () => E.showMenu({
|
||||
"" : { "title" : /*LANG*/"Find Phone" },
|
||||
"< Back" : ()=>E.showMenu(mainmenu),
|
||||
/*LANG*/"On" : _=>gb({t:"findPhone",n:true}),
|
||||
/*LANG*/"Off" : _=>gb({t:"findPhone",n:false}),
|
||||
|
|
@ -24,7 +24,28 @@
|
|||
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);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,3 +9,4 @@
|
|||
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
|
||||
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.08: fixed calendar weeknumber not shortened to two digits
|
||||
|
|
@ -99,7 +99,7 @@ function updateState() {
|
|||
}
|
||||
|
||||
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)
|
||||
|
|
@ -140,7 +140,7 @@ function draw() {
|
|||
g.setFontAlign(0, 0).setFont("Anton").drawString(timeStr, x, y); // draw time
|
||||
if (secondsScreen) {
|
||||
y += 65;
|
||||
var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).substr(-2);
|
||||
var secStr = (secondsWithColon ? ":" : "") + ("0" + date.getSeconds()).slice(-2);
|
||||
if (doColor())
|
||||
g.setColor(0, 0, 1);
|
||||
g.setFont("AntonSmall");
|
||||
|
|
@ -193,7 +193,7 @@ function draw() {
|
|||
if (calWeek || weekDay) {
|
||||
var dowcwStr = "";
|
||||
if (calWeek)
|
||||
dowcwStr = " #" + ("0" + ISO8601calWeek(date)).substring(-2);
|
||||
dowcwStr = " #" + ("0" + ISO8601calWeek(date)).slice(-2);
|
||||
if (weekDay)
|
||||
dowcwStr = require("locale").dow(date, calWeek ? 1 : 0) + dowcwStr; //weekDay e.g. Monday or weekDayShort #<calWeek> e.g. Mon #01
|
||||
else //week #01
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "antonclk",
|
||||
"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.",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))
|
||||
require("heatshrink").decompress(atob("kkkwIEBgf8AYMB//4AgN///ggEf4E/wED+EACQN8C4Pgh4TBh8BCYMAvEcEoWD4AEBnk4gFggPHwAXBj1wgIwB88An/Ah3gg/+gF+gH/+EH8Ef/+ABAPvuAIBgnyCIQjBBAMAJAIIEuAICFgIIBh14BAMB8eAg0Ajk8KAXBKAU4jwDBg+ADoIXBg4NBnxPBEgPAgP8gZaBg//KoKLBKAIEBMQMAA"))
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY="))
|
||||
require("heatshrink").decompress(atob("kUw4MA///xP5gEH/AMBh//4AHBwF4gEDwEHgEB4fw8EAsf/jEAjPh80AhngjnAgcwAIMB5kA50A+cAmfAtnAhnYmc//8zhln/+c4YjBg0w440Bxk38EB/cP/0B//Dwf/+FxwEf8EGIAJGB2BkCnhiB4EPgF//EDFQIpB+HGgOMnkxwFjh8MsEY4YQHn/x//j//8n/wHYItBCAKFBhgKBKAIQBBgIQC4AQCmAQChkD/v8gcA/wCBBoMA7+39kAPwP/WIMP4aYBCAYhCCAkHAYOAA="))
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||
0.05: Add support for bulk importing and exporting tokens
|
||||
0.06: Add spaces to codes for improved readability (thanks @BartS23)
|
||||
0.07: Bangle 2: Improve drag responsiveness and exit on button press
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
const tokenextraheight = 16;
|
||||
var tokendigitsheight = 30;
|
||||
var tokenheight = tokendigitsheight + tokenextraheight;
|
||||
const COUNTER_TRIANGLE_SIZE = 10;
|
||||
const TOKEN_EXTRA_HEIGHT = 16;
|
||||
var TOKEN_DIGITS_HEIGHT = 30;
|
||||
var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT;
|
||||
const PROGRESSBAR_HEIGHT = 3;
|
||||
const IDLE_REPEATS = 1; // when idle, the number of extra timed periods to show before hiding
|
||||
const SETTINGS = "authentiwatch.json";
|
||||
// Hash functions
|
||||
const crypto = require("crypto");
|
||||
const algos = {
|
||||
|
|
@ -8,33 +12,24 @@ const algos = {
|
|||
"SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 },
|
||||
"SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 },
|
||||
};
|
||||
const calculating = "Calculating";
|
||||
const notokens = "No tokens";
|
||||
const notsupported = "Not supported";
|
||||
const CALCULATING = /*LANG*/"Calculating";
|
||||
const NO_TOKENS = /*LANG*/"No tokens";
|
||||
const NOT_SUPPORTED = /*LANG*/"Not supported";
|
||||
|
||||
// sample settings:
|
||||
// {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}}
|
||||
var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}};
|
||||
var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[], misc:{}};
|
||||
if (settings.data ) tokens = settings.data ; /* v0.02 settings */
|
||||
if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */
|
||||
|
||||
// QR Code Text
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret}
|
||||
//
|
||||
// ${algorithm} : one of SHA1 / SHA256 / SHA512
|
||||
// ${digits} : one of 6 / 8
|
||||
// ${period} : one of 30 / 60
|
||||
// ${url} : a domain name "example.com"
|
||||
// ${secret} : the seed code
|
||||
|
||||
function b32decode(seedstr) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, retstr = "";
|
||||
for (i in seedstr) {
|
||||
var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
// RFC4648 Base16/32/64 Data Encodings
|
||||
let buf = 0, bitcount = 0, retstr = "";
|
||||
for (let c of seedstr.toUpperCase()) {
|
||||
if (c == '0') c = 'O';
|
||||
if (c == '1') c = 'I';
|
||||
if (c == '8') c = 'B';
|
||||
c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
|
|
@ -46,195 +41,127 @@ function b32decode(seedstr) {
|
|||
}
|
||||
}
|
||||
}
|
||||
var retbuf = new Uint8Array(retstr.length);
|
||||
for (i in retstr) {
|
||||
let retbuf = new Uint8Array(retstr.length);
|
||||
for (let i in retstr) {
|
||||
retbuf[i] = retstr.charCodeAt(i);
|
||||
}
|
||||
return retbuf;
|
||||
}
|
||||
function do_hmac(key, message, algo) {
|
||||
var a = algos[algo];
|
||||
// RFC2104
|
||||
|
||||
function hmac(key, message, algo) {
|
||||
let a = algos[algo.toUpperCase()];
|
||||
// RFC2104 HMAC
|
||||
if (key.length > a.blksz) {
|
||||
key = a.sha(key);
|
||||
}
|
||||
var istr = new Uint8Array(a.blksz + message.length);
|
||||
var ostr = new Uint8Array(a.blksz + a.retsz);
|
||||
for (var i = 0; i < a.blksz; ++i) {
|
||||
var c = (i < key.length) ? key[i] : 0;
|
||||
let istr = new Uint8Array(a.blksz + message.length);
|
||||
let ostr = new Uint8Array(a.blksz + a.retsz);
|
||||
for (let i = 0; i < a.blksz; ++i) {
|
||||
let c = (i < key.length) ? key[i] : 0;
|
||||
istr[i] = c ^ 0x36;
|
||||
ostr[i] = c ^ 0x5C;
|
||||
}
|
||||
istr.set(message, a.blksz);
|
||||
ostr.set(a.sha(istr), a.blksz);
|
||||
var ret = a.sha(ostr);
|
||||
// RFC4226 dynamic truncation
|
||||
var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||
let ret = a.sha(ostr);
|
||||
// RFC4226 HOTP (dynamic truncation)
|
||||
let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4);
|
||||
return v.getUint32(0) & 0x7FFFFFFF;
|
||||
}
|
||||
function hotp(d, token, dohmac) {
|
||||
var tick;
|
||||
|
||||
function formatOtp(otp, digits) {
|
||||
// add 0 padding
|
||||
let ret = "" + otp % Math.pow(10, digits);
|
||||
while (ret.length < digits) {
|
||||
ret = "0" + ret;
|
||||
}
|
||||
// add a space after every 3rd or 4th digit
|
||||
let re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : ".";
|
||||
return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
||||
}
|
||||
|
||||
function hotp(token) {
|
||||
let d = Date.now();
|
||||
let tick, next;
|
||||
if (token.period > 0) {
|
||||
// RFC6238 - timed
|
||||
var seconds = Math.floor(d.getTime() / 1000);
|
||||
tick = Math.floor(seconds / token.period);
|
||||
tick = Math.floor(Math.floor(d / 1000) / token.period);
|
||||
next = (tick + 1) * token.period * 1000;
|
||||
} else {
|
||||
// RFC4226 - counter
|
||||
tick = -token.period;
|
||||
next = d + 30000;
|
||||
}
|
||||
var msg = new Uint8Array(8);
|
||||
var v = new DataView(msg.buffer);
|
||||
let msg = new Uint8Array(8);
|
||||
let v = new DataView(msg.buffer);
|
||||
v.setUint32(0, tick >> 16 >> 16);
|
||||
v.setUint32(4, tick & 0xFFFFFFFF);
|
||||
var ret = calculating;
|
||||
if (dohmac) {
|
||||
try {
|
||||
var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase());
|
||||
ret = "" + hash % Math.pow(10, token.digits);
|
||||
while (ret.length < token.digits) {
|
||||
ret = "0" + ret;
|
||||
}
|
||||
// add a space after every 3rd or 4th digit
|
||||
var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : ".";
|
||||
ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim();
|
||||
} catch(err) {
|
||||
ret = notsupported;
|
||||
}
|
||||
let ret;
|
||||
try {
|
||||
ret = hmac(b32decode(token.secret), msg, token.algorithm);
|
||||
ret = formatOtp(ret, token.digits);
|
||||
} catch(err) {
|
||||
ret = NOT_SUPPORTED;
|
||||
}
|
||||
return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)};
|
||||
return {hotp:ret, next:next};
|
||||
}
|
||||
|
||||
// Tokens are displayed in three states:
|
||||
// 1. Unselected (state.id<0)
|
||||
// 2. Selected, inactive (no code) (state.id>=0,state.hotp.hotp=="")
|
||||
// 3. Selected, active (code showing) (state.id>=0,state.hotp.hotp!="")
|
||||
var fontszCache = {};
|
||||
var state = {
|
||||
listy: 0,
|
||||
prevcur:0,
|
||||
curtoken:-1,
|
||||
nextTime:0,
|
||||
otp:"",
|
||||
rem:0,
|
||||
hide:0
|
||||
listy:0, // list scroll position
|
||||
id:-1, // current token ID
|
||||
hotp:{hotp:"",next:0}
|
||||
};
|
||||
|
||||
function drawToken(id, r) {
|
||||
var x1 = r.x;
|
||||
var y1 = r.y;
|
||||
var x2 = r.x + r.w - 1;
|
||||
var y2 = r.y + r.h - 1;
|
||||
var adj, lbl, sz;
|
||||
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
|
||||
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
|
||||
lbl = tokens[id].label.substr(0, 10);
|
||||
if (id == state.curtoken) {
|
||||
// current token
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH)
|
||||
.setFont("Vector", tokenextraheight)
|
||||
// center just below top line
|
||||
.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
g.setColor(g.theme.fg)
|
||||
.setBgColor(g.theme.bg);
|
||||
sz = tokendigitsheight;
|
||||
function sizeFont(id, txt, w) {
|
||||
let sz = fontszCache[id];
|
||||
if (!sz) {
|
||||
sz = TOKEN_DIGITS_HEIGHT;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(lbl) > r.w);
|
||||
// center in box
|
||||
g.setFontAlign(0, 0, 0);
|
||||
adj = (y1 + y2) / 2;
|
||||
} while (g.stringWidth(txt) > w);
|
||||
fontszCache[id] = ++sz;
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2)
|
||||
.drawString(lbl, (x1 + x2) / 2, adj, false);
|
||||
if (id == state.curtoken) {
|
||||
if (tokens[id].period > 0) {
|
||||
// timed - draw progress bar
|
||||
let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period);
|
||||
g.fillRect(x1, y2 - 4, xr, y2 - 1);
|
||||
adj = 0;
|
||||
} else {
|
||||
// counter - draw triangle as swipe hint
|
||||
let yc = (y1 + y2) / 2;
|
||||
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
|
||||
adj = 12;
|
||||
}
|
||||
// digits just below label
|
||||
sz = tokendigitsheight;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(state.otp) > (r.w - adj));
|
||||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
|
||||
}
|
||||
// shaded lines top and bottom
|
||||
g.setColor(0.5, 0.5, 0.5)
|
||||
.drawLine(x1, y1, x2, y1)
|
||||
.drawLine(x1, y2, x2, y2)
|
||||
.setClipRect(0, 0, g.getWidth(), g.getHeight());
|
||||
g.setFont("Vector", sz);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
var timerfn = exitApp;
|
||||
var timerdly = 10000;
|
||||
var d = new Date();
|
||||
if (state.curtoken != -1) {
|
||||
var t = tokens[state.curtoken];
|
||||
if (state.otp == calculating) {
|
||||
state.otp = hotp(d, t, true).hotp;
|
||||
}
|
||||
if (d.getTime() > state.nextTime) {
|
||||
if (state.hide == 0) {
|
||||
// auto-hide the current token
|
||||
if (state.curtoken != -1) {
|
||||
state.prevcur = state.curtoken;
|
||||
state.curtoken = -1;
|
||||
tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy;
|
||||
half = n => Math.floor(n / 2);
|
||||
|
||||
function timerCalc() {
|
||||
let timerfn = exitApp;
|
||||
let timerdly = 10000;
|
||||
if (state.id >= 0 && state.hotp.hotp != "") {
|
||||
if (tokens[state.id].period > 0) {
|
||||
// timed HOTP
|
||||
if (state.hotp.next < Date.now()) {
|
||||
if (state.cnt > 0) {
|
||||
state.cnt--;
|
||||
state.hotp = hotp(tokens[state.id]);
|
||||
} else {
|
||||
state.hotp.hotp = "";
|
||||
}
|
||||
state.nextTime = 0;
|
||||
timerdly = 1;
|
||||
timerfn = updateCurrentToken;
|
||||
} else {
|
||||
// time to generate a new token
|
||||
var r = hotp(d, t, state.otp != "");
|
||||
state.nextTime = r.next;
|
||||
state.otp = r.hotp;
|
||||
if (t.period <= 0) {
|
||||
state.hide = 1;
|
||||
}
|
||||
state.hide--;
|
||||
}
|
||||
}
|
||||
state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000));
|
||||
}
|
||||
if (tokens.length > 0) {
|
||||
var drewcur = false;
|
||||
var id = Math.floor(state.listy / tokenheight);
|
||||
var y = id * tokenheight + Bangle.appRect.y - state.listy;
|
||||
while (id < tokens.length && y < Bangle.appRect.y2) {
|
||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight});
|
||||
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
||||
drewcur = true;
|
||||
}
|
||||
id += 1;
|
||||
y += tokenheight;
|
||||
}
|
||||
if (drewcur) {
|
||||
// the current token has been drawn - schedule a redraw
|
||||
if (tokens[state.curtoken].period > 0) {
|
||||
timerdly = (state.otp == calculating) ? 1 : 1000; // timed
|
||||
} else {
|
||||
timerdly = state.nexttime - d.getTime(); // counter
|
||||
}
|
||||
timerfn = draw;
|
||||
if (tokens[state.curtoken].period <= 0) {
|
||||
state.hide = 0;
|
||||
timerdly = 1000;
|
||||
timerfn = updateProgressBar;
|
||||
}
|
||||
} else {
|
||||
// de-select the current token if it is scrolled out of view
|
||||
if (state.curtoken != -1) {
|
||||
state.prevcur = state.curtoken;
|
||||
state.curtoken = -1;
|
||||
// counter HOTP
|
||||
if (state.cnt > 0) {
|
||||
state.cnt--;
|
||||
timerdly = 30000;
|
||||
} else {
|
||||
state.hotp.hotp = "";
|
||||
timerdly = 1;
|
||||
}
|
||||
state.nexttime = 0;
|
||||
timerfn = updateCurrentToken;
|
||||
}
|
||||
} else {
|
||||
g.setFont("Vector", tokendigitsheight)
|
||||
.setFontAlign(0, 0, 0)
|
||||
.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
}
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
|
|
@ -242,97 +169,236 @@ function draw() {
|
|||
state.drawtimer = setTimeout(timerfn, timerdly);
|
||||
}
|
||||
|
||||
function onTouch(zone, e) {
|
||||
if (e) {
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight);
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
if (id != -1) {
|
||||
var y = id * tokenheight - state.listy;
|
||||
if (y < 0) {
|
||||
state.listy += y;
|
||||
y = 0;
|
||||
function updateCurrentToken() {
|
||||
drawToken(state.id);
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
drawProgressBar();
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function drawProgressBar() {
|
||||
let id = state.id;
|
||||
if (id >= 0 && tokens[id].period > 0) {
|
||||
let rem = Math.min(tokens[id].period, Math.floor((state.hotp.next - Date.now()) / 1000));
|
||||
if (rem >= 0) {
|
||||
let y1 = tokenY(id);
|
||||
let y2 = y1 + TOKEN_HEIGHT - 1;
|
||||
if (y2 >= AR.y && y1 <= AR.y2) {
|
||||
// token visible
|
||||
y1 = y2 - PROGRESSBAR_HEIGHT;
|
||||
if (y1 <= AR.y2)
|
||||
{
|
||||
// progress bar visible
|
||||
y2 = Math.min(y2, AR.y2);
|
||||
let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x;
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH)
|
||||
.fillRect(AR.x, y1, xr, y2)
|
||||
.clearRect(xr + 1, y1, AR.x2, y2);
|
||||
}
|
||||
y += tokenheight;
|
||||
if (y > Bangle.appRect.h) {
|
||||
state.listy += (y - Bangle.appRect.h);
|
||||
}
|
||||
state.otp = "";
|
||||
} else {
|
||||
// token not visible
|
||||
state.id = -1;
|
||||
}
|
||||
state.nextTime = 0;
|
||||
state.curtoken = id;
|
||||
state.hide = 2;
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
// id = token ID number (0...)
|
||||
function drawToken(id) {
|
||||
let x1 = AR.x;
|
||||
let y1 = tokenY(id);
|
||||
let x2 = AR.x2;
|
||||
let y2 = y1 + TOKEN_HEIGHT - 1;
|
||||
let lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : "";
|
||||
let adj;
|
||||
g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2));
|
||||
if (id === state.id) {
|
||||
g.setColor(g.theme.fgH)
|
||||
.setBgColor(g.theme.bgH);
|
||||
} else {
|
||||
g.setColor(g.theme.fg)
|
||||
.setBgColor(g.theme.bg);
|
||||
}
|
||||
if (id == state.id && state.hotp.hotp != "") {
|
||||
// small label centered just below top line
|
||||
g.setFont("Vector", TOKEN_EXTRA_HEIGHT)
|
||||
.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
// large label centered in box
|
||||
sizeFont("l" + id, lbl, AR.w);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
adj = half(y1 + y2);
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2)
|
||||
.drawString(lbl, half(x1 + x2), adj, false);
|
||||
if (id == state.id && state.hotp.hotp != "") {
|
||||
adj = 0;
|
||||
if (tokens[id].period <= 0) {
|
||||
// counter - draw triangle as swipe hint
|
||||
let yc = half(y1 + y2);
|
||||
adj = COUNTER_TRIANGLE_SIZE;
|
||||
g.fillPoly([AR.x, yc, AR.x + adj, yc - adj, AR.x + adj, yc + adj]);
|
||||
adj += 2;
|
||||
}
|
||||
// digits just below label
|
||||
x1 = half(x1 + adj + x2);
|
||||
y1 += TOKEN_EXTRA_HEIGHT;
|
||||
if (state.hotp.hotp == CALCULATING) {
|
||||
sizeFont("c", CALCULATING, AR.w - adj);
|
||||
g.drawString(CALCULATING, x1, y1, false)
|
||||
.flip();
|
||||
state.hotp = hotp(tokens[id]);
|
||||
g.clearRect(AR.x + adj, y1, AR.x2, y2);
|
||||
}
|
||||
sizeFont("d" + id, state.hotp.hotp, AR.w - adj);
|
||||
g.drawString(state.hotp.hotp, x1, y1, false);
|
||||
if (tokens[id].period > 0) {
|
||||
drawProgressBar();
|
||||
}
|
||||
}
|
||||
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
|
||||
}
|
||||
|
||||
function changeId(id) {
|
||||
if (id != state.id) {
|
||||
state.hotp.hotp = CALCULATING;
|
||||
let pid = state.id;
|
||||
state.id = id;
|
||||
if (pid >= 0) {
|
||||
drawToken(pid);
|
||||
}
|
||||
if (id >= 0) {
|
||||
drawToken( id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDrag(e) {
|
||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
||||
if (e.dx == 0 && e.dy == 0) return;
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(0, newy);
|
||||
draw();
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (e.b != 0 && e.dy != 0) {
|
||||
let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h));
|
||||
if (state.listy != y) {
|
||||
let id, dy = state.listy - y;
|
||||
state.listy = y;
|
||||
g.setClipRect(AR.x, AR.y, AR.x2, AR.y2)
|
||||
.scroll(0, dy);
|
||||
if (dy > 0) {
|
||||
id = Math.floor((state.listy + dy) / TOKEN_HEIGHT);
|
||||
y = tokenY(id + 1);
|
||||
do {
|
||||
drawToken(id);
|
||||
id--;
|
||||
y -= TOKEN_HEIGHT;
|
||||
} while (y > AR.y);
|
||||
}
|
||||
if (dy < 0) {
|
||||
id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT);
|
||||
y = tokenY(id);
|
||||
while (y < AR.y2) {
|
||||
drawToken(id);
|
||||
id++;
|
||||
y += TOKEN_HEIGHT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e.b == 0) {
|
||||
timerCalc();
|
||||
}
|
||||
}
|
||||
|
||||
function onTouch(zone, e) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (e) {
|
||||
let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT);
|
||||
if (id == state.id || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.id != id) {
|
||||
if (id >= 0) {
|
||||
// scroll token into view if necessary
|
||||
let dy = 0;
|
||||
let y = id * TOKEN_HEIGHT - state.listy;
|
||||
if (y < 0) {
|
||||
dy -= y;
|
||||
y = 0;
|
||||
}
|
||||
y += TOKEN_HEIGHT;
|
||||
if (y > AR.h) {
|
||||
dy -= (y - AR.h);
|
||||
}
|
||||
onDrag({b:1, dy:dy});
|
||||
}
|
||||
changeId(id);
|
||||
}
|
||||
}
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function onSwipe(e) {
|
||||
if (e == 1) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
switch (e) {
|
||||
case 1:
|
||||
exitApp();
|
||||
break;
|
||||
case -1:
|
||||
if (state.id >= 0 && tokens[state.id].period <= 0) {
|
||||
tokens[state.id].period--;
|
||||
require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc});
|
||||
state.hotp.hotp = CALCULATING;
|
||||
drawToken(state.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
||||
tokens[state.curtoken].period--;
|
||||
let newsettings={tokens:tokens,misc:settings.misc};
|
||||
require("Storage").writeJSON("authentiwatch.json", newsettings);
|
||||
state.nextTime = 0;
|
||||
state.otp = "";
|
||||
state.hide = 2;
|
||||
}
|
||||
draw();
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function bangle1Btn(e) {
|
||||
function bangleBtn(e) {
|
||||
state.cnt = IDLE_REPEATS;
|
||||
if (tokens.length > 0) {
|
||||
if (state.curtoken == -1) {
|
||||
state.curtoken = state.prevcur;
|
||||
} else {
|
||||
switch (e) {
|
||||
case -1: state.curtoken--; break;
|
||||
case 1: state.curtoken++; break;
|
||||
}
|
||||
}
|
||||
state.curtoken = Math.max(state.curtoken, 0);
|
||||
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
|
||||
state.listy = state.curtoken * tokenheight;
|
||||
state.listy -= (Bangle.appRect.h - tokenheight) / 2;
|
||||
state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h);
|
||||
state.listy = Math.max(state.listy, 0);
|
||||
var fakee = {};
|
||||
fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y;
|
||||
state.curtoken = -1;
|
||||
state.nextTime = 0;
|
||||
onTouch(0, fakee);
|
||||
} else {
|
||||
draw(); // resets idle timer
|
||||
let id = E.clip(state.id + e, 0, tokens.length - 1);
|
||||
onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))});
|
||||
changeId(id);
|
||||
drawProgressBar();
|
||||
}
|
||||
timerCalc();
|
||||
}
|
||||
|
||||
function exitApp() {
|
||||
if (state.drawtimer) {
|
||||
clearTimeout(state.drawtimer);
|
||||
}
|
||||
Bangle.showLauncher();
|
||||
}
|
||||
|
||||
Bangle.on('touch', onTouch);
|
||||
Bangle.on('drag' , onDrag );
|
||||
Bangle.on('swipe', onSwipe);
|
||||
if (typeof BTN2 == 'number') {
|
||||
setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||
setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50});
|
||||
setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true});
|
||||
if (typeof BTN1 == 'number') {
|
||||
if (typeof BTN2 == 'number' && typeof BTN3 == 'number') {
|
||||
setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true});
|
||||
setWatch(()=>exitApp() , BTN2, {edge:"falling", debounce:50});
|
||||
setWatch(()=>bangleBtn( 1), BTN3, {edge:"rising" , debounce:50, repeat:true});
|
||||
} else {
|
||||
setWatch(()=>exitApp() , BTN1, {edge:"falling", debounce:50});
|
||||
}
|
||||
}
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Clear the screen once, at startup
|
||||
const AR = Bangle.appRect;
|
||||
// draw the initial display
|
||||
g.clear();
|
||||
draw();
|
||||
if (tokens.length > 0) {
|
||||
state.listy = AR.h;
|
||||
onDrag({b:1, dy:AR.h});
|
||||
} else {
|
||||
g.setFont("Vector", TOKEN_DIGITS_HEIGHT)
|
||||
.setFontAlign(0, 0, 0)
|
||||
.drawString(NO_TOKENS, AR.x + half(AR.w), AR.y + half(AR.h), false);
|
||||
}
|
||||
timerCalc();
|
||||
Bangle.drawWidgets();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ body.select div.select,body.export div.export{display:block}
|
|||
body.select div.export,body.export div.select{display:none}
|
||||
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#showqr,body.export div#tokens{display:block}
|
||||
#tokens th,#tokens td{padding:5px}
|
||||
#tokens tr:nth-child(odd){background-color:#ccc}
|
||||
#tokens tr:nth-child(even){background-color:#eee}
|
||||
#tokens tr:nth-child(odd){background-color:#f1f1fc}
|
||||
#tokens tr:nth-child(even){background-color:#fff}
|
||||
#qr-canvas{margin:auto;width:calc(100%-20px);max-width:400px}
|
||||
#advbtn,#scan,#tokenqr table{text-align:center}
|
||||
#edittoken tbody#adv{display:none}
|
||||
|
|
@ -54,9 +54,9 @@ var tokens = settings.tokens;
|
|||
*/
|
||||
function base32clean(val, nows) {
|
||||
var ret = val.replaceAll(/\s+/g, ' ');
|
||||
ret = ret.replaceAll(/0/g, 'O');
|
||||
ret = ret.replaceAll(/1/g, 'I');
|
||||
ret = ret.replaceAll(/8/g, 'B');
|
||||
ret = ret.replaceAll('0', 'O');
|
||||
ret = ret.replaceAll('1', 'I');
|
||||
ret = ret.replaceAll('8', 'B');
|
||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||
if (nows) {
|
||||
ret = ret.replaceAll(/\s+/g, '');
|
||||
|
|
@ -81,9 +81,9 @@ function b32encode(str) {
|
|||
|
||||
function b32decode(seedstr) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, ret = '';
|
||||
for (i in seedstr) {
|
||||
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
var buf = 0, bitcount = 0, ret = '';
|
||||
for (var c of seedstr.toUpperCase()) {
|
||||
c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
|
|
@ -226,15 +226,18 @@ function editToken(id) {
|
|||
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
|
||||
markup += '</td></tr>';
|
||||
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
||||
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||
markup += '<button class="btn" type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||
markup += '</td></tr></table></form>';
|
||||
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||
markup += '<button class="btn" type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||
markup += ' ';
|
||||
if (tokens[id].isnew) {
|
||||
markup += '<button type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||
markup += '<button class="btn" type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||
} else {
|
||||
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>';
|
||||
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||
markup += '<button class="btn" type="button" onclick="showTokenQr()">Show QR</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||
}
|
||||
document.getElementById('edit').innerHTML = markup;
|
||||
document.body.className = 'editing';
|
||||
|
|
@ -304,9 +307,23 @@ function updateTokens() {
|
|||
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
|
||||
};
|
||||
const tokenButton = function(fn, id, label, dir) {
|
||||
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||
return '<button class="btn" type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||
};
|
||||
var markup = '<table><tr><th>';
|
||||
var markup = '';
|
||||
markup += '<div class="select">';
|
||||
markup += '<button class="btn" type="button" onclick="addToken()">Add Token</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="saveTokens()">Save to watch</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||
markup += '</div><div class="export">';
|
||||
markup += '<button class="btn" type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||
markup += ' ';
|
||||
markup += '<button class="btn" type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||
markup += '</div>';
|
||||
markup += '<table><tr><th>';
|
||||
markup += tokenSelect('all');
|
||||
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
|
||||
/* any tokens marked new are cancelled new additions and must be removed */
|
||||
|
|
@ -331,15 +348,6 @@ function updateTokens() {
|
|||
markup += '</td></tr>';
|
||||
}
|
||||
markup += '</table>';
|
||||
markup += '<div class="select">';
|
||||
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
||||
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
||||
markup += '<button type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||
markup += '<button type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||
markup += '</div><div class="export">';
|
||||
markup += '<button type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||
markup += '<button type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||
markup += '</div>';
|
||||
document.getElementById('tokens').innerHTML = markup;
|
||||
document.body.className = 'select';
|
||||
}
|
||||
|
|
@ -405,7 +413,7 @@ class proto3decoder {
|
|||
constructor(str) {
|
||||
this.buf = [];
|
||||
for (let i in str) {
|
||||
this.buf = this.buf.concat(str.charCodeAt(i));
|
||||
this.buf.push(str.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
getVarint() {
|
||||
|
|
@ -487,7 +495,7 @@ function startScan(handler,cancel) {
|
|||
document.body.className = 'scanning';
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video:{facingMode:'environment'}})
|
||||
.then(function(stream){
|
||||
.then(stream => {
|
||||
scanning=true;
|
||||
video.setAttribute('playsinline',true);
|
||||
video.srcObject = stream;
|
||||
|
|
@ -604,7 +612,7 @@ function qrBack() {
|
|||
<div id="scan">
|
||||
<table>
|
||||
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
||||
<tr><td><button type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||
<tr><td><button class="btn" type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
|
@ -613,7 +621,7 @@ function qrBack() {
|
|||
|
||||
<div id="showqr">
|
||||
<table><tr><td id="qrcode"></td></tr><tr><td>
|
||||
<button type="button" onclick="qrBack()">Back</button>
|
||||
<button class="btn" type="button" onclick="qrBack()">Back</button>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"shortName": "AuthWatch",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}],
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Google Authenticator compatible tool.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
|
@ -7,3 +7,4 @@
|
|||
0.07: Update to use Bangle.setUI instead of setWatch
|
||||
0.08: Use theme colors, Layout library
|
||||
0.09: Fix time/date disappearing after fullscreen notification
|
||||
0.10: Use ClockFace library
|
||||
|
|
|
|||
|
|
@ -11,13 +11,9 @@ let locale = require("locale");
|
|||
date.setMonth(1, 3); // februari: months are zero-indexed
|
||||
const localized = locale.date(date, true);
|
||||
locale.dayFirst = /3.*2/.test(localized);
|
||||
|
||||
locale.hasMeridian = false;
|
||||
if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed
|
||||
locale.hasMeridian = (locale.meridian(date)!=="");
|
||||
}
|
||||
locale.hasMeridian = (locale.meridian(date)!=="");
|
||||
}
|
||||
Bangle.loadWidgets();
|
||||
|
||||
function renderBar(l) {
|
||||
if (!this.fraction) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!is12Hour) {
|
||||
|
|
@ -78,31 +48,48 @@ function dateText(date) {
|
|||
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
|
||||
Bangle.setUI("clock");
|
||||
Bangle.on("lcdPower", function(on) {
|
||||
if (on) {
|
||||
draw(true);
|
||||
}
|
||||
});
|
||||
g.reset().clear();
|
||||
Bangle.drawWidgets();
|
||||
draw();
|
||||
const ClockFace = require("ClockFace"),
|
||||
clock = new ClockFace({
|
||||
precision:1,
|
||||
init: function() {
|
||||
const Layout = require("Layout");
|
||||
this.layout = new Layout({
|
||||
type: "v", c: [
|
||||
{
|
||||
type: "h", c: [
|
||||
{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();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "barclock",
|
||||
"name": "Bar Clock",
|
||||
"version": "0.09",
|
||||
"version": "0.10",
|
||||
"description": "A simple digital clock showing seconds as a bar",
|
||||
"icon": "clock-bar.png",
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AE2JAAIKHnc7DyNPp4vRGAwuBGB4sBAAQvSGIovPFqYvHGAYvDGBYsGGhwvGGIQvEGBQnDMYhkNGBAvOvQABqyRTF5GJr4wLFwQACX6IwLsowJLYMrldVGAQvTsoADGBITD0YvDldPF6n+F4gyGGAdP5nMF4KKBGDJZDGI7EBcoOiGAK7DGAQvYRogxEr1Pp9VMAiSBBILBWeJIxCromBMAQwDAAZfTGBQyCxOCGAIvBGIV/F7AwMAAOIp95GAYACFqoyQMAIwGF7QADEQd5FgIADqvGF8DnEAAIvFGIWjF8CFE0QwHAAQudAAK0EGBQuecw3GqpemYIxiCGIa8cF4wwHdTwvJp9/F82jGA9VMQovf5jkHGIwvg4wvIAAgvg5miF9wwNF8QABF9QwF0YuoF4oxCqoulGBAAB42i0QvjGBPMF0gwIFswwHF1IA/AH4A/AH4AL"))
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -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);
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"screenshots": [{"url":"berlin-clock-screenshot.png"}],
|
||||
"storage": [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Barometer altitude adjustment setting
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1,16 @@
|
|||
## GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude...
|
||||
|
||||
...all taken from internal sources.
|
||||
|
||||
#### To speed-up GPS reception it is strongly recommended to upload AGPS data with ["Assisted GPS Update"](https://banglejs.com/apps/?id=assistedgps)
|
||||
|
||||
#### If "CALIB!" is shown on the display or the compass heading differs too much from GPS heading, compass calibration should be done with the ["Navigation Compass" App](https://banglejs.com/apps/?id=magnav)
|
||||
|
||||
Permanently diverging Barometer Altitude values can be compensated in the settings menu.
|
||||
|
||||
Please report bugs to https://github.com/espruino/BangleApps/issues/new?assignees=&labels=bug&template=bangle-bug-report-custom-form.yaml&title=%5BBike+Speedometer%5D+Short+description+of+bug
|
||||
|
||||
**Credits:**<br>
|
||||
Bike Speedometer App by <i>github.com/HilmarSt</i><br>
|
||||
Big parts of the software are based on <i>github.com/espruino/BangleApps/tree/master/apps/speedalt</i><br>
|
||||
Compass and Compass Calibration based on <i>github.com/espruino/BangleApps/tree/master/apps/magnav</i>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgP/ABO/AokfAgf+r4FD3lPBQcZw4FC/nD+4FC/Pn+YFCBIP7GQ4aDEIMDAol/ApQRFuAFEv0/BoQXBx0HAoPgh/nn40C4fwEoP+n/4/BWC/weBBYP5BAM/C4Pz7/7z+f//n7/z5/f//vA4Pv5//AIPv8/n//d//Ou5yBDIOfu58Bz42B+Z8Bz/8AoPgv+/AoP7w0f3IFBnc/5+bL4Oyv/nEYP/+X/mYFC+n8mff8ln+v4vfd7tfsvzvfN7tPtv2vPn6H35vg/f36vX7vj/fz9vvznH+Z3B/0+5/3/l//iDBMwMf+KEBOAPBUoOCj///CNBUQQAEA="))
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
// Bike Speedometer by https://github.com/HilmarSt
|
||||
// Big parts of this software are based on https://github.com/espruino/BangleApps/tree/master/apps/speedalt
|
||||
// Compass and Compass Calibration based on https://github.com/espruino/BangleApps/tree/master/apps/magnav
|
||||
|
||||
const BANGLEJS2 = 1;
|
||||
const screenH = g.getHeight();
|
||||
const screenYstart = 24; // 0..23 for widgets
|
||||
const screenY_Half = screenH / 2 + screenYstart;
|
||||
const screenW = g.getWidth();
|
||||
const screenW_Half = screenW / 2;
|
||||
const fontFactorB2 = 2/3;
|
||||
const colfg=g.theme.fg, colbg=g.theme.bg;
|
||||
const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
|
||||
|
||||
var altiGPS=0, altiBaro=0;
|
||||
var hdngGPS=0, hdngCompass=0, calibrateCompass=false;
|
||||
|
||||
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
||||
var KalmanFilter = (function () {
|
||||
'use strict';
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
function _defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];
|
||||
descriptor.enumerable = descriptor.enumerable || false;
|
||||
descriptor.configurable = true;
|
||||
if ("value" in descriptor) descriptor.writable = true;
|
||||
Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) {
|
||||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||||
return Constructor;
|
||||
}
|
||||
|
||||
/**
|
||||
* KalmanFilter
|
||||
* @class
|
||||
* @author Wouter Bulten
|
||||
* @see {@link http://github.com/wouterbulten/kalmanjs}
|
||||
* @version Version: 1.0.0-beta
|
||||
* @copyright Copyright 2015-2018 Wouter Bulten
|
||||
* @license MIT License
|
||||
* @preserve
|
||||
*/
|
||||
var KalmanFilter =
|
||||
/*#__PURE__*/
|
||||
function () {
|
||||
/**
|
||||
* Create 1-dimensional kalman filter
|
||||
* @param {Number} options.R Process noise
|
||||
* @param {Number} options.Q Measurement noise
|
||||
* @param {Number} options.A State vector
|
||||
* @param {Number} options.B Control vector
|
||||
* @param {Number} options.C Measurement vector
|
||||
* @return {KalmanFilter}
|
||||
*/
|
||||
function KalmanFilter() {
|
||||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
||||
_ref$R = _ref.R,
|
||||
R = _ref$R === void 0 ? 1 : _ref$R,
|
||||
_ref$Q = _ref.Q,
|
||||
Q = _ref$Q === void 0 ? 1 : _ref$Q,
|
||||
_ref$A = _ref.A,
|
||||
A = _ref$A === void 0 ? 1 : _ref$A,
|
||||
_ref$B = _ref.B,
|
||||
B = _ref$B === void 0 ? 0 : _ref$B,
|
||||
_ref$C = _ref.C,
|
||||
C = _ref$C === void 0 ? 1 : _ref$C;
|
||||
|
||||
_classCallCheck(this, KalmanFilter);
|
||||
|
||||
this.R = R; // noise power desirable
|
||||
|
||||
this.Q = Q; // noise power estimated
|
||||
|
||||
this.A = A;
|
||||
this.C = C;
|
||||
this.B = B;
|
||||
this.cov = NaN;
|
||||
this.x = NaN; // estimated signal without noise
|
||||
}
|
||||
/**
|
||||
* Filter a new value
|
||||
* @param {Number} z Measurement
|
||||
* @param {Number} u Control
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
|
||||
_createClass(KalmanFilter, [{
|
||||
key: "filter",
|
||||
value: function filter(z) {
|
||||
var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
|
||||
|
||||
if (isNaN(this.x)) {
|
||||
this.x = 1 / this.C * z;
|
||||
this.cov = 1 / this.C * this.Q * (1 / this.C);
|
||||
} else {
|
||||
// Compute prediction
|
||||
var predX = this.predict(u);
|
||||
var predCov = this.uncertainty(); // Kalman gain
|
||||
|
||||
var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction
|
||||
|
||||
this.x = predX + K * (z - this.C * predX);
|
||||
this.cov = predCov - K * this.C * predCov;
|
||||
}
|
||||
|
||||
return this.x;
|
||||
}
|
||||
/**
|
||||
* Predict next value
|
||||
* @param {Number} [u] Control
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "predict",
|
||||
value: function predict() {
|
||||
var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
||||
return this.A * this.x + this.B * u;
|
||||
}
|
||||
/**
|
||||
* Return uncertainty of filter
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "uncertainty",
|
||||
value: function uncertainty() {
|
||||
return this.A * this.cov * this.A + this.R;
|
||||
}
|
||||
/**
|
||||
* Return the last filtered measurement
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "lastMeasurement",
|
||||
value: function lastMeasurement() {
|
||||
return this.x;
|
||||
}
|
||||
/**
|
||||
* Set measurement noise Q
|
||||
* @param {Number} noise
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "setMeasurementNoise",
|
||||
value: function setMeasurementNoise(noise) {
|
||||
this.Q = noise;
|
||||
}
|
||||
/**
|
||||
* Set the process noise R
|
||||
* @param {Number} noise
|
||||
*/
|
||||
|
||||
}, {
|
||||
key: "setProcessNoise",
|
||||
value: function setProcessNoise(noise) {
|
||||
this.R = noise;
|
||||
}
|
||||
}]);
|
||||
|
||||
return KalmanFilter;
|
||||
}();
|
||||
|
||||
return KalmanFilter;
|
||||
|
||||
}());
|
||||
|
||||
|
||||
//==================================== MAIN ====================================
|
||||
|
||||
var lf = {fix:0,satellites:0};
|
||||
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
||||
var canDraw = 1;
|
||||
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
|
||||
var sec; // actual seconds for testing purposes
|
||||
|
||||
var max = {};
|
||||
max.spd = 0;
|
||||
max.alt = 0;
|
||||
max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
|
||||
|
||||
var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
|
||||
|
||||
var wp = {}; // Waypoint to use for distance from cur position.
|
||||
var SATinView = 0;
|
||||
|
||||
function radians(a) {
|
||||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function distance(a,b){
|
||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
|
||||
// Distance in selected units
|
||||
var d = Math.sqrt(x*x + y*y) * 6371000;
|
||||
d = (d/parseFloat(cfg.dist)).toFixed(2);
|
||||
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
|
||||
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
function drawFix(dat) {
|
||||
|
||||
if (!canDraw) return;
|
||||
|
||||
g.clearRect(0,screenYstart,screenW,screenH);
|
||||
|
||||
var v = '';
|
||||
var u='';
|
||||
|
||||
// Primary Display
|
||||
v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
|
||||
|
||||
// Primary Units
|
||||
u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units;
|
||||
|
||||
drawPrimary(v,u);
|
||||
|
||||
// Secondary Display
|
||||
v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString();
|
||||
|
||||
// Secondary Units
|
||||
u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit;
|
||||
|
||||
drawSecondary(v,u);
|
||||
|
||||
// Time
|
||||
drawTime();
|
||||
|
||||
//Sats
|
||||
if ( dat.age > 10 ) {
|
||||
if ( dat.age > 90 ) dat.age = '>90';
|
||||
drawSats('Age:'+dat.age);
|
||||
}
|
||||
else if (!BANGLEJS2) {
|
||||
drawSats('Sats:'+dat.sats);
|
||||
} else {
|
||||
if (lf.fix) {
|
||||
drawSats('Sats:'+dat.sats);
|
||||
} else {
|
||||
drawSats('View:' + SATinView);
|
||||
}
|
||||
}
|
||||
g.reset();
|
||||
}
|
||||
|
||||
|
||||
function drawClock() {
|
||||
if (!canDraw) return;
|
||||
g.clearRect(0,screenYstart,screenW,screenH);
|
||||
drawTime();
|
||||
g.reset();
|
||||
}
|
||||
|
||||
|
||||
function drawPrimary(n,u) {
|
||||
//if(emulator)console.log("\n1: " + n +" "+ u);
|
||||
var s=40; // Font size
|
||||
var l=n.length;
|
||||
|
||||
if ( l <= 7 ) s=48;
|
||||
if ( l <= 6 ) s=55;
|
||||
if ( l <= 5 ) s=66;
|
||||
if ( l <= 4 ) s=85;
|
||||
if ( l <= 3 ) s=110;
|
||||
|
||||
// X -1=left (default), 0=center, 1=right
|
||||
// Y -1=top (default), 0=center, 1=bottom
|
||||
g.setFontAlign(0,-1); // center, top
|
||||
if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
g.setFontVector(s);
|
||||
g.drawString(n, screenW_Half - 10, screenYstart);
|
||||
|
||||
// Primary Units
|
||||
s = 35; // Font size
|
||||
g.setFontAlign(1,-1,3); // right, top, rotate
|
||||
g.setColor(col1);
|
||||
if (BANGLEJS2) s = 20;
|
||||
g.setFontVector(s);
|
||||
g.drawString(u, screenW - 20, screenYstart + 2);
|
||||
}
|
||||
|
||||
|
||||
function drawSecondary(n,u) {
|
||||
//if(emulator)console.log("2: " + n +" "+ u);
|
||||
|
||||
if (calibrateCompass) hdngCompass = "CALIB!";
|
||||
else hdngCompass +="°";
|
||||
|
||||
g.setFontAlign(0,1);
|
||||
g.setColor(col1);
|
||||
|
||||
g.setFontVector(12).drawString("Altitude GPS / Barometer", screenW_Half - 5, screenY_Half - 10);
|
||||
g.setFontVector(20);
|
||||
g.drawString(n+" "+u+" / "+altiBaro+" "+u, screenW_Half, screenY_Half + 11);
|
||||
|
||||
g.setFontVector(12).drawString("Heading GPS / Compass", screenW_Half - 10, screenY_Half + 26);
|
||||
g.setFontVector(20);
|
||||
g.drawString(hdngGPS+"° / "+hdngCompass, screenW_Half, screenY_Half + 47);
|
||||
}
|
||||
|
||||
|
||||
function drawTime() {
|
||||
var x = 0, y = screenH;
|
||||
g.setFontAlign(-1,1); // left, bottom
|
||||
g.setFont("6x8", 2);
|
||||
|
||||
g.setColor(colbg);
|
||||
g.drawString(time,x+1,y); // clear old time
|
||||
|
||||
time = require("locale").time(new Date(),1);
|
||||
|
||||
g.setColor(colfg); // draw new time
|
||||
g.drawString(time,x+2,y);
|
||||
}
|
||||
|
||||
|
||||
function drawSats(sats) {
|
||||
|
||||
g.setColor(col1);
|
||||
g.setFont("6x8", 2);
|
||||
g.setFontAlign(1,1); //right, bottom
|
||||
g.drawString(sats,screenW,screenH);
|
||||
|
||||
g.setFontVector(18);
|
||||
g.setColor(col1);
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
if ( showMax ) {
|
||||
g.setFontAlign(0,1); //centre, bottom
|
||||
g.drawString('MAX',120,164);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onGPS(fix) {
|
||||
|
||||
if ( emulator ) {
|
||||
fix.fix = 1;
|
||||
fix.speed = Math.random()*30; // calmed by Kalman filter if cfg.spdFilt
|
||||
fix.alt = Math.random()*200 -20; // calmed by Kalman filter if cfg.altFilt
|
||||
fix.lat = 50.59; // google.de/maps/@50.59,8.53,17z
|
||||
fix.lon = 8.53;
|
||||
fix.course = 365;
|
||||
fix.satellites = sec;
|
||||
fix.time = new Date();
|
||||
fix.smoothed = 0;
|
||||
}
|
||||
|
||||
var m;
|
||||
|
||||
var sp = '---';
|
||||
var al = '---';
|
||||
var di = '---';
|
||||
var age = '---';
|
||||
|
||||
if (fix.fix) lf = fix;
|
||||
|
||||
hdngGPS = lf.course;
|
||||
if (isNaN(hdngGPS)) hdngGPS = "---";
|
||||
else if (0 == hdngGPS) hdngGPS = "0?";
|
||||
else hdngGPS = hdngGPS.toFixed(0);
|
||||
|
||||
if (emulator) hdngCompass = hdngGPS;
|
||||
if (emulator) altiBaro = lf.alt.toFixed(0);
|
||||
|
||||
if (lf.fix) {
|
||||
|
||||
if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw);
|
||||
|
||||
// Smooth data
|
||||
if ( lf.smoothed !== 1 ) {
|
||||
if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
|
||||
if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
|
||||
lf.smoothed = 1;
|
||||
if ( max.n <= 15 ) max.n++;
|
||||
}
|
||||
|
||||
|
||||
// Speed
|
||||
if ( cfg.spd == 0 ) {
|
||||
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
|
||||
sp = parseFloat(m[1]);
|
||||
cfg.spd_unit = m[2];
|
||||
}
|
||||
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
||||
|
||||
if ( sp < 10 ) sp = sp.toFixed(1);
|
||||
else sp = Math.round(sp);
|
||||
if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
|
||||
|
||||
// Altitude
|
||||
al = lf.alt;
|
||||
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
|
||||
if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al);
|
||||
|
||||
// Distance to waypoint
|
||||
di = distance(lf,wp);
|
||||
if (isNaN(di)) di = 0;
|
||||
|
||||
// Age of last fix (secs)
|
||||
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
if ( showMax )
|
||||
drawFix({
|
||||
speed:max.spd,
|
||||
sats:lf.satellites,
|
||||
alt:max.alt,
|
||||
alt_units:cfg.alt_unit,
|
||||
age:age,
|
||||
fix:lf.fix
|
||||
}); // Speed and alt maximums
|
||||
else
|
||||
drawFix({
|
||||
speed:sp,
|
||||
sats:lf.satellites,
|
||||
alt:al,
|
||||
alt_units:cfg.alt_unit,
|
||||
age:age,
|
||||
fix:lf.fix
|
||||
}); // Show speed/altitude
|
||||
}
|
||||
}
|
||||
|
||||
function setButtons(){
|
||||
setWatch(_=>load(), BTN1);
|
||||
|
||||
onGPS(lf);
|
||||
}
|
||||
|
||||
|
||||
function updateClock() {
|
||||
if (!canDraw) return;
|
||||
drawTime();
|
||||
g.reset();
|
||||
|
||||
if ( emulator ) {
|
||||
max.spd++; max.alt++;
|
||||
d=new Date(); sec=d.getSeconds();
|
||||
onGPS(lf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =Main Prog
|
||||
|
||||
// Read settings.
|
||||
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
|
||||
|
||||
cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd_unit = 'km/h'; // Displayed speed unit
|
||||
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
|
||||
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
|
||||
cfg.dist = 1000; // Multiplier for distnce unit conversions.
|
||||
cfg.dist_unit = 'km'; // Displayed distnce units
|
||||
cfg.modeA = 1;
|
||||
cfg.primSpd = 1; // 1 = Spd in primary, 0 = Spd in secondary
|
||||
|
||||
cfg.altDiff = cfg.altDiff==undefined?100:cfg.altDiff;
|
||||
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
||||
cfg.altFilt = cfg.altFilt==undefined?false:cfg.altFilt;
|
||||
// console.log("cfg.altDiff: " + cfg.altDiff);
|
||||
// console.log("cfg.spdFilt: " + cfg.spdFilt);
|
||||
// console.log("cfg.altFilt: " + cfg.altFilt);
|
||||
|
||||
if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
|
||||
if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
|
||||
|
||||
function onGPSraw(nmea) {
|
||||
var nofGP = 0, nofBD = 0, nofGL = 0;
|
||||
if (nmea.slice(3,6) == "GSV") {
|
||||
// console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
|
||||
SATinView = nofGP + nofBD + nofGL;
|
||||
} }
|
||||
if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
|
||||
|
||||
function onPressure(dat) {
|
||||
altiBaro = Number(dat.altitude.toFixed(0)) + Number(cfg.altDiff);
|
||||
}
|
||||
|
||||
Bangle.setBarometerPower(1); // needs some time...
|
||||
g.clearRect(0,screenYstart,screenW,screenH);
|
||||
onGPS(lf);
|
||||
Bangle.setGPSPower(1);
|
||||
Bangle.on('GPS', onGPS);
|
||||
Bangle.on('pressure', onPressure);
|
||||
|
||||
Bangle.setCompassPower(1);
|
||||
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
|
||||
if (!CALIBDATA) calibrateCompass = true;
|
||||
function Compass_tiltfixread(O,S){
|
||||
"ram";
|
||||
//console.log(O.x+" "+O.y+" "+O.z);
|
||||
var m = Bangle.getCompass();
|
||||
var g = Bangle.getAccel();
|
||||
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
|
||||
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
|
||||
if (d<0) d+=360;
|
||||
var phi = Math.atan(-g.x/-g.z);
|
||||
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
|
||||
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
|
||||
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
|
||||
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
|
||||
var yh = m.dz*sinphi - m.dx*cosphi;
|
||||
var psi = Math.atan2(yh,xh)*180/Math.PI;
|
||||
if (psi<0) psi+=360;
|
||||
return psi;
|
||||
}
|
||||
var Compass_heading = 0;
|
||||
function Compass_newHeading(m,h){
|
||||
var s = Math.abs(m - h);
|
||||
var delta = (m>h)?1:-1;
|
||||
if (s>=180){s=360-s; delta = -delta;}
|
||||
if (s<2) return h;
|
||||
var hd = h + delta*(1 + Math.round(s/5));
|
||||
if (hd<0) hd+=360;
|
||||
if (hd>360)hd-= 360;
|
||||
return hd;
|
||||
}
|
||||
function Compass_reading() {
|
||||
"ram";
|
||||
var d = Compass_tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
||||
Compass_heading = Compass_newHeading(d,Compass_heading);
|
||||
hdngCompass = Compass_heading.toFixed(0);
|
||||
}
|
||||
if (!calibrateCompass) setInterval(Compass_reading,200);
|
||||
|
||||
setButtons();
|
||||
if (emulator) setInterval(updateClock, 2000);
|
||||
else setInterval(updateClock, 10000);
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
After Width: | Height: | Size: 751 B |
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "bikespeedo",
|
||||
"name": "Bike Speedometer (beta)",
|
||||
"shortName": "Bike Speedometer",
|
||||
"version": "0.02",
|
||||
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"Screenshot.png"}],
|
||||
"type": "app",
|
||||
"tags": "tool,cycling,bicycle,outdoors,sport",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"bikespeedo.app.js","url":"app.js"},
|
||||
{"name":"bikespeedo.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"bikespeedo.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"bikespeedo.json"}]
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
(function(back) {
|
||||
|
||||
let settings = require('Storage').readJSON('bikespeedo.json',1)||{};
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').write('bikespeedo.json',settings);
|
||||
}
|
||||
|
||||
const appMenu = {
|
||||
'': {'title': 'Bike Speedometer'},
|
||||
'< Back': back,
|
||||
'< Load Bike Speedometer': ()=>{load('bikespeedo.app.js');},
|
||||
'Barometer Altitude adjustment' : function() { E.showMenu(altdiffMenu); },
|
||||
'Kalman Filters' : function() { E.showMenu(kalMenu); }
|
||||
};
|
||||
|
||||
const altdiffMenu = {
|
||||
'': { 'title': 'Altitude adjustment' },
|
||||
'< Back': function() { E.showMenu(appMenu); },
|
||||
'Altitude delta': {
|
||||
value: settings.altDiff || 100,
|
||||
min: -200,
|
||||
max: 200,
|
||||
step: 10,
|
||||
onchange: v => {
|
||||
settings.altDiff = v;
|
||||
writeSettings(); }
|
||||
}
|
||||
};
|
||||
|
||||
const kalMenu = {
|
||||
'': {'title': 'Kalman Filters'},
|
||||
'< Back': function() { E.showMenu(appMenu); },
|
||||
'Speed' : {
|
||||
value : settings.spdFilt,
|
||||
format : v => v?"On":"Off",
|
||||
onchange : () => { settings.spdFilt = !settings.spdFilt; writeSettings(); }
|
||||
},
|
||||
'Altitude' : {
|
||||
value : settings.altFilt,
|
||||
format : v => v?"On":"Off",
|
||||
onchange : () => { settings.altFilt = !settings.altFilt; writeSettings(); }
|
||||
}
|
||||
};
|
||||
|
||||
E.showMenu(appMenu);
|
||||
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fixed issue with wrong device informations
|
||||
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
||||
0.04: Set Bangle.js 2 compatible, show widgets
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ let menu = {
|
|||
|
||||
function showMainMenu() {
|
||||
menu["< Back"] = () => load();
|
||||
Bangle.drawWidgets();
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
|
|
@ -55,5 +56,6 @@ function waitMessage() {
|
|||
E.showMessage("scanning");
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
scan();
|
||||
waitMessage();
|
||||
|
|
|
|||