Merge remote-tracking branch 'upstream/master'

master
Fredrik Lautrup 2020-04-20 18:47:33 +02:00
commit d1bb0d34c6
70 changed files with 1996 additions and 258 deletions

View File

@ -202,6 +202,11 @@ and which gives information about the app for the Launcher.
"files:"file1,file2,file3", "files:"file1,file2,file3",
// added by BangleApps loader on upload - lists all files // added by BangleApps loader on upload - lists all files
// that belong to the app so it can be deleted // that belong to the app so it can be deleted
"data":"appid.data.json,appid.data?.json;appidStorageFile,appidStorageFile*"
// added by BangleApps loader on upload - lists files that
// the app might write, so they can be deleted on uninstall
// typically these files are not uploaded, but created by the app
// these can include '*' or '?' wildcards
} }
``` ```
@ -240,16 +245,24 @@ and which gives information about the app for the Launcher.
"evaluate":true // if supplied, data isn't quoted into a String before upload "evaluate":true // if supplied, data isn't quoted into a String before upload
// (eg it's evaluated as JS) // (eg it's evaluated as JS)
}, },
]
"data": [ // list of files the app writes to
{"name":"appid.data.json", // filename used in storage
"storageFile":true // if supplied, file is treated as storageFile
},
{"wildcard":"appid.data.*" // wildcard of filenames used in storage
}, // this is mutually exclusive with using "name"
],
"sortorder" : 0, // optional - choose where in the list this goes. "sortorder" : 0, // optional - choose where in the list this goes.
// this should only really be used to put system // this should only really be used to put system
// stuff at the top // stuff at the top
]
} }
``` ```
* name, icon and description present the app in the app loader. * name, icon and description present the app in the app loader.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty. * tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
* storage is used to identify the app files and how to handle them * storage is used to identify the app files and how to handle them
* data is used to clean up files when the app is uninstalled
### `apps.json`: `custom` element ### `apps.json`: `custom` element
@ -335,10 +348,10 @@ Example `settings.js`
```js ```js
// make sure to enclose the function in parentheses // make sure to enclose the function in parentheses
(function(back) { (function(back) {
let settings = require('Storage').readJSON('app.settings.json',1)||{}; let settings = require('Storage').readJSON('app.json',1)||{};
function save(key, value) { function save(key, value) {
settings[key] = value; settings[key] = value;
require('Storage').write('app.settings.json',settings); require('Storage').write('app.json',settings);
} }
const appMenu = { const appMenu = {
'': {'title': 'App Settings'}, '': {'title': 'App Settings'},
@ -351,19 +364,20 @@ Example `settings.js`
E.showMenu(appMenu) E.showMenu(appMenu)
}) })
``` ```
In this example the app needs to add both `app.settings.js` and In this example the app needs to add `app.settings.js` to `storage` in `apps.json`.
`app.settings.json` to `apps.json`: It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled.
```json ```json
{ "id": "app", { "id": "app",
... ...
"storage": [ "storage": [
... ...
{"name":"app.settings.js","url":"settings.js"}, {"name":"app.settings.js","url":"settings.js"},
{"name":"app.settings.json","content":"{}"} ],
"data": [
{"name":"app.json"}
] ]
}, },
``` ```
That way removing the app also cleans up `app.settings.json`.
## Coding hints ## Coding hints

143
apps.json
View File

@ -78,7 +78,7 @@
{ "id": "welcome", { "id": "welcome",
"name": "Welcome", "name": "Welcome",
"icon": "app.png", "icon": "app.png",
"version":"0.07", "version":"0.08",
"description": "Appears at first boot and explains how to use Bangle.js", "description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome", "tags": "start,welcome",
"allow_emulator":true, "allow_emulator":true,
@ -86,8 +86,10 @@
{"name":"welcome.boot.js","url":"boot.js"}, {"name":"welcome.boot.js","url":"boot.js"},
{"name":"welcome.app.js","url":"app.js"}, {"name":"welcome.app.js","url":"app.js"},
{"name":"welcome.settings.js","url":"settings.js"}, {"name":"welcome.settings.js","url":"settings.js"},
{"name":"welcome.settings.json","url":"settings-default.json","evaluate":true},
{"name":"welcome.img","url":"app-icon.js","evaluate":true} {"name":"welcome.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"welcome.json"}
] ]
}, },
{ "id": "gbridge", { "id": "gbridge",
@ -120,13 +122,12 @@
{ "id": "setting", { "id": "setting",
"name": "Settings", "name": "Settings",
"icon": "settings.png", "icon": "settings.png",
"version":"0.16", "version":"0.18",
"description": "A menu for setting up Bangle.js", "description": "A menu for setting up Bangle.js",
"tags": "tool,system", "tags": "tool,system",
"storage": [ "storage": [
{"name":"setting.app.js","url":"settings.js"}, {"name":"setting.app.js","url":"settings.js"},
{"name":"setting.boot.js","url":"boot.js"}, {"name":"setting.boot.js","url":"boot.js"},
{"name":"setting.json","url":"settings-default.json","evaluate":true},
{"name":"setting.img","url":"settings-icon.js","evaluate":true} {"name":"setting.img","url":"settings-icon.js","evaluate":true}
], ],
"sortorder" : -2 "sortorder" : -2
@ -135,16 +136,18 @@
"name": "Default Alarm", "name": "Default Alarm",
"shortName":"Alarms", "shortName":"Alarms",
"icon": "app.png", "icon": "app.png",
"version":"0.06", "version":"0.07",
"description": "Set and respond to alarms", "description": "Set and respond to alarms",
"tags": "tool,alarm,widget", "tags": "tool,alarm,widget",
"storage": [ "storage": [
{"name":"alarm.app.js","url":"app.js"}, {"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.boot.js","url":"boot.js"}, {"name":"alarm.boot.js","url":"boot.js"},
{"name":"alarm.js","url":"alarm.js"}, {"name":"alarm.js","url":"alarm.js"},
{"name":"alarm.json","content":"[]"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true}, {"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"} {"name":"alarm.wid.js","url":"widget.js"}
],
"data": [
{"name":"alarm.json"}
] ]
}, },
{ "id": "wclock", { "id": "wclock",
@ -235,7 +238,7 @@
{ "id": "compass", { "id": "compass",
"name": "Compass", "name": "Compass",
"icon": "compass.png", "icon": "compass.png",
"version":"0.01", "version":"0.02",
"description": "Simple compass that points North", "description": "Simple compass that points North",
"tags": "tool,outdoors", "tags": "tool,outdoors",
"storage": [ "storage": [
@ -280,29 +283,47 @@
{ "id": "gpsrec", { "id": "gpsrec",
"name": "GPS Recorder", "name": "GPS Recorder",
"icon": "app.png", "icon": "app.png",
"version":"0.07", "version":"0.08",
"interface": "interface.html", "interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background", "description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget", "tags": "tool,outdoors,gps,widget",
"storage": [ "storage": [
{"name":"gpsrec.app.js","url":"app.js"}, {"name":"gpsrec.app.js","url":"app.js"},
{"name":"gpsrec.json","url":"app-settings.json","evaluate":true},
{"name":"gpsrec.img","url":"app-icon.js","evaluate":true}, {"name":"gpsrec.img","url":"app-icon.js","evaluate":true},
{"name":"gpsrec.wid.js","url":"widget.js"} {"name":"gpsrec.wid.js","url":"widget.js"}
],
"data": [
{"name":"gpsrec.json"},
{"wildcard":".gpsrc?","storageFile": true}
]
},
{ "id": "gpsnav",
"name": "GPS Navigation",
"icon": "icon.png",
"version":"0.01",
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording",
"tags": "tool,outdoors,gps",
"storage": [
{"name":"gpsnav.app.js","url":"app.js"},
{"name":"waypoints.json","url":"waypoints.json","evaluate":false},
{"name":"gpsnav.img","url":"app-icon.js","evaluate":true}
] ]
}, },
{ "id": "heart", { "id": "heart",
"name": "Heart Rate Recorder", "name": "Heart Rate Recorder",
"icon": "app.png", "icon": "app.png",
"version":"0.01", "version":"0.02",
"interface": "interface.html", "interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background", "description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget", "tags": "tool,health,widget",
"storage": [ "storage": [
{"name":"heart.app.js","url":"app.js"}, {"name":"heart.app.js","url":"app.js"},
{"name":"heart.json","url":"app-settings.json","evaluate":true},
{"name":"heart.img","url":"app-icon.js","evaluate":true}, {"name":"heart.img","url":"app-icon.js","evaluate":true},
{"name":"heart.wid.js","url":"widget.js"} {"name":"heart.wid.js","url":"widget.js"}
],
"data": [
{"name":"heart.json"},
{"wildcard":".heart?","storageFile": true}
] ]
}, },
{ "id": "slevel", { "id": "slevel",
@ -319,7 +340,7 @@
{ "id": "files", { "id": "files",
"name": "App Manager", "name": "App Manager",
"icon": "files.png", "icon": "files.png",
"version":"0.02", "version":"0.03",
"description": "Show currently installed apps, free space, and allow their deletion from the watch", "description": "Show currently installed apps, free space, and allow their deletion from the watch",
"tags": "tool,system,files", "tags": "tool,system,files",
"storage": [ "storage": [
@ -342,14 +363,16 @@
"name": "Battery Level Widget (with percentage)", "name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget", "shortName": "Battery Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.09", "version":"0.11",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery", "tags": "widget,battery",
"type":"widget", "type":"widget",
"storage": [ "storage": [
{"name":"widbatpc.wid.js","url":"widget.js"}, {"name":"widbatpc.wid.js","url":"widget.js"},
{"name":"widbatpc.settings.js","url":"settings.js"}, {"name":"widbatpc.settings.js","url":"settings.js"}
{"name":"widbatpc.settings.json","content": "{}"} ],
"data": [
{"name":"widbatpc.json"}
] ]
}, },
{ "id": "widbt", { "id": "widbt",
@ -517,20 +540,22 @@
"id": "ncstart", "id": "ncstart",
"name": "NCEU Startup", "name": "NCEU Startup",
"icon": "start.png", "icon": "start.png",
"version":"0.04", "version":"0.05",
"description": "NodeConfEU 2019 'First Start' Sequence", "description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome", "tags": "start,welcome",
"storage": [ "storage": [
{"name":"ncstart.app.js","url":"start.js"}, {"name":"ncstart.app.js","url":"start.js"},
{"name":"ncstart.boot.js","url":"boot.js"}, {"name":"ncstart.boot.js","url":"boot.js"},
{"name":"ncstart.settings.js","url":"settings.js"}, {"name":"ncstart.settings.js","url":"settings.js"},
{"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true},
{"name":"ncstart.img","url":"start-icon.js","evaluate":true}, {"name":"ncstart.img","url":"start-icon.js","evaluate":true},
{"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
{"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
{"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true}, {"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true},
{"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true}, {"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true},
{"name":"nc-tf.img","url":"start-tf.js","evaluate":true} {"name":"nc-tf.img","url":"start-tf.js","evaluate":true}
],
"data": [
{"name":"ncstart.json"}
] ]
}, },
{ "id": "ncfrun", { "id": "ncfrun",
@ -890,7 +915,7 @@
{ "id": "wohrm", { "id": "wohrm",
"name": "Workout HRM", "name": "Workout HRM",
"icon": "app.png", "icon": "app.png",
"version":"0.06", "version":"0.07",
"readme": "README.md", "readme": "README.md",
"description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.", "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
"tags": "hrm,workout", "tags": "hrm,workout",
@ -1018,7 +1043,7 @@
{ "id": "astrocalc", { "id": "astrocalc",
"name": "Astrocalc", "name": "Astrocalc",
"icon": "astrocalc.png", "icon": "astrocalc.png",
"version":"0.01", "version":"0.02",
"description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
"tags": "app,sun,moon,cycles,tool,outdoors", "tags": "app,sun,moon,cycles,tool,outdoors",
"allow_emulator":true, "allow_emulator":true,
@ -1143,7 +1168,7 @@
"name": "Chrono Widget", "name": "Chrono Widget",
"shortName":"Chrono Widget", "shortName":"Chrono Widget",
"icon": "app.png", "icon": "app.png",
"version":"0.01", "version":"0.02",
"description": "Chronometer (timer) which runs as widget.", "description": "Chronometer (timer) which runs as widget.",
"tags": "tools,widget", "tags": "tools,widget",
"readme": "README.md", "readme": "README.md",
@ -1222,7 +1247,7 @@
"name": "Numerals Clock", "name": "Numerals Clock",
"shortName": "Numerals Clock", "shortName": "Numerals Clock",
"icon": "numerals.png", "icon": "numerals.png",
"version":"0.03", "version":"0.04",
"description": "A simple big numerals clock", "description": "A simple big numerals clock",
"tags": "numerals,clock", "tags": "numerals,clock",
"type":"clock", "type":"clock",
@ -1230,8 +1255,10 @@
"storage": [ "storage": [
{"name":"numerals.app.js","url":"numerals.app.js"}, {"name":"numerals.app.js","url":"numerals.app.js"},
{"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, {"name":"numerals.img","url":"numerals-icon.js","evaluate":true},
{"name":"numerals.settings.js","url":"numerals.settings.js"}, {"name":"numerals.settings.js","url":"numerals.settings.js"}
{"name":"numerals.json","url":"numerals-default.json","evaluate":true} ],
"data":[
{"name":"numerals.json"}
] ]
}, },
{ "id": "bledetect", { "id": "bledetect",
@ -1264,8 +1291,8 @@
"name": "Calculator", "name": "Calculator",
"shortName":"Calculator", "shortName":"Calculator",
"icon": "calculator.png", "icon": "calculator.png",
"version":"0.01", "version":"0.02",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"tags": "app,tool", "tags": "app,tool",
"storage": [ "storage": [
{"name":"calculator.app.js","url":"app.js"}, {"name":"calculator.app.js","url":"app.js"},
@ -1294,6 +1321,72 @@
} }
] ]
}, },
{
"id": "banglerun",
"name": "BangleRun",
"shortName": "BangleRun",
"icon": "banglerun.png",
"version": "0.01",
"description": "An app for running sessions.",
"tags": "run,running,fitness,outdoors",
"allow_emulator": false,
"storage": [
{
"name": "banglerun.app.js",
"url": "app.js"
},
{
"name": "banglerun.img",
"url": "app-icon.js",
"evaluate": true
}
]
},
{
"id": "metronome",
"name": "Metronome",
"icon": "metronome_icon.png",
"version": "0.03",
"description": "Makes the watch blinking and vibrating with a given rate",
"tags": "tool",
"allow_emulator": true,
"storage": [
{
"name": "metronome.app.js",
"url": "metronome.js"
},
{
"name": "metronome.img",
"url": "metronome-icon.js",
"evaluate": true
}
]
},
{ "id": "blackjack",
"name": "Black Jack game",
"shortName":"Black Jack game",
"icon": "blackjack.png",
"version":"0.01",
"description": "Simple implementation of card game Black Jack",
"tags": "game",
"allow_emulator":true,
"storage": [
{"name":"blackjack.app.js","url":"blackjack.app.js"},
{"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true}
]
},
{ "id": "hidcam",
"name": "Camera shutter",
"shortName":"Cam shutter",
"icon": "app.png",
"version":"0.01",
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
"tags": "tools",
"storage": [
{"name":"hidcam.app.js","url":"app.js"},
{"name":"hidcam.img","url":"app-icon.js","evaluate":true}
]
},
{ {
"id": "rclock", "id": "rclock",
"name": "Round clock with seconds, minutes and date", "name": "Round clock with seconds, minutes and date",

View File

@ -4,3 +4,4 @@
0.04: Tweaks for variable size widget system 0.04: Tweaks for variable size widget system
0.05: Add alarm.boot.js and move code from the bootloader 0.05: Add alarm.boot.js and move code from the bootloader
0.06: Change 'New Alarm' to 'Save', allow Deletion of Alarms 0.06: Change 'New Alarm' to 'Save', allow Deletion of Alarms
0.07: Don't overwrite existing settings on app update

View File

@ -1 +1,2 @@
0.01: Create astrocalc app 0.01: Create astrocalc app
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start

View File

@ -1,8 +1,18 @@
/** /**
* BangleJS ASTROCALC
*
* Inspired by: https://www.timeanddate.com * Inspired by: https://www.timeanddate.com
*
* Original Author: Paul Cockrell https://github.com/paulcockrell
* Created: April 2020
*
* Calculate the Sun and Moon positions based on watch GPS and display graphically
*/ */
const SunCalc = require("suncalc.js"); const SunCalc = require("suncalc.js");
const storage = require("Storage");
const LAST_GPS_FILE = "astrocalc.gps.json";
let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null);
function drawMoon(phase, x, y) { function drawMoon(phase, x, y) {
const moonImgFiles = [ const moonImgFiles = [
@ -296,22 +306,49 @@ function indexPageMenu(gps) {
return E.showMenu(menu); return E.showMenu(menu);
} }
function getCenterStringX(str) {
return (g.getWidth() - g.stringWidth(str)) / 2;
}
/** /**
* GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page
*/ */
function drawGPSWaitPage() { function drawGPSWaitPage() {
const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")) const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="));
const str1 = "Astrocalc v0.02";
const str2 = "Locating GPS";
const str3 = "Please wait...";
g.clear(); g.clear();
g.drawImage(img, 100, 50); g.drawImage(img, 100, 50);
g.setFont("6x8", 1); g.setFont("6x8", 1);
g.drawString("Astrocalc v0.01", 80, 105); g.drawString(str1, getCenterStringX(str1), 105);
g.drawString("Locating GPS", 85, 140); g.drawString(str2, getCenterStringX(str2), 140);
g.drawString("Please wait...", 80, 155); g.drawString(str3, getCenterStringX(str3), 155);
if (lastGPS) {
lastGPS = JSON.parse(lastGPS);
lastGPS.time = new Date();
const str4 = "Press Button 3 to use last GPS";
g.setColor("#d32e29");
g.fillRect(0, 190, g.getWidth(), 215);
g.setColor("#ffffff");
g.drawString(str4, getCenterStringX(str4), 200);
setWatch(() => {
clearWatch();
Bangle.setGPSPower(0);
m = indexPageMenu(lastGPS);
}, BTN3, {repeat: false});
}
g.flip(); g.flip();
const DEBUG = false; const DEBUG = false;
if (DEBUG) { if (DEBUG) {
clearWatch();
const gps = { const gps = {
"lat": 56.45783133333, "lat": 56.45783133333,
"lon": -3.02188583333, "lon": -3.02188583333,
@ -330,7 +367,10 @@ function drawGPSWaitPage() {
Bangle.on('GPS', (gps) => { Bangle.on('GPS', (gps) => {
if (gps.fix === 0) return; if (gps.fix === 0) return;
clearWatch();
if (isNaN(gps.course)) gps.course = 0;
require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps));
Bangle.setGPSPower(0); Bangle.setGPSPower(0);
Bangle.buzz(); Bangle.buzz();
Bangle.setLCDPower(true); Bangle.setLCDPower(true);

1
apps/banglerun/ChangeLog Executable file
View File

@ -0,0 +1 @@
0.01: First release

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwMB/4ACx4ED/0DApP8AqAXB84GDg/DAgXj/+DCAUABgIFB4EAv4FCwEAj0PAoJPBgwFEgEfDgMOAoM/AoMegFAAoP8jkA8F/AoM8gP4DgP4nBvD/F4KQfwuAFE+A/CAoPgAofx8A/CKYRwELIIFDLII6BAoZSBLIYeC/0BwAFDgfAGAQFBHgf8g4BBIIUH/wFBSYMPAoXwAog/Bj4FEv4FDDQQCBQoQFCZYYFi/6KE/+P/4A="))

314
apps/banglerun/app.js Normal file
View File

@ -0,0 +1,314 @@
/** Global constants */
const DEG_TO_RAD = Math.PI / 180;
const EARTH_RADIUS = 6371008.8;
/** Utilities for handling vectors */
class Vector {
static magnitude(a) {
let sum = 0;
for (const key of Object.keys(a)) {
sum += a[key] * a[key];
}
return Math.sqrt(sum);
}
static add(a, b) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] + b[key];
}
return result;
}
static sub(a, b) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] - b[key];
}
return result;
}
static multiplyScalar(a, x) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] * x;
}
return result;
}
static divideScalar(a, x) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] / x;
}
return result;
}
}
/** Interquartile range filter, to detect outliers */
class IqrFilter {
constructor(size, threshold) {
const q = Math.floor(size / 4);
this._buffer = [];
this._size = 4 * q + 2;
this._i1 = q;
this._i3 = 3 * q + 1;
this._threshold = threshold;
}
isReady() {
return this._buffer.length === this._size;
}
isOutlier(point) {
let result = true;
if (this._buffer.length === this._size) {
result = false;
for (const key of Object.keys(point)) {
const data = this._buffer.map(item => item[key]);
data.sort((a, b) => (a - b) / Math.abs(a - b));
const q1 = data[this._i1];
const q3 = data[this._i3];
const iqr = q3 - q1;
const lower = q1 - this._threshold * iqr;
const upper = q3 + this._threshold * iqr;
if (point[key] < lower || point[key] > upper) {
result = true;
break;
}
}
}
this._buffer.push(point);
this._buffer = this._buffer.slice(-this._size);
return result;
}
}
/** Process GPS data */
class Gps {
constructor() {
this._lastCall = Date.now();
this._lastValid = 0;
this._coords = null;
this._filter = new IqrFilter(10, 1.5);
this._shift = { x: 0, y: 0, z: 0 };
}
isReady() {
return this._filter.isReady();
}
getDistance(gps) {
const time = Date.now();
const interval = (time - this._lastCall) / 1000;
this._lastCall = time;
if (!gps.fix) {
return { t: interval, d: 0 };
}
const p = gps.lat * DEG_TO_RAD;
const q = gps.lon * DEG_TO_RAD;
const coords = {
x: EARTH_RADIUS * Math.sin(p) * Math.cos(q),
y: EARTH_RADIUS * Math.sin(p) * Math.sin(q),
z: EARTH_RADIUS * Math.cos(p),
};
if (!this._coords) {
this._coords = coords;
this._lastValid = time;
return { t: interval, d: 0 };
}
const ds = Vector.sub(coords, this._coords);
const dt = (time - this._lastValid) / 1000;
const v = Vector.divideScalar(ds, dt);
if (this._filter.isOutlier(v)) {
return { t: interval, d: 0 };
}
this._shift = Vector.add(this._shift, ds);
const length = Vector.magnitude(this._shift);
const remainder = length % 10;
const distance = length - remainder;
this._coords = coords;
this._lastValid = time;
if (distance > 0) {
this._shift = Vector.multiplyScalar(this._shift, remainder / length);
}
return { t: interval, d: distance };
}
}
/** Process step counter data */
class Step {
constructor(size) {
this._buffer = [];
this._size = size;
}
getCadence() {
this._buffer.push(Date.now() / 1000);
this._buffer = this._buffer.slice(-this._size);
const interval = this._buffer[this._buffer.length - 1] - this._buffer[0];
return interval ? Math.round(60 * (this._buffer.length - 1) / interval) : 0;
}
}
const gps = new Gps();
const step = new Step(10);
let totDist = 0;
let totTime = 0;
let totSteps = 0;
let speed = 0;
let cadence = 0;
let heartRate = 0;
let gpsReady = false;
let hrmReady = false;
let running = false;
function formatClock(date) {
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
}
function formatDistance(m) {
return ('0' + (m / 1000).toFixed(2) + ' km').substr(-7);
}
function formatTime(s) {
const hrs = Math.floor(s / 3600);
const min = Math.floor(s / 60);
const sec = Math.floor(s % 60);
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
function formatSpeed(kmh) {
if (kmh <= 0.6) {
return `__'__"`;
}
const skm = 3600 / kmh;
const min = Math.floor(skm / 60);
const sec = Math.floor(skm % 60);
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
}
function drawBackground() {
g.setColor(running ? 0x00E0 : 0x0000);
g.fillRect(0, 30, 240, 240);
g.setColor(0xFFFF);
g.setFontAlign(0, -1, 0);
g.setFont('6x8', 2);
g.drawString('DISTANCE', 120, 50);
g.drawString('TIME', 60, 100);
g.drawString('PACE', 180, 100);
g.drawString('STEPS', 60, 150);
g.drawString('STP/m', 180, 150);
g.drawString('SPEED', 40, 200);
g.drawString('HEART', 120, 200);
g.drawString('CADENCE', 200, 200);
}
function draw() {
const totSpeed = totTime ? 3.6 * totDist / totTime : 0;
const totCadence = totTime ? Math.round(60 * totSteps / totTime) : 0;
g.setColor(running ? 0x00E0 : 0x0000);
g.fillRect(0, 30, 240, 50);
g.fillRect(0, 70, 240, 100);
g.fillRect(0, 120, 240, 150);
g.fillRect(0, 170, 240, 200);
g.fillRect(0, 220, 240, 240);
g.setFont('6x8', 2);
g.setFontAlign(-1, -1, 0);
g.setColor(gpsReady ? 0x07E0 : 0xF800);
g.drawString(' GPS', 6, 30);
g.setFontAlign(1, -1, 0);
g.setColor(0xFFFF);
g.drawString(formatClock(new Date()), 234, 30);
g.setFontAlign(0, -1, 0);
g.setFontVector(20);
g.drawString(formatDistance(totDist), 120, 70);
g.drawString(formatTime(totTime), 60, 120);
g.drawString(formatSpeed(totSpeed), 180, 120);
g.drawString(totSteps, 60, 170);
g.drawString(totCadence, 180, 170);
g.setFont('6x8', 2);
g.drawString(formatSpeed(speed), 40, 220);
g.setColor(hrmReady ? 0x07E0 : 0xF800);
g.drawString(heartRate, 120, 220);
g.setColor(0xFFFF);
g.drawString(cadence, 200, 220);
}
function handleGps(coords) {
const step = gps.getDistance(coords);
gpsReady = coords.fix > 0 && gps.isReady();
speed = isFinite(gps.speed) ? gps.speed : 0;
if (running) {
totDist += step.d;
totTime += step.t;
}
}
function handleHrm(hrm) {
hrmReady = hrm.confidence > 50;
heartRate = hrm.bpm;
}
function handleStep() {
cadence = step.getCadence();
if (running) {
totSteps += 1;
}
}
function start() {
running = true;
drawBackground();
draw();
}
function stop() {
if (!running) {
totDist = 0;
totTime = 0;
totSteps = 0;
}
running = false;
drawBackground();
draw();
}
Bangle.on('GPS', handleGps);
Bangle.on('HRM', handleHrm);
Bangle.on('step', handleStep);
Bangle.setGPSPower(1);
Bangle.setHRMPower(1);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawBackground();
draw();
setInterval(draw, 500);
setWatch(start, BTN1, { repeat: true });
setWatch(stop, BTN3, { repeat: true });

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

1
apps/blackjack/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New game! BTN4- Hit card, BTN5- Stand

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIQNgfAAgU///wAgMH/4dBAoMMAQMQAQMIAQMYAQ4RCApcPwAFDgIwBAoQ4BAoMP8EHwfghk//AXBuEMv38n+AjEMvl8/4FDvoFBmEMvF994FBg04vgdBAoMAAot4AoNgAoPwAoZFBAongAoPggyIBAoPAg0HwAFDh4BBAoUeh0PwOAg08AocDv/+Ao3DAod//a3BAorBDAohRBgf+AocBAokApgCBhzSCWIkHVYgYCWIngYwQrB/gFDgF//AFDD4QAD8AFEAAIA="))

View File

@ -0,0 +1,191 @@
const Clubs = { width : 48, height : 48, bpp : 1,
buffer : require("heatshrink").decompress(atob("ACcP+AFDn/8Aod//wFD///AgUBAoOAApsDAoPAAr4vLI4pTEgP8L4M/wEH/5rB//gh//x/x//wj//9/3//4n4iBAAIZBAol/Aof+Apv5z4FP+OPAo41BAoX8I4Pj45HBAoPD4YFBLIOD4JZBRAMD4CKC/AFBj59Cg/gQYYFXAB4="))
};
const Spades = { width : 48, height : 48, bpp : 1,
buffer : require("heatshrink").decompress(atob("ABsBwAFDgfAAocH8AFDh/wAocf/AFDn/8Aod//wFD///FwYFBGAUDAoIwCg4FBGAUPAoIwCj4FBGAU/AoIwCv4FBGAQEBGAQuCGAQuCGAQFLHQQ8CAupHLL4prB+fPTgU/8fHVwbLLApbXFbpYFLdIoADA=="))
};
const Hearts = { width : 48, height : 48, bpp : 4,
buffer : require("heatshrink").decompress(atob("ADlVqtQBQ8FBYIKIrnMAAINGqoKC4okGCwYAB4AKDhgKE4oWKAAILDBQwYEBYwwDFwojFgoLHEgQ6H5hhCBZAkCBRAjLEgI6IC4YLIC5Y7BBZXBjgjVABYX/C8CnKABbXLABTvMC8sMC6fAC4KQURwIABRypgULwRgULwRIUCwhIRIwiRSRoZITCwx5POoowRCxAwNFxIwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQwDFycAgoXBqAXTgFc4oWUJAJGUJARGVAEo"))
};
const Diamonds = { width : 48, height : 48, bpp : 4,
buffer : require("heatshrink").decompress(atob("AHUFC60M4AXV5nFIyvM5hGVC4JIUCwJIUIwRIUIwRIUCwZISIwgABqBGUJCQWFPKBGGJCFcC455OCw4wOOox5QIxB5NOpBIOFxZ5LCxYwKOpQwMIxh5KOxipLL6xgNR5QwMX5TvXPJZ1JJBpGLPJR1LJBZGNPJIWOJA5GOPJB1NJBIWQPIpGRJApGRPIoWSJAa8PJA5GTJAYWUJAJGVAAJGVAHo="))
};
var deck = [];
var player = {Hand:[]};
var computer = {Hand:[]};
function createDeck() {
var suits = ["Spades", "Hearts", "Diamonds", "Clubs"];
var values = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"];
var dck = [];
for (var i = 0 ; i < values.length; i++) {
for(var x = 0; x < suits.length; x++) {
dck.push({ Value: values[i], Suit: suits[x] });
}
}
return dck;
}
function shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
function EndGameMessdage(msg){
g.drawString(msg, 155, 200);
setTimeout(function(){
startGame();
}, 2500);
}
function hitMe() {
player.Hand.push(deck.pop());
renderOnScreen(1);
var playerWeight = calcWeight(player.Hand, 0);
if(playerWeight == 21)
EndGameMessdage('WINNER');
else if(playerWeight > 21)
EndGameMessdage('LOOSER');
}
function calcWeight(hand, hideCard) {
if(hideCard === 1) {
if (hand[0].Value == "J" || hand[0].Value == "Q" || hand[0].Value == "K")
return "10 +";
else if (hand[0].Value == "A")
return "11 +";
else
return parseInt(hand[0].Value) +" +";
}
else {
var weight = 0;
for(i=0; i<hand.length; i++){
if (hand[i].Value == "J" || hand[i].Value == "Q" || hand[i].Value == "K") {
weight += 10;
}
else if (hand[i].Value == "A") {
weight += 1;
}
else
weight += parseInt(hand[i].Value);
}
// Find count of aces because it may be 11 or 1
var numOfAces = hand.filter(function(x){ return x.Value === "A"; }).length;
for (var j = 0; j < numOfAces; j++) {
if (weight + 10 <= 21) {
weight +=10;
}
}
return weight;
}
}
function stand(){
function sleepFor( sleepDuration ){
console.log("Sleeping...");
var now = new Date().getTime();
while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
}
renderOnScreen(0);
var playerWeight = calcWeight(player.Hand, 0);
var bangleWeight = calcWeight(computer.Hand, 0);
while(bangleWeight<17){
sleepFor(500);
computer.Hand.push(deck.pop());
renderOnScreen(0);
bangleWeight = calcWeight(computer.Hand, 0);
}
if (bangleWeight == playerWeight)
EndGameMessdage('TIES');
else if(playerWeight==21 || bangleWeight > 21 || bangleWeight < playerWeight)
EndGameMessdage('WINNER');
else if(bangleWeight > playerWeight)
EndGameMessdage('LOOSER');
}
function renderOnScreen(HideCard) {
const fontName = "6x8";
g.clear(); // clear screen
g.reset(); // default draw styles
g.setFont(fontName, 1);
g.drawString('RST', 220, 35);
g.drawString('Hit', 60, 230);
g.drawString('Stand', 165, 230);
g.setFont(fontName, 3);
for(i=0; i<computer.Hand.length; i++){
g.drawImage(eval(computer.Hand[i].Suit), i*48, 10);
if(i == 1 && HideCard == 1)
g.drawString("?", i*48+18, 58);
else
g.drawString(computer.Hand[i].Value, i*48+18, 58);
}
g.setFont(fontName, 2);
g.drawString('BangleJS has '+ calcWeight(computer.Hand, HideCard), 5, 85);
g.setFont(fontName, 3);
for(i=0; i<player.Hand.length; i++){
g.drawImage(eval(player.Hand[i].Suit), i*48, 125);
g.drawString(player.Hand[i].Value, i*48+18, 175);
}
g.setFont(fontName, 2);
g.drawString('You have ' + calcWeight(player.Hand, 0), 5, 202);
}
function dealHands() {
player.Hand= [];
computer.Hand= [];
setTimeout(function(){
player.Hand.push(deck.pop());
renderOnScreen(0);
}, 500);
setTimeout(function(){
computer.Hand.push(deck.pop());
renderOnScreen(1);
}, 1000);
setTimeout(function(){
player.Hand.push(deck.pop());
renderOnScreen(1);
}, 1500);
setTimeout(function(){
computer.Hand.push(deck.pop());
renderOnScreen(1);
}, 2000);
}
function startGame(){
deck = createDeck();
deck = shuffle(deck);
dealHands();
}
setWatch(hitMe, BTN4, {repeat:true, edge:"falling"});
setWatch(stand, BTN5, {repeat:true, edge:"falling"});
setWatch(startGame, BTN1, {repeat:true, edge:"falling"});
startGame();

Binary file not shown.

View File

@ -0,0 +1 @@
{"id":"blackjack","name":"Black Jack","src":"blackjack.app.js","icon":"blackjack.img","version":"0.1","files":"blackjack.info,blackjack.app.js,blackjack.img"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: fix precision rounding issue + no reset when equals pressed

23
apps/calculator/README.md Normal file
View File

@ -0,0 +1,23 @@
# Calculator
Basic calculator reminiscent of MacOs's one. Handy for small calculus.
<img src="https://user-images.githubusercontent.com/702227/79086938-bd3f4380-7d35-11ea-9988-a1a42916643f.png" height="384" width="384" />
## Features
- add / substract / divide / multiply
- handles floats
- basic memory button
## Controls
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5
- SELECT: BTN2
## Creator
<https://twitter.com/fredericrous>

View File

@ -144,19 +144,57 @@ function drawKey(name, k, selected) {
g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin); g.drawString(k.val || name, k.xy[0] + RIGHT_MARGIN + rMargin, k.xy[1] + BOTTOM_MARGIN + bMargin);
} }
function getIntWithPrecision(x) {
var xStr = x.toString();
var xRadix = xStr.indexOf('.');
var xPrecision = xRadix === -1 ? 0 : xStr.length - xRadix - 1;
return {
num: Number(xStr.replace('.', '')),
p: xPrecision
};
}
function multiply(x, y) {
var xNum = getIntWithPrecision(x);
var yNum = getIntWithPrecision(y);
return xNum.num * yNum.num / Math.pow(10, xNum.p + yNum.p);
}
function divide(x, y) {
var xNum = getIntWithPrecision(x);
var yNum = getIntWithPrecision(y);
return xNum.num / yNum.num / Math.pow(10, xNum.p - yNum.p);
}
function sum(x, y) {
let xNum = getIntWithPrecision(x);
let yNum = getIntWithPrecision(y);
let diffPrecision = Math.abs(xNum.p - yNum.p);
if (diffPrecision > 0) {
if (xNum.p > yNum.p) {
yNum.num = yNum.num * Math.pow(10, diffPrecision);
} else {
xNum.num = xNum.num * Math.pow(10, diffPrecision);
}
}
return (xNum.num + yNum.num) / Math.pow(10, Math.max(xNum.p, yNum.p));
}
function subtract(x, y) {
return sum(x, -y);
}
function doMath(x, y, operator) { function doMath(x, y, operator) {
// might not be a number due to display of dot "." algo
x = Number(x);
y = Number(y);
switch (operator) { switch (operator) {
case '/': case '/':
return x / y; return divide(x, y);
case '*': case '*':
return x * y; return multiply(x, y);
case '+': case '+':
return x + y; return sum(x, y);
case '-': case '-':
return x - y; return subtract(x, y);
} }
} }
@ -204,7 +242,7 @@ function displayOutput(num) {
} }
len = (num + '').length; len = (num + '').length;
if (numNumeric < 0) { if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) {
// minus is not available in font 7x11Numeric7Seg, we use Vector // minus is not available in font 7x11Numeric7Seg, we use Vector
g.setFont('Vector', 20); g.setFont('Vector', 20);
g.drawString('-', 220 - (len * 15), 10); g.drawString('-', 220 - (len * 15), 10);
@ -214,18 +252,33 @@ function displayOutput(num) {
} }
g.drawString(num, 220 - (len * 15) + minusMarge, 10); g.drawString(num, 220 - (len * 15) + minusMarge, 10);
} }
var wasPressedEquals = false;
var hasPressedNumber = false;
function calculatorLogic(x) { function calculatorLogic(x) {
if (hasPressedEquals) { if (wasPressedEquals && hasPressedNumber !== false) {
currNumber = results;
prevNumber = null; prevNumber = null;
operator = null; currNumber = hasPressedNumber;
results = null; wasPressedEquals = false;
isDecimal = null; hasPressedNumber = false;
displayOutput(currNumber); return;
hasPressedEquals = false;
} }
if (prevNumber != null && currNumber != null && operator != null) { if (hasPressedEquals) {
if (hasPressedNumber) {
prevNumber = null;
hasPressedNumber = false;
operator = null;
} else {
currNumber = null;
prevNumber = results;
}
hasPressedEquals = false;
wasPressedEquals = true;
}
if (currNumber == null && operator != null && '/*-+'.indexOf(x) !== -1) {
operator = x;
displayOutput(prevNumber);
} else if (prevNumber != null && currNumber != null && operator != null) {
// we execute the calculus only when there was a previous number entered before and an operator // we execute the calculus only when there was a previous number entered before and an operator
results = doMath(prevNumber, currNumber, operator); results = doMath(prevNumber, currNumber, operator);
operator = x; operator = x;
@ -255,8 +308,10 @@ function buttonPress(val) {
operator = null; operator = null;
} else { } else {
keys.R.val = 'AC'; keys.R.val = 'AC';
drawKey('R', keys.R); drawKey('R', keys.R, true);
} }
wasPressedEquals = false;
hasPressedNumber = false;
displayOutput(0); displayOutput(0);
break; break;
case '%': case '%':
@ -265,11 +320,12 @@ function buttonPress(val) {
} else if (currNumber != null) { } else if (currNumber != null) {
displayOutput(currNumber /= 100); displayOutput(currNumber /= 100);
} }
hasPressedNumber = false;
break; break;
case 'N': case 'N':
if (results != null) { if (results != null) {
displayOutput(results *= -1); displayOutput(results *= -1);
} else if (currNumber != null) { } else {
displayOutput(currNumber *= -1); displayOutput(currNumber *= -1);
} }
break; break;
@ -278,6 +334,7 @@ function buttonPress(val) {
case '-': case '-':
case '+': case '+':
calculatorLogic(val); calculatorLogic(val);
hasPressedNumber = false;
break; break;
case '.': case '.':
keys.R.val = 'C'; keys.R.val = 'C';
@ -290,18 +347,24 @@ function buttonPress(val) {
results = doMath(prevNumber, currNumber, operator); results = doMath(prevNumber, currNumber, operator);
prevNumber = results; prevNumber = results;
displayOutput(results); displayOutput(results);
hasPressedEquals = true; hasPressedEquals = 1;
} }
hasPressedNumber = false;
break; break;
default: default:
keys.R.val = 'C'; keys.R.val = 'C';
drawKey('R', keys.R); drawKey('R', keys.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
if (isDecimal) { if (isDecimal) {
currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val; currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
isDecimal = false; isDecimal = false;
} else { } else {
currNumber = currNumber == null ? val : currNumber + val; currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val);
} }
if (hasPressedEquals === 1) {
hasPressedEquals = 2;
}
hasPressedNumber = currNumber;
displayOutput(currNumber); displayOutput(currNumber);
break; break;
} }
@ -315,38 +378,15 @@ for (var k in keys) {
g.setFont('7x11Numeric7Seg', 2.8); g.setFont('7x11Numeric7Seg', 2.8);
g.drawString('0', 205, 10); g.drawString('0', 205, 10);
function moveDirection(d) {
setWatch(function() {
drawKey(selected, keys[selected]);
// key 0 is 2 keys wide, go up to 1 if it was previously selected
if (selected == '0' && prevSelected === '1') {
prevSelected = selected;
selected = '1';
} else {
prevSelected = selected;
selected = keys[selected].trbl[0];
}
drawKey(selected, keys[selected], true);
}, BTN1, {repeat: true, debounce: 100});
setWatch(function() {
drawKey(selected, keys[selected]); drawKey(selected, keys[selected]);
prevSelected = selected; prevSelected = selected;
selected = keys[selected].trbl[2]; selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d];
drawKey(selected, keys[selected], true); drawKey(selected, keys[selected], true);
}, BTN3, {repeat: true, debounce: 100}); }
Bangle.on('touch', function(direction) { setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
drawKey(selected, keys[selected]); setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
prevSelected = selected; setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
if (direction == 1) { setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
selected = keys[selected].trbl[3]; setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});
} else if (direction == 2) {
selected = keys[selected].trbl[1];
}
drawKey(selected, keys[selected], true);
});
setWatch(function() {
buttonPress(selected);
}, BTN2, {repeat: true, debounce: 100});

273
apps/calculator/tests.html Normal file
View File

@ -0,0 +1,273 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Calculator tests</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/7.1.0/mocha.min.css">
<style>
#header {
margin: 60px 50px;
font: 1em "Helvetica Neue",Helvetica,Arial,sans-serif;
font-weight: 200;
}
</style>
</head>
<body>
<div id="header"></div>
<div id="mocha"></div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/7.1.0/mocha.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js"></script>
<script>
// mocks
const _ = () => {}
setWatch = _
drawKey = _
currentOutput = 0;
g = {
setFont: _,
drawString: n => currentOutput = n,
setColor: _,
fillRect: _,
clear: _
}
_graphics = function () {
this.setFontCustom = _
}
Graphics = _graphics
BTN1 = 1
BTN2 = 2
BTN3 = 3
BTN4 = 4
BTN5 = 5
Bangle = {
on: _
}
Terminal = { println: console.log }
</script>
<script src="app.js"></script>
<script>
header`
// Unit tests for the BangleJS's Calculator app
`
mocha.setup({ui:'bdd'})
chai.should()
var expect = chai.expect
const sequencePress = x => x.split('').forEach(y => buttonPress(y))
const sequenceReset = _ => [...Array(2)].forEach(x => buttonPress('R'))
describe("Simple arithmetic", function(){
it("multiplication", function(){
multiply(1.4,2.4).should.equal(3.36)
})
it("division", function(){
divide(4.4,2).should.equal(2.2)
})
it("sum", function(){
sum(4.1,2).should.equal(6.1)
})
it("subtract", function(){
subtract(4.1,2).should.equal(2.1)
})
})
describe("Simple Operation with Reset", function(){
it("Simple addition", function(){
sequencePress("50+3=")
currentOutput.should.equal(53)
})
it("Reset output 'C' then 'AC'", function(){
sequenceReset()
currentOutput.should.equal(0)
})
it("Complex calculus", function(){
sequenceReset()
sequencePress("3*3+3-2/2=")
currentOutput.should.equal(5)
})
it("Change operator", function(){
sequenceReset()
sequencePress("3*+/3=")
currentOutput.should.equal(1)
})
})
describe("Operations on Double-s", function(){
it("Simple addition", function(){
sequenceReset()
sequencePress("1.3+1.7=")
currentOutput.should.equal(3)
})
it("some calculation", function(){
sequenceReset()
sequencePress("1.3+1.7*2.22/2=")
currentOutput.should.equal(3.33)
})
it("No corrupt opposed to what javascript Number would", function(){
sequenceReset()
sequencePress("1.3+1.7*2.2/2=")
currentOutput.should.equal(3.3)
})
it("Complex calcul", function(){
sequenceReset()
sequencePress("48/.2/")
currentOutput.should.equal(240)
})
})
describe("Negative Operations", function(){
it("Negative on first number", function(){
sequenceReset()
sequencePress("50N+3=")
currentOutput.should.equal(-47)
})
it("Substract negative", function(){
sequenceReset()
sequencePress("50N-3=")
currentOutput.should.equal(-53)
})
it("Negative before number is typed", function(){
sequenceReset()
sequencePress("N50-3=")
currentOutput.should.equal(-53)
})
it("Negative addition on second number", function(){
sequenceReset()
sequencePress("50-N33=")
currentOutput.should.equal(83)
})
it("Negative zero", function(){
sequenceReset()
sequencePress("N")
currentOutput.should.equal(-0)
sequenceReset()
sequencePress("0N")
currentOutput.should.equal(-0)
sequenceReset()
sequencePress("N0")
currentOutput.should.equal('-0')
sequenceReset()
sequencePress("0N")
currentOutput.should.equal(-0)
sequencePress("N0")
currentOutput.should.equal('0')
})
})
describe("Zero division", function(){
it("Divide 0 by 0", function(){
sequenceReset()
sequencePress("0/0=")
currentOutput.should.equal('NOT A NUMBER')
})
it("Divde N by 0", function(){
sequenceReset()
sequencePress("1/0=")
currentOutput.should.equal('INFINITY')
})
it("Divde -N by 0", function(){
sequenceReset()
sequencePress("N1/0=")
currentOutput.should.equal('-INFINITY')
})
})
describe("Press equals '='", function(){
it("should display result when new operation button is pressed", function(){
sequenceReset()
sequencePress("5+6+")
currentOutput.should.equal(11)
sequenceReset()
sequencePress("5-6*4/2/")
currentOutput.should.equal(-2)
})
it("New operation after '='", function(){
sequenceReset()
sequencePress("5+4=5")
currentOutput.should.equal('5')
sequenceReset()
sequencePress("N5+4*3-3/-1=5")
currentOutput.should.equal('5')
})
it("Double '=' repeats last operation", function(){
sequenceReset()
sequencePress("2+2==")
currentOutput.should.equal(6)
})
it("New operation applied to calculated result", function(){
sequenceReset()
sequencePress("9*9=*9=")
currentOutput.should.equal(729)
})
it("Turn result negative, do addition", function(){
sequenceReset()
sequencePress("9*9=N+1=")
currentOutput.should.equal(-80)
})
it("New operation after '=' dissociated from previous one", function(){
sequenceReset()
sequencePress("9*9=9*")
currentOutput.should.equal('9')
sequenceReset()
sequencePress("9*9=99+1=*2=")
currentOutput.should.equal(200)
})
})
describe("Memory", function(){
it("Reset 1st number with 'C'", function(){
sequenceReset()
sequencePress("50R3+6=")
currentOutput.should.equal(9)
})
it("Reset 2nd number with 'C'", function(){
sequenceReset()
sequencePress("50+3R+6=")
currentOutput.should.equal(56)
})
it("Complex calcul", function(){
sequenceReset()
sequencePress("/3*3+3R-+2/2=")
currentOutput.should.equal(5.5)
})
})
mocha.run()
function header(str) { document.getElementById('header').innerHTML = str[0].replace(/\n/, '').replace(/\n/g, '<br>') }
</script>
</body>
</html>

View File

@ -1 +1,2 @@
0.01: New widget and app! 0.01: New widget and app!
0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme)

View File

@ -5,6 +5,8 @@ The advantage is, that you can still see your normal watchface and other widgets
The widget is always active, but only shown when the timer is on. The widget is always active, but only shown when the timer is on.
Hours, minutes, seconds and timer status can be set with an app. Hours, minutes, seconds and timer status can be set with an app.
Depending on when you start the timer, it may alert up to 0,999 seconds early. This is because it checks only for full seconds. When there is less than one seconds left, it buzzes. This cannot be avoided without checking more than every second, which I would like to avoid.
## Screenshots ## Screenshots
TBD TBD
@ -18,18 +20,19 @@ TBD
There are no settings section in the settings app, timer can be set using an app. There are no settings section in the settings app, timer can be set using an app.
* Reset values: Reset hours, minutes, seconds to 0; set timer on to false; write to settings file
* Hours: Set the hours for the timer * Hours: Set the hours for the timer
* Minutes: Set the minutes for the timer * Minutes: Set the minutes for the timer
* Seconds: Set the seconds for the timer * Seconds: Set the seconds for the timer
* Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app. The widget is always there, but only visible when timer is on. * Timer on: Starts the timer and displays the widget when set to 'On'. You have to leave the app to load the widget which starts the timer. The widget is always there, but only visible when timer is on.
## Releases ## Releases
* Offifical app loader: Not yet published. * Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/chronowid (https://banglejs.com/apps/)
* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#) * Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/chronowid (https://purple-tentacle.github.io/BangleApps/index.html#)
* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid * Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid
## Requests ## Requests
If you have any feature requests, please contact me on the Espruino forum: http://forum.espruino.com/profiles/155005/ If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/

View File

@ -30,7 +30,6 @@ settingsChronowid = storage.readJSON('chronowid.json',1);
if (!settingsChronowid) resetSettings(); if (!settingsChronowid) resetSettings();
E.on('kill', () => { E.on('kill', () => {
print("-KILL-");
updateSettings(); updateSettings();
}); });
@ -45,6 +44,14 @@ function showMenu() {
timerMenu.started.value = settingsChronowid.started; timerMenu.started.value = settingsChronowid.started;
} }
}, },
'Reset values': function() {
settingsChronowid.hours = 0;
settingsChronowid.minutes = 0;
settingsChronowid.seconds = 0;
settingsChronowid.started = false;
updateSettings();
showMenu();
},
'Hours': { 'Hours': {
value: settingsChronowid.hours, value: settingsChronowid.hours,
min: 0, min: 0,

View File

@ -36,19 +36,18 @@
//counts down, calculates and displays //counts down, calculates and displays
function countDown() { function countDown() {
//printDebug();
now = new Date(); now = new Date();
diff = settingsChronowid.goal - now; //calculate difference diff = settingsChronowid.goal - now; //calculate difference
WIDGETS["chronowid"].draw(); WIDGETS["chronowid"].draw();
//time is up //time is up
if (settingsChronowid.started && diff <= 0) { if (settingsChronowid.started && diff < 1000) {
Bangle.buzz(1500); Bangle.buzz(1500);
//write timer off to file //write timer off to file
settingsChronowid.started = false; settingsChronowid.started = false;
storage.writeJSON('chronowid.json', settingsChronowid); storage.writeJSON('chronowid.json', settingsChronowid);
clearInterval(interval); //stop interval clearInterval(interval); //stop interval
//printDebug();
} }
//printDebug();
} }
// draw your widget // draw your widget
@ -72,12 +71,13 @@
g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00 g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00
} }
} }
else { // not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed.
width = 58; // else {
g.clearRect(this.x,this.y,this.x+width,this.y+height); // width = 58;
g.setFont("6x8", 2); // g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.drawString("END", this.x+15, this.y+5); // g.setFont("6x8", 2);
} // g.drawString("END", this.x+15, this.y+5);
// }
} }
if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second

2
apps/compass/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Show text if uncalibrated

View File

@ -20,10 +20,19 @@ Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return; if (!Bangle.isLCDOn()) return;
g.setFont("6x8",3); g.setFont("6x8",3);
g.setColor(0); g.setColor(0);
g.fillRect(70,0,170,24); g.fillRect(0,0,230,40);
g.setColor(0xffff); g.setColor(0xffff);
if (isNaN(m.heading)) {
g.setFontAlign(-1,-1);
g.setFont("6x8",2);
g.drawString("Uncalibrated",50,12);
g.drawString("turn 360° around",25,26);
}
else {
g.setFontAlign(0,0); g.setFontAlign(0,0);
g.drawString(isNaN(m.heading)?"---":Math.round(m.heading),120,12); g.setFont("6x8",3);
g.drawString(Math.round(m.heading),120,12);
}
g.setColor(0,0,0); g.setColor(0,0,0);
arrow(oldHeading,0); arrow(oldHeading,0);
arrow(oldHeading+180,0); arrow(oldHeading+180,0);

View File

@ -1 +1,2 @@
0.02: Fix deletion of apps - now use files list in app.info (fix #262) 0.02: Fix deletion of apps - now use files list in app.info (fix #262)
0.03: Add support for data files

View File

@ -30,29 +30,80 @@ function showMainMenu() {
return E.showMenu(mainmenu); return E.showMenu(mainmenu);
} }
function eraseApp(app) { function isGlob(f) {return /[?*]/.test(f)}
E.showMessage('Erasing\n' + app.name + '...'); function globToRegex(pattern) {
const ESCAPE = '.*+-?^${}()|[]\\';
const regex = pattern.replace(/./g, c => {
switch (c) {
case '?': return '.';
case '*': return '.*';
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
}
});
return new RegExp('^'+regex+'$');
}
function eraseFiles(app) {
app.files.split(",").forEach(f=>storage.erase(f)); app.files.split(",").forEach(f=>storage.erase(f));
} }
function eraseData(app) {
if(!app.data) return;
const d=app.data.split(';'),
files=d[0].split(','),
sFiles=(d[1]||'').split(',');
let erase = f=>storage.erase(f);
files.forEach(f=>{
if (!isGlob(f)) erase(f);
else storage.list(globToRegex(f)).forEach(erase);
})
erase = sf=>storage.open(sf,'r').erase();
sFiles.forEach(sf=>{
if (!isGlob(sf)) erase(sf);
else storage.list(globToRegex(sf+'\u0001'))
.forEach(fs=>erase(fs.substring(0,fs.length-1)));
})
}
function eraseApp(app, files,data) {
E.showMessage('Erasing\n' + app.name + '...');
if (files) eraseFiles(app)
if (data) eraseData(app)
}
function eraseOne(app, files,data){
E.showPrompt('Erase\n'+app.name+'?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
eraseApp(app, files,data)
showApps();
} else {
showAppMenu(app)
}
})
}
function eraseAll(apps, files,data) {
E.showPrompt('Erase all?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
for(var n = 0; n<apps.length; n++)
eraseApp(apps[n], files,data);
}
showApps();
})
}
function showAppMenu(app) { function showAppMenu(app) {
const appmenu = { let appmenu = {
'': { '': {
'title': app.name, 'title': app.name,
}, },
'< Back': () => m = showApps(), '< Back': () => m = showApps(),
'Erase': () => { }
E.showPrompt('Erase\n' + app.name + '?').then((v) => { if (app.data) {
if (v) { appmenu['Erase Completely'] = () => eraseOne(app, true, true)
Bangle.buzz(100, 1); appmenu['Erase App,Keep Data'] = () => eraseOne(app,true, false)
eraseApp(app); appmenu['Only Erase Data'] = () => eraseOne(app,false, true)
m = showApps();
} else { } else {
m = showAppMenu(app) appmenu['Erase'] = () => eraseOne(app,true, false)
} }
});
}
};
return E.showMenu(appmenu); return E.showMenu(appmenu);
} }
@ -78,13 +129,12 @@ function showApps() {
return menu; return menu;
}, appsmenu); }, appsmenu);
appsmenu['Erase All'] = () => { appsmenu['Erase All'] = () => {
E.showPrompt('Erase all?').then((v) => { E.showMenu({
if (v) { '': {'title': 'Erase All'},
Bangle.buzz(100, 1); 'Erase Everything': () => eraseAll(list, true, true),
for (var n = 0; n < list.length; n++) 'Erase Apps,Keep Data': () => eraseAll(list, true, false),
eraseApp(list[n]); 'Only Erase Data': () => eraseAll(list, false, true),
} '< Back': () => showApps(),
m = showApps();
}); });
}; };
} else { } else {

1
apps/gpsnav/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

66
apps/gpsnav/README.md Normal file
View File

@ -0,0 +1,66 @@
## gpsnav - navigate to waypoints
The app is aimed at small boat navigation although it can also be used to mark the location of your car, bicycle etc and then get directions back to it. Please note that it would be foolish in the extreme to rely on this as your only boat navigation aid!
The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix.
![](first_screen.jpg)
The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn white. Then use BTN1 and BTN3 to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected.
![](waypoint_screen.jpg)
The display shows that Stone Henge is 108.75Km from the location where I made the screenshot and the direction is 255 degrees - approximately west. The display shows that I am currently moving approximately north - albeit slowly!. The position of the blue circle indicates that I need to turn left to get on course to Stone Henge. When the circle and red triangle line up you are on course and course will equal bearing.
### Marking Waypoints
The app lets you mark your current location as follows. There are vacant slots in the waypoint file which can be allocated a location. In the distributed waypoint file these are labelled WP0 to WP4. Select one of these - WP2 is shown below.
![](select_screen.jpg)
Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2.
![](marked_screen.jpg)
The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot as you have just marked your current location.
### Waypoint JSON file
When the app is loaded from the app loader, a file named waypoints.json is loaded along with the javascript etc. The file has the following contents:
~~~
[
{
"mark":0,
"name":"NONE"
},
{
"mark":1,
"name":"No10",
"lat":51.5032,
"lon":-0.1269
},
{
"mark":1,
"name":"Stone",
"lat":51.1788,
"lon":-1.8260
},
{ "name":"WP0" },
{ "name":"WP1" },
{ "name":"WP2" },
{ "name":"WP3" },
{ "name":"WP4" }
]
~~~
The file contains the initial NONE waypoint which is useful if you just want to display course and speed. The next two entries are waypoints to No 10 Downing Street and to Stone Henge - obtained from Google Maps. The last five entries are entries which can be *marked*.
You add and delete entries using the Web IDE to load and then save the file from and to watch storage. The app itself does not limit the number of entries although it does load the entire file into RAM which will obviously limit this.
I plan to release an accompanying watch app to edit waypoint files in the near future and a way to download your own waypoint file using the app loader.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AFmACysDC9+IC6szC/8AgUgLwYXBPAgLDAA8kC5MyC5cyogXHmYiDURMkDAMzC4JgBmcyoAXMGANCC4YDBkgXMHwVEC4hQDC5kyF4kjJ4QAMOgMjC4eCohGNMARbCC4ODkilLAAQSBCYJ3EmYVLhAWCCgQaCAAUwCpowCFwYADIRAYHC4wZFRQIAGnAhJXgwAFxAYHwC9JFwiQCFhIZISAQwDX5sCoQTCDYUjUpAAFglElAXDmS9JAAtEoUyC4ckkbvMC4QQBC4YeBC5sEB4IXEkgfBJBkEH4QXCCYMkoQXMHwcIC4ZQCUpYMDC4oiBC5YEDC40AkCRNAAIXBCJ4X2URgAJhAXvCyoA/ACoA="))

224
apps/gpsnav/app.js Normal file
View File

@ -0,0 +1,224 @@
const Yoff = 40;
var pal2color = new Uint16Array([0x0000,0xffff,0x07ff,0xC618],0,2);
var buf = Graphics.createArrayBuffer(240,50,2,{msb:true});
function flip(b,y) {
g.drawImage({width:240,height:50,bpp:2,buffer:b.buffer, palette:pal2color},0,y);
b.clear();
}
var brg=0;
var wpindex=0;
const labels = ["N","NE","E","SE","S","SW","W","NW"];
function drawCompass(course) {
buf.setColor(1);
buf.setFont("Vector",16);
var start = course-90;
if (start<0) start+=360;
buf.fillRect(28,45,212,49);
var xpos = 30;
var frag = 15 - start%15;
if (frag<15) xpos+=frag; else frag = 0;
for (var i=frag;i<=180-frag;i+=15){
var res = start + i;
if (res%90==0) {
buf.drawString(labels[Math.floor(res/45)%8],xpos-8,0);
buf.fillRect(xpos-2,25,xpos+2,45);
} else if (res%45==0) {
buf.drawString(labels[Math.floor(res/45)%8],xpos-12,0);
buf.fillRect(xpos-2,30,xpos+2,45);
} else if (res%15==0) {
buf.fillRect(xpos,35,xpos+1,45);
}
xpos+=15;
}
if (wpindex!=0) {
var bpos = brg - course;
if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360;
bpos+=120;
if (bpos<30) bpos = 14;
if (bpos>210) bpos = 226;
buf.setColor(2);
buf.fillCircle(bpos,40,8);
}
flip(buf,Yoff);
}
//displayed heading
var heading = 0;
function newHeading(m,h){
var s = Math.abs(m - h);
var delta = 1;
if (s<2) return h;
if (m > h){
if (s >= 180) { delta = -1; s = 360 - s;}
} else if (m <= h){
if (s < 180) delta = -1;
else s = 360 -s;
}
delta = delta * (1 + Math.round(s/15));
heading+=delta;
if (heading<0) heading += 360;
if (heading>360) heading -= 360;
return heading;
}
var course =0;
var speed = 0;
var satellites = 0;
var wp;
var dist=0;
function radians(a) {
return a*Math.PI/180;
}
function degrees(a) {
var d = a*180/Math.PI;
return (d+360)%360;
}
function bearing(a,b){
var delta = radians(b.lon-a.lon);
var alat = radians(a.lat);
var blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
}
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);
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
}
var selected = false;
function drawN(){
buf.setColor(1);
buf.setFont("6x8",2);
buf.drawString("o",100,0);
buf.setFont("6x8",1);
buf.drawString("kph",220,40);
buf.setFont("Vector",40);
var cs = course.toString();
cs = course<10?"00"+cs : course<100 ?"0"+cs : cs;
buf.drawString(cs,10,0);
var txt = (speed<10) ? speed.toFixed(1) : Math.round(speed);
buf.drawString(txt,140,4);
flip(buf,Yoff+70);
buf.setColor(1);
buf.setFont("Vector",20);
var bs = brg.toString();
bs = brg<10?"00"+bs : brg<100 ?"0"+bs : bs;
buf.setColor(3);
buf.drawString("Brg: ",0,0);
buf.drawString("Dist: ",0,30);
buf.setColor(selected?1:2);
buf.drawString(wp.name,140,0);
buf.setColor(1);
buf.drawString(bs,60,0);
if (dist<1000)
buf.drawString(dist.toString()+"m",60,30);
else
buf.drawString((dist/1000).toFixed(2)+"Km",60,30);
flip(buf,Yoff+130);
g.setFont("6x8",1);
g.setColor(0,0,0);
g.fillRect(10,230,60,239);
g.setColor(1,1,1);
g.drawString("Sats " + satellites.toString(),10,230);
}
var savedfix;
function onGPS(fix) {
savedfix = fix;
if (fix!==undefined){
course = isNaN(fix.course) ? course : Math.round(fix.course);
speed = isNaN(fix.speed) ? speed : fix.speed;
satellites = fix.satellites;
}
if (Bangle.isLCDOn()) {
if (fix!==undefined && fix.fix==1){
dist = distance(fix,wp);
if (isNaN(dist)) dist = 0;
brg = bearing(fix,wp);
if (isNaN(brg)) brg = 0;
}
drawN();
}
}
var intervalRef;
function clearTimers() {
if(intervalRef) {clearInterval(intervalRef);}
}
function startTimers() {
intervalRefSec = setInterval(function() {
newHeading(course,heading);
if (course!=heading) drawCompass(heading);
},200);
}
Bangle.on('lcdPower',function(on) {
if (on) {
g.clear();
Bangle.drawWidgets();
startTimers();
drawAll();
}else {
clearTimers();
}
});
function drawAll(){
g.setColor(1,0.5,0.5);
g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]);
g.setColor(1,1,1);
drawN();
drawCompass(heading);
}
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
wp=waypoints[0];
function nextwp(inc){
if (!selected) return;
wpindex+=inc;
if (wpindex>=waypoints.length) wpindex=0;
if (wpindex<0) wpindex = waypoints.length-1;
wp = waypoints[wpindex];
drawN();
}
function doselect(){
if (selected && waypoints[wpindex].mark===undefined && savedfix.fix) {
waypoints[wpindex] ={mark:1, name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon};
wp = waypoints[wpindex];
require("Storage").writeJSON("waypoints.json", waypoints);
}
selected=!selected;
drawN();
}
g.clear();
Bangle.setLCDBrightness(1);
Bangle.loadWidgets();
Bangle.drawWidgets();
// load widgets can turn off GPS
Bangle.setGPSPower(1);
drawAll();
startTimers();
Bangle.on('GPS', onGPS);
// Toggle selected
setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"});

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
apps/gpsnav/gpsnav.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
apps/gpsnav/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,23 @@
[
{
"mark":0,
"name":"NONE"
},
{
"mark":1,
"name":"No10",
"lat":51.5032,
"lon":-0.1269
},
{
"mark":1,
"name":"Stone",
"lat":51.1788,
"lon":-1.8260
},
{ "name":"WP0" },
{ "name":"WP1" },
{ "name":"WP2" },
{ "name":"WP3" },
{ "name":"WP4" }
]

View File

@ -5,3 +5,5 @@
0.05: Tweaks for variable size widget system 0.05: Tweaks for variable size widget system
0.06: Ensure widget update itself (fix #118) and change to using icons 0.06: Ensure widget update itself (fix #118) and change to using icons
0.07: Added @jeffmer's awesome track viewer 0.07: Added @jeffmer's awesome track viewer
0.08: Don't overwrite existing settings on app update
Clean up recorded tracks on app removal

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Don't overwrite existing settings on app update
Clean up recordings on app removal

1
apps/hidcam/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Core functionnality

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

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDCEAzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMxERERERETMzMzMzMzMzMzMzMzMzMzMzMRERERERERMzMzMzMzMzMzMzMzMzMzMzMREREREREREzMzMzMzMzMzMzMzMAAAAzEREREREREREzMzMzMzMzMzMzMzMAAAAxERERERERERETMzMzMzMzMzMzMxERERERERERERERERERERERETMzMzMzMRERERERERERERERERERERERERMzMzMzEREREREREREAAAAAEREREREREREzMzMzEREREREREQAAAAAAABERESIiIREzMzMzEREREREREAAAAAAAAAERESIiIREzMzMzEREREREQAAAKqqqgAAABESIiIREzMzMzEREREREQAAqqqqqqoAABESIiIREzMzMzEREREREAAKqqqqqqqgAAEREREREzMzMzERERERAACqqqqqqqqqAAAREREREzMzMzERERERAAqqqiIiIqqqoAAREREREzMzMzqqqqqgAAqqoiIiIiKqoAAKqqqqozMzMzqqqqqgAKqqIiIiIiKqqgAKqqqqozMzMzqqqqqgAKqqIiqqqiKqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAKqqqqqqqqqqqgAKqqqqozMzMzqqqqqgAAqqqqqqqqqqoAAKqqqqozMzMzqqqqqqAAqqqqqqqqqqoACqqqqqozMzMzqqqqqqAACqqqqqqqqqAACqqqqqozMzMzqqqqqqoAAKqqqqqqqgAAqqqqqqozMzMzqqqqqqoAAAqqqqqqoAAAqqqqqqozMzMzqqqqqqqgAAAKqqqgAAAKqqqqqqozMzMzqqqqqqqqAAAAAAAAAACqqqqqqqozMzMzqqqqqqqqqgAAAAAAAKqqqqqqqqozMzMzqqqqqqqqqqoAAAAAqqqqqqqqqqozMzMzOqqqqqqqqqqqqqqqqqqqqqqqqqMzMzMzM6qqqqqqqqqqqqqqqqqqqqqqqjMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMw=="))

52
apps/hidcam/app.js Normal file
View File

@ -0,0 +1,52 @@
var storage = require('Storage');
const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, camShot, profile;
if (settings.HID) {
profile = 'camShutter';
sendHid = function (code, cb) {
try {
NRF.sendHIDReport([1,code], () => {
NRF.sendHIDReport([1,0], () => {
if (cb) cb();
});
});
} catch(e) {
print(e);
}
};
camShot = function (cb) { sendHid(0x80, cb); };
} else {
E.showMessage('HID disabled');
setTimeout(load, 1000);
}
function drawApp() {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
g.fillCircle(122,127,60);
g.drawImage(storage.read("hidcam.img"),100,105);
const d = g.getWidth() - 18;
function c(a) {
return {
width: 8,
height: a.length,
bpp: 1,
buffer: (new Uint8Array(a)).buffer
};
}
g.fillRect(180,130, 240, 124);
}
if (camShot) {
setWatch(function(e) {
E.showMessage('camShot !');
setTimeout(drawApp, 1000);
camShot(() => {});
}, BTN2, { edge:"falling",repeat:true,debounce:50});
drawApp();
}

BIN
apps/hidcam/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

10
apps/metronome/README.md Normal file
View File

@ -0,0 +1,10 @@
# Metronome
This metronome makes your watch blink and vibrate with a given rate.
## Usage
* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat.
* Use `BTN1` to increase the bmp value by one.
* Use `BTN3` to decrease the bmp value by one.
* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+ABt4AB4fOFyFOABtUGDotOAAYvcp4ARqovbq0rACAvbqwABF98yGCAvdGAcHgAAEF8tWmIuGGA6QaF4lWFw4vgFwovPmIvuYDIvd0ejF59cF6qQFFwIvnMAguSqxfaFyQvYvOi0QuTF64uCAAQuRXwIvUqouEF6guFF5+cAAiOZF6iOaF5sxv+iF6xfRmVWFwWjv8rp4tSL6YvBqwuDMgQvnFwovURwIvQRggAELygvPgwuIF8ouEBwIvnFwwwXF54uBvwuFq0yF6buCF5guClQuFGAgvfFwcAF49WmIvRFwQvKFwkAmQvHYQMxF7l+FwgvKGAIvalQuGF5dWFx1VABVUvF4p0qAAdPCZNPF51OAD4vOKQIACF/4waF9wuEqgv/F/gwMF97vvAAUqADYtQAAMAADYuRGDgmLA="))

View File

@ -0,0 +1,93 @@
var tStart = Date.now();
var cindex=0; // index to iterate through colous
var bpm=60; // ininital bpm value
var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm
var tindex=0; //index to iterate through time_diffs
Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app
function changecolor() {
const maxColors = 2;
const colors = {
0: { value: 0xFFFF, name: "White" },
1: { value: 0x000F, name: "Navy" },
// 2: { value: 0x03E0, name: "DarkGreen" },
// 3: { value: 0x03EF, name: "DarkCyan" },
// 4: { value: 0x7800, name: "Maroon" },
// 5: { value: 0x780F, name: "Purple" },
// 6: { value: 0x7BE0, name: "Olive" },
// 7: { value: 0xC618, name: "LightGray" },
// 8: { value: 0x7BEF, name: "DarkGrey" },
// 9: { value: 0x001F, name: "Blue" },
// 10: { value: 0x07E0, name: "Green" },
// 11: { value: 0x07FF, name: "Cyan" },
// 12: { value: 0xF800, name: "Red" },
// 13: { value: 0xF81F, name: "Magenta" },
// 14: { value: 0xFFE0, name: "Yellow" },
// 15: { value: 0xFFFF, name: "White" },
// 16: { value: 0xFD20, name: "Orange" },
// 17: { value: 0xAFE5, name: "GreenYellow" },
// 18: { value: 0xF81F, name: "Pink" },
};
g.setColor(colors[cindex].value);
if (cindex == maxColors-1) {
cindex = 0;
}
else {
cindex += 1;
}
return cindex;
}
function updateScreen() {
g.clear();
changecolor();
Bangle.buzz(50, 0.75);
g.setFont("Vector",48);
g.drawString(Math.floor(bpm)+"bpm", -1, 70);
}
Bangle.on('touch', function(button) {
// setting bpm by tapping the screen. Uses the mean time difference between several tappings.
if (tindex < time_diffs.length) {
if (Date.now()-tStart < 5000) {
time_diffs[tindex] = Date.now()-tStart;
}
} else {
tindex=0;
time_diffs[tindex] = Date.now()-tStart;
}
tindex += 1;
mean_time = 0.0;
for(count = 0; count < time_diffs.length; count++) {
mean_time += time_diffs[count];
}
time_diff = mean_time/count;
tStart = Date.now();
clearInterval(time_diff);
g.clear();
g.setFont("Vector",48);
bpm = (60 * 1000/(time_diff));
g.drawString(Math.floor(bpm)+"bpm", -1, 70);
clearInterval(interval);
interval = setInterval(updateScreen, 60000 / bpm);
return bpm;
});
// enable bpm finetuning via buttons.
setWatch(() => {
bpm += 1;
clearInterval(interval);
interval = setInterval(updateScreen, 60000 / bpm);
}, BTN1, {repeat:true});
setWatch(() => {
if (bpm > 1) {
bpm -= 1;
clearInterval(interval);
interval = setInterval(updateScreen, 60000 / bpm);
}
}, BTN3, {repeat:true});
interval = setInterval(updateScreen, 60000 / bpm);

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -5,3 +5,4 @@
0.04: Run again when updated 0.04: Run again when updated
Don't run again when settings app is updated (or absent) Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings Add "Run Now" option to settings
0.05: Don't overwrite existing settings on app update

View File

@ -1,11 +1,11 @@
(function() { (function() {
let s = require('Storage').readJSON('ncstart.settings.json', 1) let s = require('Storage').readJSON('ncstart.json', 1)
|| require('Storage').readJSON('setting.json', 1) || require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent || {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('ncstart.app.js')) { if (!s.welcomed && require('Storage').read('ncstart.app.js')) {
setTimeout(() => { setTimeout(() => {
s.welcomed = true s.welcomed = true
require('Storage').write('ncstart.settings.json', s) require('Storage').write('ncstart.json', s)
load('ncstart.app.js') load('ncstart.app.js')
}) })
} }

View File

@ -1,3 +0,0 @@
{
"welcomed": false
}

View File

@ -1,13 +1,12 @@
// The welcome app is special, and gets to use global settings
(function(back) { (function(back) {
let settings = require('Storage').readJSON('ncstart.settings.json', 1) let settings = require('Storage').readJSON('ncstart.json', 1)
|| require('Storage').readJSON('setting.json', 1) || {} || require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({ E.showMenu({
'': { 'title': 'NCEU Startup' }, '': { 'title': 'NCEU Startup' },
'Run on Next Boot': { 'Run on Next Boot': {
value: !settings.welcomed, value: !settings.welcomed,
format: v => v ? 'OK' : 'No', format: v => v ? 'OK' : 'No',
onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}), onchange: v => require('Storage').write('ncstart.json', {welcomed: !v}),
}, },
'Run Now': () => load('ncstart.app.js'), 'Run Now': () => load('ncstart.app.js'),
'< Back': back, '< Back': back,

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Use BTN2 for settings menu like other clocks 0.02: Use BTN2 for settings menu like other clocks
0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting 0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting
0.04: Don't overwrite existing settings on app update

View File

@ -1,5 +0,0 @@
{
color:0,
drawMode:"fill",
menuButton:22
}

View File

@ -18,3 +18,5 @@
0.14: Reduce memory usage when running app settings page 0.14: Reduce memory usage when running app settings page
0.15: Reduce memory usage when running default clock chooser (#294) 0.15: Reduce memory usage when running default clock chooser (#294)
0.16: Reduce memory usage further when running app settings page 0.16: Reduce memory usage further when running app settings page
0.17: Remove need for "settings" in appid.info
0.18: Don't overwrite existing settings on app update

View File

@ -1,25 +0,0 @@
{
ble: true, // Bluetooth enabled by default
blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used?
log: false, // Do log messages appear on screen?
timeout: 10, // Default LCD timeout in seconds
vibrate: true, // Vibration enabled by default. App must support
beep: "vib", // Beep enabled by default. App must support
timezone: 0, // Set the timezone for the device
HID : false, // BLE HID mode, off by default
clock: null, // a string for the default clock's name
"12hour" : false, // 12 or 24 hour clock?
// welcomed : undefined/true (whether welcome app should show)
brightness: 1, // LCD brightness from 0 to 1
options: {
wakeOnBTN1: true,
wakeOnBTN2: true,
wakeOnBTN3: true,
wakeOnFaceUp: false,
wakeOnTouch: false,
wakeOnTwist: true,
twistThreshold: 819.2,
twistMaxY: -800,
twistTimeout: 1000
}
}

View File

@ -416,10 +416,19 @@ function showAppSettingsMenu() {
'': { 'title': 'App Settings' }, '': { 'title': 'App Settings' },
'< Back': ()=>showMainMenu(), '< Back': ()=>showMainMenu(),
} }
const apps = storage.list(/\.info$/) const apps = storage.list(/\.settings\.js$/)
.map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?{sortorder:a.sortorder,name:a.name,settings:a.settings}:undefined}) .map(s => s.substr(0, s.length-12))
.filter(app => app) // filter out any undefined apps .map(id => {
.sort((a, b) => a.sortorder - b.sortorder) const a=storage.readJSON(id+'.info',1);
return {id:id,name:a.name,sortorder:a.sortorder};
})
.sort((a, b) => {
const n = (0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
})
if (apps.length === 0) { if (apps.length === 0) {
appmenu['No app has settings'] = () => { }; appmenu['No app has settings'] = () => { };
} }
@ -433,10 +442,7 @@ function showAppSettings(app) {
E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`); E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`);
setWatch(showAppSettingsMenu, BTN1, { repeat: false }); setWatch(showAppSettingsMenu, BTN1, { repeat: false });
} }
let appSettings = storage.read(app.settings); let appSettings = storage.read(app.id+'.settings.js');
if (!appSettings) {
return showError('Missing settings');
}
try { try {
appSettings = eval(appSettings); appSettings = eval(appSettings);
} catch (e) { } catch (e) {

View File

@ -7,3 +7,4 @@
0.07: Run again when updated 0.07: Run again when updated
Don't run again when settings app is updated (or absent) Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings Add "Run Now" option to settings
0.08: Don't overwrite existing settings on app update

View File

@ -1,11 +1,11 @@
(function() { (function() {
let s = require('Storage').readJSON('welcome.settings.json', 1) let s = require('Storage').readJSON('welcome.json', 1)
|| require('Storage').readJSON('setting.json', 1) || require('Storage').readJSON('setting.json', 1)
|| {welcomed: true} // do NOT run if global settings are also absent || {welcomed: true} // do NOT run if global settings are also absent
if (!s.welcomed && require('Storage').read('welcome.app.js')) { if (!s.welcomed && require('Storage').read('welcome.app.js')) {
setTimeout(() => { setTimeout(() => {
s.welcomed = true s.welcomed = true
require('Storage').write('welcome.settings.json', {welcomed: "yes"}) require('Storage').write('welcome.json', {welcomed: "yes"})
load('welcome.app.js') load('welcome.app.js')
}) })
} }

View File

@ -1,3 +0,0 @@
{
"welcomed": false
}

View File

@ -1,13 +1,12 @@
// The welcome app is special, and gets to use global settings
(function(back) { (function(back) {
let settings = require('Storage').readJSON('welcome.settings.json', 1) let settings = require('Storage').readJSON('welcome.json', 1)
|| require('Storage').readJSON('setting.json', 1) || {} || require('Storage').readJSON('setting.json', 1) || {}
E.showMenu({ E.showMenu({
'': { 'title': 'Welcome App' }, '': { 'title': 'Welcome App' },
'Run on Next Boot': { 'Run on Next Boot': {
value: !settings.welcomed, value: !settings.welcomed,
format: v => v ? 'OK' : 'No', format: v => v ? 'OK' : 'No',
onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}), onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
}, },
'Run Now': () => load('welcome.app.js'), 'Run Now': () => load('welcome.app.js'),
'< Back': back, '< Back': back,

View File

@ -6,3 +6,5 @@
0.07: Add settings: percentage/color/charger icon 0.07: Add settings: percentage/color/charger icon
0.08: Draw percentage as inverted on monochrome battery 0.08: Draw percentage as inverted on monochrome battery
0.09: Fix regression stopping correct widget updates 0.09: Fix regression stopping correct widget updates
0.10: Add 'hide if charge greater than'
0.11: Don't overwrite existing settings on app update

View File

@ -3,7 +3,7 @@
* @param {function} back Use back() to return to settings menu * @param {function} back Use back() to return to settings menu
*/ */
(function(back) { (function(back) {
const SETTINGS_FILE = 'widbatpc.settings.json' const SETTINGS_FILE = 'widbatpc.json'
const COLORS = ['By Level', 'Green', 'Monochrome'] const COLORS = ['By Level', 'Green', 'Monochrome']
// initialize with default settings... // initialize with default settings...
@ -11,21 +11,22 @@
'color': COLORS[0], 'color': COLORS[0],
'percentage': true, 'percentage': true,
'charger': true, 'charger': true,
'hideifmorethan': 100,
} }
// ...and overwrite them with any saved values // ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings // This way saved values are preserved if a new version adds more settings
const storage = require('Storage') const storage = require('Storage')
const saved = storage.readJSON(SETTINGS_FILE, 1) || {} const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
for (const key in saved) { for (const key in saved) {
s[key] = saved[key] s[key] = saved[key];
} }
// creates a function to safe a specific setting, e.g. save('color')(1) // creates a function to safe a specific setting, e.g. save('color')(1)
function save(key) { function save(key) {
return function (value) { return function (value) {
s[key] = value s[key] = value;
storage.write(SETTINGS_FILE, s) storage.write(SETTINGS_FILE, s);
WIDGETS["batpc"].reload() WIDGETS["batpc"].reload();
} }
} }
@ -51,7 +52,15 @@
const newIndex = (oldIndex + 1) % COLORS.length const newIndex = (oldIndex + 1) % COLORS.length
s.color = COLORS[newIndex] s.color = COLORS[newIndex]
save('color')(s.color) save('color')(s.color)
}
}, },
'Hide if >': {
value: s.hideifmorethan||100,
min: 10,
max : 100,
step: 10,
format: x => x+"%",
onchange: save('hideifmorethan'),
}, },
} }
E.showMenu(menu) E.showMenu(menu)

View File

@ -1,9 +1,4 @@
(function(){ (function(){
const DEFAULTS = {
'color': 'By Level',
'percentage': true,
'charger': true,
}
const COLORS = { const COLORS = {
'white': -1, 'white': -1,
'charging': 0x07E0, // "Green" 'charging': 0x07E0, // "Green"
@ -11,15 +6,24 @@ const COLORS = {
'ok': 0xFD20, // "Orange" 'ok': 0xFD20, // "Orange"
'low':0xF800, // "Red" 'low':0xF800, // "Red"
} }
const SETTINGS_FILE = 'widbatpc.settings.json' const SETTINGS_FILE = 'widbatpc.json'
let settings let settings
function loadSettings() { function loadSettings() {
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {} settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}
const DEFAULTS = {
'color': 'By Level',
'percentage': true,
'charger': true,
'hideifmorethan': 100,
};
Object.keys(DEFAULTS).forEach(k=>{
if (settings[k]===undefined) settings[k]=DEFAULTS[k]
});
} }
function setting(key) { function setting(key) {
if (!settings) { loadSettings() } if (!settings) { loadSettings() }
return (key in settings) ? settings[key] : DEFAULTS[key] return settings[key];
} }
const levelColor = (l) => { const levelColor = (l) => {
@ -45,16 +49,27 @@ const levelColor = (l) => {
const chargerColor = () => { const chargerColor = () => {
return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging
} }
// sets width, returns true if it changed
function setWidth() { function setWidth() {
WIDGETS["batpc"].width = 40; var w = 40;
if (Bangle.isCharging() && setting('charger')) { if (Bangle.isCharging() && setting('charger'))
WIDGETS["batpc"].width += 16; w += 16;
} if (E.getBattery() > setting('hideifmorethan'))
w = 0;
var changed = WIDGETS["batpc"].width != w;
WIDGETS["batpc"].width = w;
return changed;
} }
function draw() { function draw() {
// if hidden, don't draw
if (!WIDGETS["batpc"].width) return;
// else...
var s = 39; var s = 39;
var x = this.x, y = this.y; var x = this.x, y = this.y;
const l = E.getBattery(),
c = levelColor(l);
const xl = x+4+l*(s-12)/100
if (Bangle.isCharging() && setting('charger')) { if (Bangle.isCharging() && setting('charger')) {
g.setColor(chargerColor()).drawImage(atob( g.setColor(chargerColor()).drawImage(atob(
"DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); "DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
@ -64,9 +79,7 @@ function draw() {
g.fillRect(x,y+2,x+s-4,y+21); g.fillRect(x,y+2,x+s-4,y+21);
g.clearRect(x+2,y+4,x+s-6,y+19); g.clearRect(x+2,y+4,x+s-6,y+19);
g.fillRect(x+s-3,y+10,x+s,y+14); g.fillRect(x+s-3,y+10,x+s,y+14);
const l = E.getBattery(),
c = levelColor(l);
const xl = x+4+l*(s-12)/100
g.setColor(c).fillRect(x+4,y+6,xl,y+17); g.setColor(c).fillRect(x+4,y+6,xl,y+17);
g.setColor(-1); g.setColor(-1);
if (!setting('percentage')) { if (!setting('percentage')) {
@ -97,20 +110,24 @@ function reload() {
g.clear(); g.clear();
Bangle.drawWidgets(); Bangle.drawWidgets();
} }
// update widget - redraw just widget, or all widgets if size changed
function update() {
if (setWidth()) Bangle.drawWidgets();
else WIDGETS["batpc"].draw();
}
Bangle.on('charging',function(charging) { Bangle.on('charging',function(charging) {
if(charging) Bangle.buzz(); if(charging) Bangle.buzz();
setWidth(); update();
Bangle.drawWidgets(); // relayout widgets
g.flip(); g.flip();
}); });
var batteryInterval; var batteryInterval;
Bangle.on('lcdPower', function(on) { Bangle.on('lcdPower', function(on) {
if (on) { if (on) {
WIDGETS["batpc"].draw(); update();
// refresh once a minute if LCD on // refresh once a minute if LCD on
if (!batteryInterval) if (!batteryInterval)
batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000); batteryInterval = setInterval(update, 60000);
} else { } else {
if (batteryInterval) { if (batteryInterval) {
clearInterval(batteryInterval); clearInterval(batteryInterval);

View File

@ -39,10 +39,26 @@ try{
const APP_KEYS = [ const APP_KEYS = [
'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type', 'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type',
'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator', 'sortorder', 'readme', 'custom', 'interface', 'storage', 'data', 'allow_emulator',
]; ];
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate']; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate'];
const DATA_KEYS = ['name', 'wildcard', 'storageFile'];
const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info
function globToRegex(pattern) {
const ESCAPE = '.*+-?^${}()|[]\\';
const regex = pattern.replace(/./g, c => {
switch (c) {
case '?': return '.';
case '*': return '.*';
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
}
});
return new RegExp('^'+regex+'$');
}
const isGlob = f => /[?*]/.test(f)
// All storage+data files in all apps: {app:<appid>,[file:<storage.name> | data:<data.name|data.wildcard>]}
let allFiles = [];
apps.forEach((app,appIdx) => { apps.forEach((app,appIdx) => {
if (!app.id) ERROR(`App ${appIdx} has no id`); if (!app.id) ERROR(`App ${appIdx} has no id`);
//console.log(`Checking ${app.id}...`); //console.log(`Checking ${app.id}...`);
@ -74,9 +90,13 @@ apps.forEach((app,appIdx) => {
var fileNames = []; var fileNames = [];
app.storage.forEach((file) => { app.storage.forEach((file) => {
if (!file.name) ERROR(`App ${app.id} has a file with no name`); if (!file.name) ERROR(`App ${app.id} has a file with no name`);
if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`);
let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS)
if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`)
if (fileNames.includes(file.name)) if (fileNames.includes(file.name))
ERROR(`App ${app.id} file ${file.name} is a duplicate`); ERROR(`App ${app.id} file ${file.name} is a duplicate`);
fileNames.push(file.name); fileNames.push(file.name);
allFiles.push({app: app.id, file: file.name});
if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`);
if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`); if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`);
var fileContents = ""; var fileContents = "";
@ -115,6 +135,54 @@ apps.forEach((app,appIdx) => {
if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`); if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`);
} }
}); });
let dataNames = [];
(app.data||[]).forEach((data)=>{
if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`);
if (dataNames.includes(data.name||data.wildcard))
ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`);
dataNames.push(data.name||data.wildcard)
allFiles.push({app: app.id, data: (data.name||data.wildcard)});
if ('name' in data && 'wildcard' in data)
ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`);
if (isGlob(data.name))
ERROR(`App ${app.id} data file name ${data.name} contains wildcards`);
if (data.wildcard) {
if (!isGlob(data.wildcard))
ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`);
if (data.wildcard.replace(/\?|\*/g,'') === '')
ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`);
else if (data.wildcard.replace(/\?|\*/g,'').length < 3)
WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`);
else if (!data.wildcard.includes(app.id))
WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`);
}
let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS)
if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`)
if ('storageFile' in data && typeof data.storageFile !== 'boolean')
ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`);
for (const key in data) {
if (!DATA_KEYS.includes(key))
ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`);
}
});
// prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?)
if (dataNames.includes(app.id+".settings.json") && !dataNames.includes(app.id+".json"))
WARN(`App ${app.id} uses data file ${app.id+'.settings.json'} instead of ${app.id+'.json'}`)
// settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?)
if (fileNames.includes(app.id+".settings.json"))
WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`)
if (fileNames.includes(app.id+".json"))
WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`)
// warn if storage file matches data file of same app
dataNames.forEach(dataName=>{
const glob = globToRegex(dataName)
fileNames.forEach(fileName=>{
if (glob.test(fileName)) {
if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`)
else WARN(`App ${app.id} storage file ${fileName} is also listed in data`)
}
})
})
//console.log(fileNames); //console.log(fileNames);
if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`);
if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`);
@ -123,3 +191,20 @@ apps.forEach((app,appIdx) => {
if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`);
} }
}); });
// Do not allow files from different apps to collide
let fileA
while(fileA=allFiles.pop()) {
const nameA = (fileA.file||fileA.data),
globA = globToRegex(nameA),
typeA = fileA.file?'storage':'data'
allFiles.forEach(fileB => {
const nameB = (fileB.file||fileB.data),
globB = globToRegex(nameB),
typeB = fileB.file?'storage':'data'
if (globA.test(nameB)||globB.test(nameA)) {
if (isGlob(nameA)||isGlob(nameB))
ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`)
else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
}
})
}

View File

@ -113,6 +113,7 @@
<div class="panel"> <div class="panel">
<div class="panel-header" style="text-align:right"> <div class="panel-header" style="text-align:right">
<button class="btn refresh">Refresh...</button> <button class="btn refresh">Refresh...</button>
<button class="btn btn-primary updateapps hidden">Update X apps</button>
</div> </div>
<div class="panel-body columns"><!-- apps go here --></div> <div class="panel-body columns"><!-- apps go here --></div>
</div> </div>

View File

@ -60,8 +60,6 @@ var AppInfo = {
if (app.type && app.type!="app") json.type = app.type; if (app.type && app.type!="app") json.type = app.type;
if (fileContents.find(f=>f.name==app.id+".app.js")) if (fileContents.find(f=>f.name==app.id+".app.js"))
json.src = app.id+".app.js"; json.src = app.id+".app.js";
if (fileContents.find(f=>f.name==app.id+".settings.js"))
json.settings = app.id+".settings.js";
if (fileContents.find(f=>f.name==app.id+".img")) if (fileContents.find(f=>f.name==app.id+".img"))
json.icon = app.id+".img"; json.icon = app.id+".img";
if (app.sortorder) json.sortorder = app.sortorder; if (app.sortorder) json.sortorder = app.sortorder;
@ -69,13 +67,48 @@ var AppInfo = {
var fileList = fileContents.map(storageFile=>storageFile.name); var fileList = fileContents.map(storageFile=>storageFile.name);
fileList.unshift(appJSONName); // do we want this? makes life easier! fileList.unshift(appJSONName); // do we want this? makes life easier!
json.files = fileList.join(","); json.files = fileList.join(",");
if ('data' in app) {
let data = {dataFiles: [], storageFiles: []};
// add "data" files to appropriate list
app.data.forEach(d=>{
if (d.storageFile) data.storageFiles.push(d.name||d.wildcard)
else data.dataFiles.push(d.name||d.wildcard)
})
const dataString = AppInfo.makeDataString(data)
if (dataString) json.data = dataString
}
fileContents.push({ fileContents.push({
name : appJSONName, name : appJSONName,
content : JSON.stringify(json) content : JSON.stringify(json)
}); });
resolve(fileContents); resolve(fileContents);
}); });
} },
// (<appid>.info).data holds filenames of data: both regular and storageFiles
// These are stored as: (note comma vs semicolons)
// "fil1,file2", "file1,file2;storageFileA,storageFileB" or ";storageFileA"
/**
* Convert appid.info "data" to object with file names/patterns
* Passing in undefined works
* @param data "data" as stored in appid.info
* @returns {{storageFiles:[], dataFiles:[]}}
*/
parseDataString(data) {
data = data || '';
let [files = [], storage = []] = data.split(';').map(d => d.split(','))
return {dataFiles: files, storageFiles: storage}
},
/**
* Convert object with file names/patterns to appid.info "data" string
* Passing in an incomplete object will not work
* @param data {{storageFiles:[], dataFiles:[]}}
* @returns {string} "data" to store in appid.info
*/
makeDataString(data) {
if (!data.dataFiles.length && !data.storageFiles.length) { return '' }
if (!data.storageFiles.length) { return data.dataFiles.join(',') }
return [data.dataFiles.join(','),data.storageFiles.join(',')].join(';')
},
}; };
if ("undefined"!=typeof module) if ("undefined"!=typeof module)

View File

@ -94,10 +94,29 @@ getInstalledApps : () => {
}); });
}, },
removeApp : app => { // expects an appid.info structure (i.e. with `files`) removeApp : app => { // expects an appid.info structure (i.e. with `files`)
if (app.files === '') return Promise.resolve(); // nothing to erase if (!app.files && !app.data) return Promise.resolve(); // nothing to erase
Progress.show({title:`Removing ${app.name}`,sticky:true}); Progress.show({title:`Removing ${app.name}`,sticky:true});
var cmds = app.files.split(',').map(file=>{ let cmds = '\x10const s=require("Storage");\n';
return `\x10require("Storage").erase(${toJS(file)});\n`; // remove App files: regular files, exact names only
cmds += app.files.split(',').map(file => `\x10s.erase(${toJS(file)});\n`).join("");
// remove app Data: (dataFiles and storageFiles)
const data = AppInfo.parseDataString(app.data)
const isGlob = f => /[?*]/.test(f)
// regular files, can use wildcards
cmds += data.dataFiles.map(file => {
if (!isGlob(file)) return `\x10s.erase(${toJS(file)});\n`;
const regex = new RegExp(globToRegex(file))
return `\x10s.list(${regex}).forEach(f=>s.erase(f));\n`;
}).join("");
// storageFiles, can use wildcards
cmds += data.storageFiles.map(file => {
if (!isGlob(file)) return `\x10s.open(${toJS(file)},'r').erase();\n`;
// storageFiles have a chunk number appended to their real name
const regex = globToRegex(file+'\u0001')
// open() doesn't want the chunk number though
let cmd = `\x10s.list(${regex}).forEach(f=>s.open(f.substring(0,f.length-1),'r').erase());\n`
// using a literal \u0001 char fails (not sure why), so escape it
return cmd.replace('\u0001', '\\x01')
}).join(""); }).join("");
console.log("removeApp", cmds); console.log("removeApp", cmds);
return Comms.reset().then(new Promise((resolve,reject) => { return Comms.reset().then(new Promise((resolve,reject) => {

View File

@ -207,7 +207,7 @@ function refreshLibrary() {
var version = getVersionInfo(app, appInstalled); var version = getVersionInfo(app, appInstalled);
var versionInfo = version.text; var versionInfo = version.text;
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>"; if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
var readme = `<a href="#" onclick="showReadme('${app.id}')">Read more...</a>`; var readme = `<a class="c-hand" onclick="showReadme('${app.id}')">Read more...</a>`;
var favourite = favourites.find(e => e == app.id); var favourite = favourites.find(e => e == app.id);
return `<div class="tile column col-6 col-sm-12 col-xs-12"> return `<div class="tile column col-6 col-sm-12 col-xs-12">
<div class="tile-icon"> <div class="tile-icon">
@ -349,6 +349,14 @@ function updateApp(app) {
.filter(f => f !== app.id + '.info') .filter(f => f !== app.id + '.info')
.filter(f => !app.storage.some(s => s.name === f)) .filter(f => !app.storage.some(s => s.name === f))
.join(','); .join(',');
let data = AppInfo.parseDataString(remove.data)
if ('data' in app) {
// only remove data files which are no longer declared in new app version
const removeData = (f) => !app.data.some(d => (d.name || d.wildcard)===f)
data.dataFiles = data.dataFiles.filter(removeData)
data.storageFiles = data.storageFiles.filter(removeData)
}
remove.data = AppInfo.makeDataString(data)
return Comms.removeApp(remove); return Comms.removeApp(remove);
}).then(()=>{ }).then(()=>{
showToast(`Updating ${app.name}...`); showToast(`Updating ${app.name}...`);
@ -393,10 +401,18 @@ function showLoadingIndicator(id) {
panelbody.innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>'; panelbody.innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>';
} }
function getAppsToUpdate() {
var appsToUpdate = [];
appsInstalled.forEach(appInstalled => {
var app = appNameToApp(appInstalled.id);
if (app.version != appInstalled.version)
appsToUpdate.push(app);
});
return appsToUpdate;
}
function refreshMyApps() { function refreshMyApps() {
var panelbody = document.querySelector("#myappscontainer .panel-body"); var panelbody = document.querySelector("#myappscontainer .panel-body");
var tab = document.querySelector("#tab-myappscontainer a");
tab.setAttribute("data-badge", appsInstalled.length);
panelbody.innerHTML = appsInstalled.map(appInstalled => { panelbody.innerHTML = appsInstalled.map(appInstalled => {
var app = appNameToApp(appInstalled.id); var app = appNameToApp(appInstalled.id);
var version = getVersionInfo(app, appInstalled); var version = getVersionInfo(app, appInstalled);
@ -428,6 +444,17 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
if (icon.classList.contains("icon-download")) handleAppInterface(app); if (icon.classList.contains("icon-download")) handleAppInterface(app);
}); });
}); });
var appsToUpdate = getAppsToUpdate();
var tab = document.querySelector("#tab-myappscontainer a");
var updateApps = document.querySelector("#myappscontainer .updateapps");
if (appsToUpdate.length) {
updateApps.innerHTML = `Update ${appsToUpdate.length} apps`;
updateApps.classList.remove("hidden");
tab.setAttribute("data-badge", `${appsInstalled.length}${appsToUpdate.length}`);
} else {
updateApps.classList.add("hidden");
tab.setAttribute("data-badge", appsInstalled.length);
}
} }
let haveInstalledApps = false; let haveInstalledApps = false;
@ -463,6 +490,22 @@ htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addE
showToast("Getting app list failed, "+err,"error"); showToast("Getting app list failed, "+err,"error");
}); });
})); }));
htmlToArray(document.querySelectorAll(".btn.updateapps")).map(button => button.addEventListener("click", () => {
var appsToUpdate = getAppsToUpdate();
var count = appsToUpdate.length;
function updater() {
if (!appsToUpdate.length) return;
var app = appsToUpdate.pop();
return updateApp(app).then(function() {
return updater();
});
}
updater().then(err => {
showToast(`Updated ${count} apps`,"success");
}).catch(err => {
showToast("Update failed, "+err,"error");
});
}));
connectMyDeviceBtn.addEventListener("click", () => { connectMyDeviceBtn.addEventListener("click", () => {
if (connectMyDeviceBtn.classList.contains('is-connected')) { if (connectMyDeviceBtn.classList.contains('is-connected')) {
Comms.disconnectDevice(); Comms.disconnectDevice();
@ -613,4 +656,3 @@ document.getElementById("installfavourite").addEventListener("click",event=>{
showToast("App Install failed, "+err,"error"); showToast("App Install failed, "+err,"error");
}); });
}); });

View File

@ -8,6 +8,18 @@ function escapeHtml(text) {
}; };
return text.replace(/[&<>"']/g, function(m) { return map[m]; }); return text.replace(/[&<>"']/g, function(m) { return map[m]; });
} }
// simple glob to regex conversion, only supports "*" and "?" wildcards
function globToRegex(pattern) {
const ESCAPE = '.*+-?^${}()|[]\\';
const regex = pattern.replace(/./g, c => {
switch (c) {
case '?': return '.';
case '*': return '.*';
default: return ESCAPE.includes(c) ? ('\\' + c) : c;
}
});
return new RegExp('^'+regex+'$');
}
function htmlToArray(collection) { function htmlToArray(collection) {
return [].slice.call(collection); return [].slice.call(collection);
} }
@ -49,7 +61,7 @@ function getVersionInfo(appListing, appInstalled) {
var versionText = ""; var versionText = "";
var canUpdate = false; var canUpdate = false;
function clicky(v) { function clicky(v) {
return `<a href="#" onclick="showChangeLog('${appListing.id}')">${v}</a>`; return `<a class="c-hand" onclick="showChangeLog('${appListing.id}')">${v}</a>`;
} }
if (!appInstalled) { if (!appInstalled) {