Merge branch 'espruino-master'

pull/421/head
Johan Bové 2020-05-14 22:14:19 +02:00
commit 1ac35546ba
349 changed files with 20413 additions and 1103 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
.htaccess
node_modules
package-lock.json
.DS_Store
*.js.bak
appdates.csv

View File

@ -5,3 +5,16 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong
* Added optional `README.md` file for apps
* Remove 2v04 version warning, add links in About to official/developer versions
* Fix issue removing an app that was just installed (fix #253)
* Add `Favourite` functionality
* Version number now clickable even when you're at the latest version (fix #291)
* Rewrite 'getInstalledApps' to minimize RAM usage
* Added code to handle Settings
* Added espruinotools.js for pretokenisation
* Included image and compression tools in repo
* Added better upload of large files (incl. compression)
* URL fetch is now async
* Adding '#search' after the URL (when not the name of a 'filter' chip) will set up search for that term
* If `bin/pre-publish.sh` has been run and recent.csv created, add 'Sort By' chip
* New 'espruinotools' which fixes pretokenise issue when ID follows ID (fix #416)

View File

@ -6,7 +6,7 @@ Bangle.js App Loader (and Apps)
* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps)
* Try the **development version** at [github.io](https://espruino.github.io/BangleApps/)
**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By
**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By
submitting code to this repository you confirm that you are happy with it being MIT licensed,
and that it is not licensed in another way that would make this impossible.
@ -29,7 +29,7 @@ Check out:
## What filenames are used
Filenames in storage are limited to 8 characters. To
Filenames in storage are limited to 28 characters. To
easily distinguish between file types, we use the following:
* `stuff.info` is JSON that describes an app - this is auto-generated by the App Loader
@ -202,6 +202,11 @@ and which gives information about the app for the Launcher.
"files:"file1,file2,file3",
// added by BangleApps loader on upload - lists all files
// 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
// (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.
// this should only really be used to put system
// stuff at the top
]
}
```
* 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.
* 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
@ -328,18 +341,21 @@ See [apps/gpsrec/interface.html](the GPS Recorder) for a full example.
Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings".
To do so, the app needs to include a `settings.js` file, containing a single function
that handles configuring the app.
When the app settings are opened, this function is called with one
When the app settings are opened, this function is called with one
argument, `back`: a callback to return to the settings menu.
Usually it will save any information in `app.json` where `app` is the name
of your app - so you should change the example accordingly.
Example `settings.js`
```js
// make sure to enclose the function in parentheses
(function(back) {
let settings = require('Storage').readJSON('app.settings.json',1)||{};
let settings = require('Storage').readJSON('app.json',1)||{};
function save(key, value) {
settings[key] = value;
require('Storage').write('app.settings.json',settings);
}
require('Storage').write('app.json',settings);
}
const appMenu = {
'': {'title': 'App Settings'},
'< Back': back,
@ -351,19 +367,20 @@ Example `settings.js`
E.showMenu(appMenu)
})
```
In this example the app needs to add both `app.settings.js` and
`app.settings.json` to `apps.json`:
In this example the app needs to add `app.settings.js` to `storage` in `apps.json`.
It should also add `app.json` to `data`, to make sure it is cleaned up when the app is uninstalled.
```json
{ "id": "app",
...
"storage": [
...
{"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

973
apps.json

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
# App Name
Describe the app...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -5,6 +5,7 @@
"version":"0.01",
"description": "A detailed description of my great app",
"tags": "",
"readme": "README.md",
"storage": [
{"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true}

View File

@ -0,0 +1,25 @@
# Widget Name
Describe the app...
Add screen shots (if possible) to the app folder and link then into this file with ![](<name>.png)
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -7,6 +7,7 @@
"description": "A detailed description of my great widget",
"tags": "widget",
"type": "widget",
"readme": "README.md",
"storage": [
{"name":"7chname.wid.js","url":"widget.js"}
]

View File

@ -2,3 +2,4 @@
0.02: Update version checker for new filename type
0.03: Actual pixels as of 5 Mar 2020
0.04: Actual pixels as of 9 Mar 2020
0.05: Actual pixels as of 27 Apr 2020

File diff suppressed because one or more lines are too long

View File

@ -6,3 +6,5 @@
0.09: center date, remove box around it, internal refactor to remove redundant code.
0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed
0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date
0.12: Fix regression after 0.11
0.13: Fix broken date padding (fix #376)

View File

@ -1,7 +1,3 @@
// eliminate ide undefined errors
let g;
let Bangle;
// http://forum.espruino.com/conversations/345155/#comment15172813
const locale = require('locale');
const p = Math.PI / 2;
@ -88,7 +84,7 @@ const drawDate = () => {
const dayString = locale.dow(currentDate, true);
// pad left date
const dateString = (currentDate.getDate() < 10) ? '0' : '' + currentDate.getDate().toString();
const dateString = ("0"+currentDate.getDate().toString()).substr(-2);
const dateDisplay = `${dayString}-${dateString}`;
// console.log(`${dayString}|${dateString}`);
// center date

BIN
apps/activepedom/10600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
apps/activepedom/1600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

BIN
apps/activepedom/600.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

View File

@ -0,0 +1,4 @@
0.01: New Widget!
0.02: Distance calculation and display
0.03: Data logging and display
0.04: Steps are set to 0 in log on new day

View File

@ -0,0 +1,72 @@
# Active Pedometer
Pedometer that filters out arm movement and displays a step goal progress.
I changed the step counting algorithm completely.
Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long.
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
When you reach the step threshold, the steps needed to reach the threshold are counted as well.
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
## Screenshots
* 600 steps
![](600.png)
* 1600 steps
![](1600.png)
* 10600 steps
![](10600.png)
## Features Widget
* Two line display
* Can display distance (in km) or steps in each line
* Large number for good readability
* Small number with the exact steps counted or more exact distance
* Large number is displayed in green when status is 'active'
* Progress bar for step goal
* Counts steps only if they are reached in a certain time
* Filters out steps where time between two steps is too long or too short
* Step detection sensitivity from firmware can be configured
* Steps are saved to a file and read-in at start (to not lose step progress)
* Settings can be changed in Settings - App/widget settings - Active Pedometer
## Features App
* The app accesses the data stored for the current day
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
## Data storage
* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data)
* One file is created for each day
* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime
* 'now' is UNIX timestamp in ms
* You can use the app to watch a steps graph
* You can import the file into Excel
* The file does not include a header
* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400)
* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss
## Settings
* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100
* Min time (ms): Minimum time between two steps in milliseconds, steps will not be counted if fallen below. Standard: 240
* Step threshold: How many steps are needed to reach 'active' mode. If you do not reach the threshold in the 'Active Reset' time, the steps are not counted. Standard: 30
* Act.Res. (ms): Active Reset. After how many miliseconds will the 'active mode' reset. You have to reach the step threshold in this time, otherwise the steps are not counted. Standard: 30000
* Step sens.: Step Sensitivity. How sensitive should the sted detection be? This changes sensitivity in step detection in the firmware. Standard in firmware: 80
* Step goal: This is your daily step goal. Standard: 10000
* Step length: Length of one step in cm. Standard: 75
* Line One: What to display in line one, steps or distance. Standard: steps
* Line Two: What to display in line two, steps or distance. Standard: distance
## Releases
* Offifical app loader: https://github.com/espruino/BangleApps/tree/master/apps/activepedom (https://banglejs.com/apps)
* Forked app loader: https://github.com/Purple-Tentacle/BangleApps/tree/master/apps/activepedom (https://purple-tentacle.github.io/BangleApps/#widget)
* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/pedometer
## Requests
If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIGDvAEDgP+ApMD/4FVEZY1FABcP8AFDn/wAod/AocB//4AoUHAokPAokf5/8AocfAoc+j5HDvgFEvEf7+AAoP4AoJCC+E/54qCsE/wYkDn+AAos8AohZDj/AAohrEp4FEs5xEuJfDgF5Aon4GgYFBGgZOBnyJD+EeYgfgj4FEh6VD4AFDh+AAIJMCBoIFFLQQtBgYFCHIIFDjA3BC4I="))

165
apps/activepedom/app.js Normal file
View File

@ -0,0 +1,165 @@
(() => {
//Graph module, as long as modules are not added by the app loader
Modules.addCached("graph",function(){exports.drawAxes=function(b,c,a){function h(a){return e+m*(a-t)/x}function l(a){return f+g-g*(a-n)/u}var k=a.padx||0,d=a.pady||0,t=-k,w=c.length+k-1,n=(void 0!==a.miny?a.miny:a.miny=c.reduce(function(a,b){return Math.min(a,b)},c[0]))-d;c=(void 0!==a.maxy?a.maxy:a.maxy=c.reduce(function(a,b){return Math.max(a,b)},c[0]))+d;a.gridy&&(d=a.gridy,n=d*Math.floor(n/d),c=d*Math.ceil(c/d));var e=a.x||0,f=a.y||0,m=a.width||b.getWidth()-(e+1),g=a.height||b.getHeight()-(f+1);a.axes&&(null!==a.ylabel&&
(e+=6,m-=6),null!==a.xlabel&&(g-=6));a.title&&(f+=6,g-=6);a.axes&&(b.drawLine(e,f,e,f+g),b.drawLine(e,f+g,e+m,f+g));a.title&&(b.setFontAlign(0,-1),b.drawString(a.title,e+m/2,f-6));var x=w-t,u=c-n;u||(u=1);if(a.gridx){b.setFontAlign(0,-1,0);var v=a.gridx;for(d=Math.ceil((t+k)/v)*v;d<=w-k;d+=v){var r=h(d),p=a.xlabel?a.xlabel(d):d;b.setPixel(r,f+g-1);var q=b.stringWidth(p)/2;null!==a.xlabel&&r>q&&b.getWidth()>r+q&&b.drawString(p,r,f+g+2)}}if(a.gridy)for(b.setFontAlign(0,0,1),d=n;d<=c;d+=a.gridy)k=l(d),
p=a.ylabel?a.ylabel(d):d,b.setPixel(e+1,k),q=b.stringWidth(p)/2,null!==a.ylabel&&k>q&&b.getHeight()>k+q&&b.drawString(p,e-5,k+1);b.setFontAlign(-1,-1,0);return{x:e,y:f,w:m,h:g,getx:h,gety:l}};exports.drawLine=function(b,c,a){a=a||{};a=exports.drawAxes(b,c,a);var h=!0,l;for(l in c)h?b.moveTo(a.getx(l),a.gety(c[l])):b.lineTo(a.getx(l),a.gety(c[l])),h=!1;return a};exports.drawBar=function(b,c,a){a=a||{};a.padx=1;a=exports.drawAxes(b,c,a);for(var h in c)b.fillRect(a.getx(h-.5)+1,a.gety(c[h]),a.getx(h+
.5)-1,a.gety(0));return a}});
const storage = require("Storage");
const SETTINGS_FILE = 'activepedom.settings.json';
var history = 86400000; // 28800000=8h 43200000=12h //86400000=24h
//return setting
function setting(key) {
//define default settings
const DEFAULTS = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
'stepLength' : 75,
};
if (!settings) { loadSettings(); }
return (key in settings) ? settings[key] : DEFAULTS[key];
}
//Convert ms to time
function getTime(t) {
date = new Date(t);
offset = date.getTimezoneOffset() / 60;
//var milliseconds = parseInt((t % 1000) / 100),
seconds = Math.floor((t / 1000) % 60);
minutes = Math.floor((t / (1000 * 60)) % 60);
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
hours = hours - offset;
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
}
function getDate(t) {
date = new Date(t*1);
year = date.getFullYear();
month = date.getMonth()+1; //month is zero-based
day = date.getDate();
month = (month < 10) ? "0" + month : month;
day = (day < 10) ? "0" + day : day;
return year + "-" + month + "-" + day;
}
//columns: 0=time, 1=stepsCounted, 2=active, 3=stepsTooShort, 4=stepsTooLong, 5=stepsOutsideTime
function getArrayFromCSV(file, column) {
i = 0;
array = [];
now = new Date();
while ((nextLine = file.readLine())) { //as long as there is a next line
if(nextLine) {
dataSplitted = nextLine.split(','); //split line,
diff = now - dataSplitted[0]; //calculate difference between now and stored time
if (diff <= history) { //only entries from the last x ms
array.push(dataSplitted[column]);
}
}
i++;
}
return array;
}
function drawGraph() {
//times
// actives = getArrayFromCSV(csvFile, 2);
// shorts = getArrayFromCSV(csvFile, 3);
// longs = getArrayFromCSV(csvFile, 4);
// outsides = getArrayFromCSV(csvFile, 5); //array.push(dataSplitted[5].slice(0,-1));
now = new Date();
month = now.getMonth() + 1;
if (month < 10) month = "0" + month;
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data";
var csvFile = storage.open(filename, "r");
times = getArrayFromCSV(csvFile, 0);
first = getDate(times[0]) + " " + getTime(times[0]); //first entry in datafile
last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); //last entry in datafile
//free memory
csvFile = undefined;
times = undefined;
//steps
var csvFile = storage.open(filename, "r");
steps = getArrayFromCSV(csvFile, 1);
first = first + " " + steps[0] + "/" + setting('stepGoal');
last = last + " " + steps[steps.length-1] + "/" + setting('stepGoal');
//define y-axis grid labels
stepsLastEntry = steps[steps.length-1];
if (stepsLastEntry < 1000) gridyValue = 100;
if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 1000;
if (stepsLastEntry > 10000) gridyValue = 5000;
//draw
drawMenu();
g.drawString("First: " + first, 10, 30);
g.drawString(" Last: " + last, 10, 40);
require("graph").drawLine(g, steps, {
//title: "Steps Counted",
axes : true,
gridy : gridyValue,
y : 60, //offset on screen
x : 5, //offset on screen
});
//free memory from big variables
allData = undefined;
allDataFile = undefined;
csvFile = undefined;
times = undefined;
}
function drawMenu () {
g.clear();
g.setFont("6x8", 1);
g.drawString("BTN1:Timespan | BTN2:Draw", 20, 10);
g.drawString("Timespan: " + history/1000/60/60 + " hours", 20, 20);
}
setWatch(function() { //BTN1
switch(history) {
case 3600000 : //1h
history = 14400000; //4h
break;
case 86400000 : //24
history = 3600000; //1h
break;
default :
history = history + 14400000; //4h
break;
}
drawMenu();
}, BTN1, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN2
g.setFont("6x8", 2);
g.drawString ("Drawing...",30,60);
drawGraph();
}, BTN2, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN3
}, BTN3, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN4
}, BTN4, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN5
}, BTN5, {edge:"rising", debounce:50, repeat:true});
//load settings
let settings;
function loadSettings() {
settings = storage.readJSON(SETTINGS_FILE, 1) || {};
}
drawMenu();
})();

BIN
apps/activepedom/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,112 @@
// This file should contain exactly one function, which shows the app's settings
/**
* @param {function} back Use back() to return to settings menu
*/
(function(back) {
const SETTINGS_FILE = 'activepedom.settings.json';
const LINES = ['Steps', 'Distance'];
// initialize with default settings...
let s = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
'stepLength' : 75,
'lineOne': LINES[0],
'lineTwo': LINES[1],
};
// ...and overwrite them with any saved values
// This way saved values are preserved if a new version adds more settings
const storage = require('Storage');
const saved = storage.readJSON(SETTINGS_FILE, 1) || {};
for (const key in saved) {
s[key] = saved[key];
}
// creates a function to safe a specific setting, e.g. save('color')(1)
function save(key) {
return function (value) {
s[key] = value;
storage.write(SETTINGS_FILE, s);
//WIDGETS["activepedom"].draw();
};
}
const menu = {
'': { 'title': 'Active Pedometer' },
'< Back': back,
'Max time (ms)': {
value: s.cMaxTime,
min: 0,
max: 10000,
step: 100,
onchange: save('cMaxTime'),
},
'Min time (ms)': {
value: s.cMinTime,
min: 0,
max: 500,
step: 10,
onchange: save('cMinTime'),
},
'Step threshold': {
value: s.stepThreshold,
min: 0,
max: 100,
step: 1,
onchange: save('stepThreshold'),
},
'Act.Res. (ms)': {
value: s.intervalResetActive,
min: 100,
max: 100000,
step: 1000,
onchange: save('intervalResetActive'),
},
'Step sens.': {
value: s.stepSensitivity,
min: 0,
max: 1000,
step: 10,
onchange: save('stepSensitivity'),
},
'Step goal': {
value: s.stepGoal,
min: 1000,
max: 100000,
step: 1000,
onchange: save('stepGoal'),
},
'Step length (cm)': {
value: s.stepLength,
min: 1,
max: 150,
step: 1,
onchange: save('stepLength'),
},
'Line One': {
format: () => s.lineOne,
onchange: function () {
// cycles through options
const oldIndex = LINES.indexOf(s.lineOne)
const newIndex = (oldIndex + 1) % LINES.length
s.lineOne = LINES[newIndex]
save('lineOne')(s.lineOne)
},
},
'Line Two': {
format: () => s.lineTwo,
onchange: function () {
// cycles through options
const oldIndex = LINES.indexOf(s.lineTwo)
const newIndex = (oldIndex + 1) % LINES.length
s.lineTwo = LINES[newIndex]
save('lineTwo')(s.lineTwo)
},
},
};
E.showMenu(menu);
});

232
apps/activepedom/widget.js Normal file
View File

@ -0,0 +1,232 @@
(() => {
var stepTimeDiff = 9999; //Time difference between two steps
var startTimeStep = new Date(); //set start time
var stopTimeStep = 0; //Time after one step
var timerResetActive = 0; //timer to reset active
var timerStoreData = 0; //timer to store data
var steps = 0; //steps taken
var stepsCounted = 0; //active steps counted
var active = 0; //x steps in y seconds achieved
var stepGoalPercent = 0; //percentage of step goal
var stepGoalBarLength = 0; //length og progress bar
var lastUpdate = new Date(); //used to reset counted steps on new day
var width = 46; //width of widget
//used for statistics and debugging
var stepsTooShort = 0;
var stepsTooLong = 0;
var stepsOutsideTime = 0;
var distance = 0; //distance travelled
const s = require('Storage');
const SETTINGS_FILE = 'activepedom.settings.json';
const PEDOMFILE = "activepedom.steps.json";
var dataFile;
var storeDataInterval = 5*60*1000; //ms
let settings;
//load settings
function loadSettings() {
settings = s.readJSON(SETTINGS_FILE, 1) || {};
}
function storeData() {
now = new Date();
month = now.getMonth() + 1; //month is 0-based
if (month < 10) month = "0" + month; //leading 0
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; //new file for each day
dataFile = s.open(filename,"a");
if (dataFile) { //check if filen already exists
if (dataFile.getLength() == 0) {
//new day, set steps to 0
stepsCounted = 0;
stepsTooShort = 0;
stepsTooLong = 0;
stepsOutsideTime = 0;
}
dataFile.write([
now.getTime(),
stepsCounted,
active,
stepsTooShort,
stepsTooLong,
stepsOutsideTime,
].join(",")+"\n");
}
dataFile = undefined; //save memory
}
//return setting
function setting(key) {
//define default settings
const DEFAULTS = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
'stepLength' : 75,
};
if (!settings) { loadSettings(); }
return (key in settings) ? settings[key] : DEFAULTS[key];
}
function setStepSensitivity(s) {
function sqr(x) { return x*x; }
var X=sqr(8192-s);
var Y=sqr(8192+s);
Bangle.setOptions({stepCounterThresholdLow:X,stepCounterThresholdHigh:Y});
}
//format number to make them shorter
function kFormatterSteps(num) {
if (num <= 999) return num; //smaller 1.000, return 600 as 600
if (num >= 1000 && num < 10000) { //between 1.000 and 10.000
num = Math.floor(num/100)*100;
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 1600 as 1.6k
}
if (num >= 10000) { //greater 10.000
num = Math.floor(num/1000)*1000;
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; //return 10.600 as 10k
}
}
//Set Active to 0
function resetActive() {
active = 0;
steps = 0;
if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw();
}
function calcSteps() {
stopTimeStep = new Date(); //stop time after each step
stepTimeDiff = stopTimeStep - startTimeStep; //time between steps in milliseconds
startTimeStep = new Date(); //start time again
//Remove step if time between first and second step is too long
if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds
stepsTooLong++; //count steps which are not counted, because time too long
steps--;
}
//Remove step if time between first and second step is too short
if (stepTimeDiff <= setting('cMinTime')) { //milliseconds
stepsTooShort++; //count steps which are not counted, because time too short
steps--;
}
//Step threshold reached
if (steps >= setting('stepThreshold')) {
if (active == 0) {
stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1
stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reach active status
}
active = 1;
clearInterval(timerResetActive); //stop timer which resets active
timerResetActive = setInterval(resetActive, setting('intervalResetActive')); //reset active after timer runs out
steps = 0;
}
if (active == 1) {
stepsCounted++; //count steps
}
else {
stepsOutsideTime++;
}
settings = 0; //reset settings to save memory
}
function draw() {
var height = 23; //width is deined globally
distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km
//Check if same day
let date = new Date();
if (lastUpdate.getDate() == date.getDate()){ //if same day
}
else { //different day, set all steps to 0
stepsCounted = 0;
stepsTooShort = 0;
stepsTooLong = 0;
stepsOutsideTime = 0;
}
lastUpdate = date;
g.reset();
g.clearRect(this.x, this.y, this.x+width, this.y+height);
//draw numbers
if (active == 1) g.setColor(0x07E0); //green
else g.setColor(0xFFFF); //white
g.setFont("6x8", 2);
if (setting('lineOne') == 'Steps') {
g.drawString(kFormatterSteps(stepsCounted),this.x+1,this.y); //first line, big number, steps
}
if (setting('lineOne') == 'Distance') {
g.drawString(distance.toFixed(2),this.x+1,this.y); //first line, big number, distance
}
g.setFont("6x8", 1);
g.setColor(0xFFFF); //white
if (setting('lineTwo') == 'Steps') {
g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number, steps
}
if (setting('lineTwo') == 'Distance') {
g.drawString(distance.toFixed(3) + "km",this.x+1,this.y+14); //second line, small number, distance
}
//draw step goal bar
stepGoalPercent = (stepsCounted / setting('stepGoal')) * 100;
stepGoalBarLength = width / 100 * stepGoalPercent;
if (stepGoalBarLength > width) stepGoalBarLength = width; //do not draw across width of widget
g.setColor(0x7BEF); //grey
g.fillRect(this.x, this.y+height, this.x+width, this.y+height); // draw background bar
g.setColor(0xFFFF); //white
g.fillRect(this.x, this.y+height, this.x+1, this.y+height-1); //draw start of bar
g.fillRect(this.x+width, this.y+height, this.x+width-1, this.y+height-1); //draw end of bar
g.fillRect(this.x, this.y+height, this.x+stepGoalBarLength, this.y+height); // draw progress bar
settings = 0; //reset settings to save memory
}
//This event is called just before the device shuts down for commands such as reset(), load(), save(), E.reboot() or Bangle.off()
E.on('kill', () => {
let d = { //define array to write to file
lastUpdate : lastUpdate.toISOString(),
stepsToday : stepsCounted,
stepsTooShort : stepsTooShort,
stepsTooLong : stepsTooLong,
stepsOutsideTime : stepsOutsideTime
};
s.write(PEDOMFILE,d); //write array to file
});
//When Step is registered by firmware
Bangle.on('step', (up) => {
steps++; //increase step count
calcSteps();
if (Bangle.isLCDOn()) WIDGETS["activepedom"].draw();
});
// redraw when the LCD turns on
Bangle.on('lcdPower', function(on) {
if (on) WIDGETS["activepedom"].draw();
});
//Read data from file and set variables
let pedomData = s.readJSON(PEDOMFILE,1);
if (pedomData) {
if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate);
stepsCounted = pedomData.stepsToday|0;
stepsTooShort = pedomData.stepsTooShort;
stepsTooLong = pedomData.stepsTooLong;
stepsOutsideTime = pedomData.stepsOutsideTime;
}
pedomdata = 0; //reset pedomdata to save memory
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly
//Add widget
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw};
})();

View File

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

View File

@ -84,15 +84,15 @@ function editAlarm(alarmIndex) {
last : day, rp : repeat
};
}
if (newAlarm) {
menu["> New Alarm"] = function() {
alarms.push(getAlarm());
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
} else {
menu["> Save"] = function() {
alarms[alarmIndex] = getAlarm();
menu["> Save"] = function() {
if (newAlarm) alarms.push(getAlarm());
else alarms[alarmIndex] = getAlarm();
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};
if (!newAlarm) {
menu["> Delete"] = function() {
alarms.splice(alarmIndex,1);
require("Storage").write("alarm.json",JSON.stringify(alarms));
showMainMenu();
};

View File

@ -1 +1,2 @@
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
*
* 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 storage = require("Storage");
const LAST_GPS_FILE = "astrocalc.gps.json";
let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null);
function drawMoon(phase, x, y) {
const moonImgFiles = [
@ -296,22 +306,49 @@ function indexPageMenu(gps) {
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
*/
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.drawImage(img, 100, 50);
g.setFont("6x8", 1);
g.drawString("Astrocalc v0.01", 80, 105);
g.drawString("Locating GPS", 85, 140);
g.drawString("Please wait...", 80, 155);
g.drawString(str1, getCenterStringX(str1), 105);
g.drawString(str2, getCenterStringX(str2), 140);
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();
const DEBUG = false;
if (DEBUG) {
clearWatch();
const gps = {
"lat": 56.45783133333,
"lon": -3.02188583333,
@ -330,7 +367,10 @@ function drawGPSWaitPage() {
Bangle.on('GPS', (gps) => {
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.buzz();
Bangle.setLCDPower(true);

1
apps/astroid/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast

View File

@ -59,6 +59,7 @@ function gameStart() {
function onFrame() {
"ram"
var t = getTime();
var d = (lastFrame===undefined)?0:(t-lastFrame)*20;
lastFrame = t;

15
apps/ballmaze/README.md Normal file
View File

@ -0,0 +1,15 @@
# Ball Maze
Navigate a ball through a maze by tilting your watch.
![Screenshot](size_select.png)
![Screenshot](maze.png)
## Usage
Select a maze size to begin the game.
Tilt your watch to steer the ball towards the target and advance to the next level.
## Creator
Richard de Boer <rigrig+banglejs@tubul.net>

552
apps/ballmaze/app.js Normal file
View File

@ -0,0 +1,552 @@
(() => {
let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {};
// density, elasticity of bounces, "drag coefficient"
const rho = 100, e = 0.3, C = 0.01;
// screen width & height in pixels
const sW = 240, sH = 160;
// gravity constant (lowercase was already taken)
const G = 9.80665;
// wall bit flags
const TOP = 1<<0, LEFT = 1<<1, BOTTOM = 1<<2, RIGHT = 1<<3,
LINKED = 1<<4; // used in maze generation
// The play area is 240x160, sizes are the ball radius, so we can use common
// denominators of 120x80 to get square rooms
// Reverse the order to show the easiest on top of the menu
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(),
// even size 1 actually works, but larger mazes take forever to generate
minSize = 4, defaultSize = 10;
const sizeNames = {
1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large",
10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial",
};
/**
* Draw something to all screen buffers
* @param draw {function} Callback which performs the drawing
*/
function drawAll(draw) {
draw();
g.flip();
draw();
g.flip();
}
/**
* Clear all buffers
*/
function clearAll() {
drawAll(() => g.clear());
}
// use unbuffered graphics for UI stuff
function showMessage(message, title) {
Bangle.setLCDMode();
return E.showMessage(message, title);
}
function showPrompt(prompt, options) {
Bangle.setLCDMode();
return E.showPrompt(prompt, options);
}
function showMenu(menu) {
Bangle.setLCDMode();
return E.showMenu(menu);
}
const sign = (n) => n<0?-1:1; // we don't really care about zero
/**
* Play the game, using a ball with radius size
* @param size {number}
*/
function playMaze(size) {
const r = size;
// ball mass, weight, "drag"
// Yes, larger maze = larger ball = heavier ball
// (atm our physics is so oversimplified that mass cancels out though)
const m = rho*(r*r*r), w = G*m, d = C*w;
// number of columns/rows
const cols = Math.round(sW/(r*2.5)),
rows = Math.round(sH/(r*2.5));
// width & height of one column/row in pixels
const cW = sW/cols, rH = sH/rows;
// list of rooms, every room can have one or more wall bits set
// actual layout: 0 1 2
// 3 4 5
// this means that for room with index "i": (except edge cases!)
// i-1 = room to the left
// i+1 = room to the right
// i-cols = room above
// i+cols = room below
let rooms = new Uint8Array(rows*cols);
// shortest route from start to finish
let route;
let x, y, // current position
px, py, ppx, ppy, // previous positions (for erasing old image)
vx, vy; // velocity
function start() {
// start in top left corner
x = cW/2;
y = rH/2;
vx = vy = 0;
ppx = px = x;
ppy = py = y;
generateMaze(); // this shows unbuffered progress messages
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
Bangle.setLCDMode("doublebuffered");
clearAll();
drawAll(drawMaze);
intervalID = setInterval(tick, 100);
}
// Position conversions
// index: index of room in rooms[]
// rowcol: position measured in roomsizes
// xy: position measured in pixels
/**
* Index from RowCol
* @param row {number}
* @param col {number}
* @returns {number} rooms[] index
*/
function iFromRC(row, col) {
return row*cols+col;
}
/**
* RowCol from index
* @param index {number}
* @returns {(number)[]} [row,column]
*/
function rcFromI(index) {
return [
Math.floor(index/cols),
index%cols,
];
}
/**
* RowCol from Xy
* @param x {number}
* @param y {number}
* @returns {(number)[]} [row,column]
*/
function rcFromXy(x, y) {
return [
Math.floor(y/sH*rows),
Math.floor(x/sW*cols),
];
}
/**
* Link another room up
* @param index {number} Dig from already linked room with this index
* @param dir {number} in this direction
* @return {number} index of room we just linked up
*/
function dig(index, dir) {
rooms[index] &= ~dir;
let neighbour;
switch(dir) {
case LEFT:
neighbour = index-1;
rooms[neighbour] &= ~RIGHT;
break;
case RIGHT:
neighbour = index+1;
rooms[neighbour] &= ~LEFT;
break;
case TOP:
neighbour = index-cols;
rooms[neighbour] &= ~BOTTOM;
break;
case BOTTOM:
neighbour = index+cols;
rooms[neighbour] &= ~TOP;
break;
}
rooms[neighbour] |= LINKED;
return neighbour;
}
/**
* Generate the maze
*/
function generateMaze() {
// Maze generation basically works like this:
// 1. Start with all rooms set to completely walled off and "unlinked"
// 2. Then mark a room as "linked", and add it to the "to do" list
// 3. When the "to do" list is empty, we're done
// 4. pick a random room from the list
// 5. if all adjacent rooms are linked -> remove room from list, goto 3
// 6. pick a random unlinked adjacent room
// 7. remove the walls between the rooms
// 8. mark the adjacent room as linked and add it to the "to do" list
// 9. go to 4
let pdotnum = 0;
const title = "Please wait",
message = "Generating maze\n",
showProgress = (done, total) => {
const dotnum = Math.floor(done/total*10);
if (dotnum>pdotnum) {
const dots = ".".repeat(dotnum)+" ".repeat(10-dotnum);
showMessage(message+dots, title);
pdotnum = dotnum;
}
};
showProgress(0, 100);
// start with all rooms completely walled off
rooms.fill(TOP|LEFT|BOTTOM|RIGHT);
const
// is room at row,col already linked?
linked = (row, col) => !!(rooms[iFromRC(row, col)]&LINKED),
// pick random array element
pickRandom = (arr) => arr[Math.floor(Math.random()*arr.length)];
// starting with top-right room seems to generate more interesting mazes
rooms[cols] |= LINKED;
let todo = [cols], done = 1;
while(todo.length) {
const index = pickRandom(todo);
const rc = rcFromI(index),
row = rc[0], col = rc[1];
let sides = [];
if ((col>0) && !linked(row, col-1)) sides.push(LEFT);
if ((col<cols-1) && !linked(row, col+1)) sides.push(RIGHT);
if ((row>0) && !linked(row-1, col)) sides.push(TOP);
if ((row<rows-1) && !linked(row+1, col)) sides.push(BOTTOM);
if (sides.length<=1) {
// no need to visit this room again
todo.splice(todo.indexOf(index), 1);
}
if (!sides.length) {
// no neighbours need linking
continue;
}
todo.push(dig(index, pickRandom(sides)));
showProgress(done++, rooms.length);
}
}
/**
* We wouldn't want to generate a maze we can't solve ourselves...
*/
function findRoute() {
let dist = new Uint16Array(rooms.length), todo = [0];
dist.fill(-1);
dist[0] = 0;
while(true) {
const i = todo.shift(), d = dist[i], walls = rooms[i],
rc = rcFromI(i),
row = rc[0], col = rc[1];
if (i===rooms.length-1) { break; }
if (col>0 && !(walls&LEFT) && dist[i-1]>d+1) {
dist[i-1] = d+1;
todo.push(i-1);
}
if (row>0 && !(walls&TOP) && dist[i-cols]>d+1) {
dist[i-cols] = d+1;
todo.push(i-cols);
}
if (col<cols-1 && !(walls&RIGHT) && dist[i+1]>d+1) {
dist[i+1] = d+1;
todo.push(i+1);
}
if (row<rows-1 && !(walls&BOTTOM) && dist[i+cols]>d+1) {
dist[i+cols] = d+1;
todo.push(i+cols);
}
}
route = [rooms.length-1];
while(true) {
const i = route[0], d = dist[i], walls = rooms[i],
rc = rcFromI(i),
row = rc[0], col = rc[1];
if (i===0) { break; }
if (col<cols-1 && !(walls&RIGHT) && dist[i+1]<d) {
route.unshift(i+1);
continue;
}
if (row<rows-1 && !(walls&BOTTOM) && dist[i+cols]<d) {
route.unshift(i+cols);
continue;
}
if (row>0 && !(walls&TOP) && dist[i-cols]<d) {
route.unshift(i-cols);
continue;
}
if (col>0 && !(walls&LEFT) && dist[i-1]<d) {
route.unshift(i-1);
continue;
}
// this should never happen!
console.log("No route found!");
break;
}
}
/**
* Draw the maze:
* - room borders
* - maze border
* - exit
*/
function drawMaze() {
const range = {top: 0, left: 0, bottom: rows, right: cols};
const w = sW/cols, h = sH/rows;
g.clear();
g.setColor(0.76, 0.60, 0.42);
for(let row = range.top; row<=range.bottom; row++) {
for(let col = range.left; col<=range.right; col++) {
const walls = rooms[row*cols+col], x = col*w, y = row*h;
if (walls&BOTTOM) g.drawLine(x, y+h, x+w, y+h);
if (walls&RIGHT) g.drawLine(x+w, y, x+w, y+h);
}
}
// outline
g.setColor(0.29, 0.23, 0.17).drawRect(0, 0, sW-1, sH-1);
// target
g.setColor(0, 0.5, 0).fillCircle(sW-cW/2, sH-rH/2, r-1);
if (route) drawRoute();
}
/**
* Redraw a part of the maze (after we erased the ball image)
* @param range Draw rooms in this range {top,left,bottom,right}
*/
function redrawMaze(range) {
const w = sW/cols, h = sH/rows;
g.setColor(0.76, 0.60, 0.42);
for(let row = range.top; row<=range.bottom; row++) {
for(let col = range.left; col<=range.right; col++) {
const walls = rooms[row*cols+col], x = col*w, y = row*h;
if (row===range.top && walls&TOP) g.drawLine(x, y, x+w, y);
if (col===range.left && walls&LEFT) g.drawLine(x, y, x, y+h);
if (walls&BOTTOM) g.drawLine(x, y+h, x+w, y+h);
if (walls&RIGHT) g.drawLine(x+w, y, x+w, y+h);
}
}
g.setColor(0.29, 0.23, 0.17).drawRect(0, 0, sW-1, sH-1);
}
/**
* Draw the ball, with glare offset depending on ball position
*/
function drawBall() {
g.setColor(0.7, 0.7, 0.8).fillCircle(x, y, r-1);
const gx = -x/sW, gy = -y/sH;
g.setColor(0.8, 0.8, 0.9).fillCircle(x+gx*r/5, y+gy*r/5, r/2)
.setColor(0.85, 0.85, 0.95).fillCircle(x+gx*r/4, y+gy*r/4.5, r/2.5)
.setColor(0.9, 0.9, 1).fillCircle(x+gx*r/3, y+gy*r/3, r/3.5)
.setColor(1, 1, 1).fillCircle(x+gx*r/3, y+gy*r/3, r/6);
}
/**
* Update the screen:
* - erase previous ball image
* - redraw maze around the erased area
* - draw the ball
*/
function drawUpdate() {
g.clearRect(ppx-r, ppy-r, ppx+r, ppy+r);
const rc = rcFromXy(ppx, ppy),
row = rc[0], col = rc[1];
redrawMaze({top: row-1, left: col-1, bottom: row+1, right: col+1});
drawBall();
g.flip();
}
function drawRoute() {
let i = route[0], rc = rcFromI(i),
row = rc[0], col = rc[1],
x = (col+0.5)*cW, y = (row+0.5)*rH;
g.setColor(1, 0, 0).moveTo(x, y);
route.forEach(i => {
const rc = rcFromI(i),
row = rc[0], col = rc[1],
x = (col+0.5)*cW, y = (row+0.5)*rH;
g.lineTo(x, y);
});
}
/**
* Move the ball
*/
function move() {
const a = Bangle.getAccel();
const fx = (-a.x*w)-(sign(vx)*d*a.z), fy = (-a.y*w)-(sign(vy)*d*a.z);
vx += fx/m;
vy += fy/m;
const s = Math.ceil(Math.max(Math.abs(vx), Math.abs(vy)));
for(let n = s; n>0; n--) {
x += vx/s;
y += vy/s;
bounce();
}
if (x>sW-cW && y>sH-rH) win();
}
/**
* Check whether we hit any walls, and if so: Bounce.
*
* Bounce = reverse velocity in bounce direction, multiply with elasticity
* Also apply drag in perpendicular direction ("friction with the wall")
*/
function bounce() {
const row = Math.floor(y/sH*rows), col = Math.floor(x/sW*cols),
i = row*cols+col, walls = rooms[i];
const left = col*cW,
right = (col+1)*cW,
top = row*rH,
bottom = (row+1)*rH;
let bounced = false;
if (vx<0) {
if ((walls&LEFT) && x<=left+r) {
x += (1+e)*(left+r-x);
const fy = sign(vy)*d*Math.abs(vx);
vy -= fy/m;
vx = -vx*e;
bounced = true;
}
} else {
if ((walls&RIGHT) && x>=right-r) {
x -= (1+e)*(x+r-right);
const fy = sign(vy)*d*Math.abs(vx);
vy -= fy/m;
vx = -vx*e;
bounced = true;
}
}
if (vy<0) {
if ((walls&TOP) && y<=top+r) {
y += (1+e)*(top+r-y);
const fx = sign(vx)*d*Math.abs(vy);
vx -= fx/m;
vy = -vy*e;
bounced = true;
}
} else {
if ((walls&BOTTOM) && y>=bottom-r) {
y -= (1+e)*(y+r-bottom);
const fx = sign(vx)*d*Math.abs(vy);
vx -= fx/m;
vy = -vy*e;
bounced = true;
}
}
if (bounced) return;
let cx, cy;
if ((rooms[i-1]&TOP) || rooms[i-cols]&LEFT) {
if ((x-left)*(x-left)+(y-top)*(y-top)<=r*r) {
cx = left;
cy = top;
}
}
else if ((rooms[i-1]&BOTTOM) || rooms[i+cols]&LEFT) {
if ((x-left)*(x-left)+(bottom-y)*(bottom-y)<=r*r) {
cx = left;
cy = bottom;
}
}
else if ((rooms[i+1]&TOP) || rooms[i-cols]&RIGHT) {
if ((right-x)*(right-x)+(y-top)*(y-top)<=r*r) {
cx = right;
cy = top;
}
}
else if ((rooms[i+1]&BOTTOM) || rooms[i+cols]&RIGHT) {
if ((right-x)*(right-x)+(bottom-y)*(bottom-y)<=r*r) {
cx = right;
cy = bottom;
}
}
if (!cx) return;
let nx = x-cx, ny = y-cy;
const l = Math.sqrt(nx*nx+ny*ny);
nx /= l;
ny /= l;
const p = vx*nx+vy*ny;
vx -= 2*p*nx*e;
vy -= 2*p*ny*e;
}
/**
* You reached the bottom-right corner, you win!
*/
function win() {
clearInterval(intervalID);
Bangle.buzz().then(askAgain);
}
/**
* You solved the maze, try the next one?
*/
function askAgain() {
const nextLevel = (size>minSize)?"next level":"again";
const nextSize = (size>minSize)?sizes[sizes.indexOf(size)+1]:size;
showPrompt(`Well done!\n\nPlay ${nextLevel}?`,
{"title": "Congratulations!"})
.then(function(again) {
if (again) {
playMaze(nextSize);
} else {
startGame();
}
});
}
function tick() {
ppx = px;
ppy = py;
px = x;
py = y;
move();
drawUpdate();
}
start();
}
/**
* Ask player what size maze they would like to play
*/
function startGame() {
let menu = {
"": {
title: "Select Maze Size",
selected: sizes.indexOf(settings.size || defaultSize),
},
};
sizes.filter(s => s>=minSize).forEach(size => {
let name = sizeNames[size];
if (size<minSize) name = "! "+size;
let cols = Math.round(sW/(size*2.5)),
rows = Math.round(sH/(size*2.5));
if (rows<10) rows = " "+rows;
if (cols<10) cols = " "+cols;
name += " ".repeat(14-name.length);
name += `${cols}x${rows}`;
menu[name] = () => {
// remember chosen size
settings.size = size;
require("Storage").write("ballmaze.json", settings);
playMaze(size);
};
});
menu["< Exit"] = () => load();
showMenu(menu);
}
startGame();
})();

1
apps/ballmaze/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA"))

BIN
apps/ballmaze/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
apps/ballmaze/maze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

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

View File

@ -2,3 +2,4 @@
0.02: Apply locale, 12-hour setting
0.03: Fix dates drawing over each other at midnight
0.04: Small bugfix
0.05: Clock does not start if app Languages is not installed

View File

@ -12,7 +12,12 @@
date.setMonth(1, 3) // februari: months are zero-indexed
const localized = locale.date(date, true)
locale.dayFirst = /3.*2/.test(localized)
locale.hasMeridian = (locale.meridian(date) !== '')
locale.hasMeridian = false
if(typeof locale.meridian === 'function') { // function does not exists if languages app is not installed
locale.hasMeridian = (locale.meridian(date) !== '')
}
}
const screen = {
width: g.getWidth(),

10
apps/batchart/ChangeLog Normal file
View File

@ -0,0 +1,10 @@
0.01: New app and widget
0.02: Widget stores data to file (1 dataset/10min)
0.03: Rotate log files once a week.
0.04: chart in the app is now active.
0.05: Display temperature and LCD state in chart
0.06: Fixes widget events and charting of component states
0.07: Improve logging and charting of component states and add widget icon
0.08: Fix for Home button in the app and README added.
0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state
0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381)

67
apps/batchart/README.md Normal file
View File

@ -0,0 +1,67 @@
# Summary
Battery Chart contains a widget that records the battery usage as well as information that might influence this usage.
The app that comes with provides a graph that accumulates this information in a single screen.
## How the widget works
The widget records data in a fixed interval of ten minutes.
When this timespan has passed, it saves the following information to a file called `bclogx` where `x` is
the current day retrieved by `new Date().getDay()`:
- Battery percentage
- Temperature (of the die)
- LCD state
- Compass state
- HRM state
- GPS state
After seven days the logging rolls over and the previous data is overwritten.
To properly handle the roll-over, the day of the previous logging operation is stored in `bcprvday`.
The value is changed with the first recording operation of the new day.
## How the App works
### Events
The app charts the last 144 (6/h * 24h) datapoints that have been recorded.
If for the current day the 144 events have not been reached the list is padded with
events from the previous `bclog` file(s).
### Graph
The graph then contains the battery percentage (left y-axis) and the temperature (right y-axis).
In case the recorded temperature is outside the limits of the graph, the value is set to a minimum of 19 or a maximum of 41 and thus should be clearly visible outside of the graph's boundaries for the y-axis.
The states of the various SoC devices are depicted below the graph. If at the time of recording the device was enabled a marker in the respective color is set, if not the pixels for this point in time stay black.
If a device was not enabled during the 144 selected events, the name is not displayed.
## File schema
You can download the `bclog` files for your own analysis. They are `CSV` files without header rows and contain
```
timestamp,batteryPercentage,temperatureInDegreeC,deviceStates
```
with the `deviceStates` resembling a flag set consisting of
```
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
bluetooth: 4,
gps: 8,
hrm: 16
};
```

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AS64AIF/4pZABYuuGDIv/F/4v/F9+Gw0rAQIASF7YxTF7cxwAvtrdVF9qQTF/4vMYCQvcYCQvcSCQvdqpgQF7oEBYJ4veAoNbF9uGmMrrgvsw2AGILFKF8IACrYxJF8gxDSowvmBwWAF9oPGF9NbmIvtCAovqMAgvqCIgvrrdVF9oSDF9iPuF7crACxf/F++wFqmG2AvXGCouZAH4A/AGY"))

246
apps/batchart/app.js Normal file
View File

@ -0,0 +1,246 @@
const GraphXZero = 40;
const GraphYZero = 180;
const GraphY100 = 80;
const GraphMarkerOffset = 5;
const MaxValueCount = 144;
const GraphXMax = GraphXZero + MaxValueCount;
const GraphLcdY = GraphYZero + 10;
const GraphCompassY = GraphYZero + 16;
const GraphBluetoothY = GraphYZero + 22;
const GraphGpsY = GraphYZero + 28;
const GraphHrmY = GraphYZero + 34;
const Storage = require("Storage");
function renderCoordinateSystem() {
g.setFont("6x8", 1);
// Left Y axis (Battery)
g.setColor(1, 1, 0);
g.drawLine(GraphXZero, GraphYZero + GraphMarkerOffset, GraphXZero, GraphY100);
g.drawString("%", 39, GraphY100 - 10);
g.setFontAlign(1, -1, 0);
g.drawString("100", 30, GraphY100 - GraphMarkerOffset);
g.drawLine(GraphXZero - GraphMarkerOffset, GraphY100, GraphXZero, GraphY100);
g.drawString("50", 30, GraphYZero - 50 - GraphMarkerOffset);
g.drawLine(GraphXZero - GraphMarkerOffset, 130, GraphXZero, 130);
g.drawString("0", 30, GraphYZero - GraphMarkerOffset);
g.setColor(1,1,1);
g.setFontAlign(1, -1, 0);
g.drawLine(GraphXZero - GraphMarkerOffset, GraphYZero, GraphXMax + GraphMarkerOffset, GraphYZero);
// Right Y axis (Temperature)
g.setColor(0.4, 0.4, 1);
g.drawLine(GraphXMax, GraphYZero + GraphMarkerOffset, GraphXMax, GraphY100);
g.drawString("°C", GraphXMax + GraphMarkerOffset, GraphY100 - 10);
g.setFontAlign(-1, -1, 0);
g.drawString("20", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - GraphMarkerOffset);
g.drawLine(GraphXMax + GraphMarkerOffset, 130, GraphXMax, 130);
g.drawString("30", GraphXMax + 2 * GraphMarkerOffset, GraphYZero - 50 - GraphMarkerOffset);
g.drawLine(GraphXMax + GraphMarkerOffset, 80, GraphXMax, 80);
g.drawString("40", GraphXMax + 2 * GraphMarkerOffset, GraphY100 - GraphMarkerOffset);
g.setColor(1,1,1);
}
function decrementDay(dayToDecrement) {
return dayToDecrement === 0 ? 6 : dayToDecrement-1;
}
function loadData() {
const startingDay = new Date().getDay();
// Load data for the current day
let logFileName = "bclog" + startingDay;
let dataLines = loadLinesFromFile(MaxValueCount, logFileName);
// Top up to MaxValueCount from previous days as required
let previousDay = decrementDay(startingDay);
while (dataLines.length < MaxValueCount && previousDay !== startingDay) {
let topUpLogFileName = "bclog" + previousDay;
let remainingLines = MaxValueCount - dataLines.length;
let topUpLines = loadLinesFromFile(remainingLines, topUpLogFileName);
if(topUpLines) {
dataLines = topUpLines.concat(dataLines);
}
previousDay = decrementDay(previousDay);
}
return dataLines;
}
function loadLinesFromFile(requestedLineCount, fileName) {
let allLines = [];
let returnLines = [];
var readFile = Storage.open(fileName, "r");
while ((nextLine = readFile.readLine())) {
if(nextLine) {
allLines.push(nextLine);
}
}
readFile = null;
if (allLines.length <= 0) return;
let linesToReadCount = Math.min(requestedLineCount, allLines.length);
let startingLineIndex = Math.max(0, allLines.length - requestedLineCount - 1);
for (let i = startingLineIndex; i < linesToReadCount + startingLineIndex; i++) {
if(allLines[i]) {
returnLines.push(allLines[i]);
}
}
allLines = null;
return returnLines;
}
function renderData(dataArray) {
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
bluetooth: 4,
gps: 8,
hrm: 16
};
//const timestampIndex = 0;
const batteryIndex = 1;
const temperatureIndex = 2;
const switchabelsIndex = 3;
const minTemperature = 20;
const maxTemparature = 40;
const belowMinIndicatorValue = minTemperature - 1;
const aboveMaxIndicatorValue = maxTemparature + 1;
var allConsumers = switchableConsumers.none | switchableConsumers.lcd | switchableConsumers.compass | switchableConsumers.bluetooth | switchableConsumers.gps | switchableConsumers.hrm;
for (let i = 0; i < dataArray.length; i++) {
const element = dataArray[i];
var dataInfo = element.split(",");
// Battery percentage
g.setColor(1, 1, 0);
g.setPixel(GraphXZero + i, GraphYZero - parseInt(dataInfo[batteryIndex]));
// Temperature
g.setColor(0.4, 0.4, 1);
let datapointTemp = parseFloat(dataInfo[temperatureIndex]);
if (datapointTemp < minTemperature) {
datapointTemp = belowMinIndicatorValue;
}
if (datapointTemp > maxTemparature) {
datapointTemp = aboveMaxIndicatorValue;
}
// Scale down the range of 20 - 40°C to a 100px y-axis, where 1px = .25°
let scaledTemp = Math.floor(((datapointTemp * 100) - 2000) / 20) + ((((datapointTemp * 100) - 2000) % 100) / 25);
g.setPixel(GraphXZero + i, GraphYZero - scaledTemp);
// LCD state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.lcd) {
g.setColor(1, 1, 1);
g.setFontAlign(1, -1, 0);
g.drawString("LCD", GraphXZero - GraphMarkerOffset, GraphLcdY - 2, true);
g.drawLine(GraphXZero + i, GraphLcdY, GraphXZero + i, GraphLcdY + 1);
}
// Compass state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.compass) {
g.setColor(0, 1, 0);
g.setFontAlign(-1, -1, 0);
g.drawString("Compass", GraphXMax + GraphMarkerOffset, GraphCompassY - 2, true);
g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1);
}
// Bluetooth state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.bluetooth) {
g.setColor(0, 0, 1);
g.setFontAlign(1, -1, 0);
g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true);
g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1);
}
// Gps state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) {
g.setColor(0.8, 0.5, 0.24);
g.setFontAlign(-1, -1, 0);
g.drawString("GPS", GraphXMax + GraphMarkerOffset, GraphGpsY - 2, true);
g.drawLine(GraphXZero + i, GraphGpsY, GraphXZero + i, GraphGpsY + 1);
}
// Hrm state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.hrm) {
g.setColor(1, 0, 0);
g.setFontAlign(1, -1, 0);
g.drawString("HRM", GraphXZero - GraphMarkerOffset, GraphHrmY - 2, true);
g.drawLine(GraphXZero + i, GraphHrmY, GraphXZero + i, GraphHrmY + 1);
}
}
dataArray = null;
}
function renderHomeIcon() {
//Home for Btn2
g.setColor(1, 1, 1);
g.drawLine(220, 118, 227, 110);
g.drawLine(227, 110, 234, 118);
g.drawPoly([222,117,222,125,232,125,232,117], false);
g.drawRect(226,120,229,125);
}
function renderBatteryChart() {
renderCoordinateSystem();
let data = loadData();
renderData(data);
data = null;
}
// Show launcher when middle button pressed
function switchOffApp(){
Bangle.showLauncher();
}
// special function to handle display switch on
Bangle.on('lcdPower', (on) => {
if (on) {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
renderBatteryChart();
}
});
setWatch(switchOffApp, BTN2, {edge:"falling", debounce:50, repeat:true});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
renderHomeIcon();
renderBatteryChart();

BIN
apps/batchart/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

124
apps/batchart/widget.js Normal file
View File

@ -0,0 +1,124 @@
(() => {
let recordingInterval = null;
const Storage = require("Storage");
const switchableConsumers = {
none: 0,
lcd: 1,
compass: 2,
bluetooth: 4,
gps: 8,
hrm: 16
};
var batChartFile; // file for battery percentage recording
const recordingInterval10Min = 60 * 10 * 1000;
const recordingInterval1Min = 60 * 1000; //For testing
const recordingInterval10S = 10 * 1000; //For testing
var compassEventReceived = false;
var gpsEventReceived = false;
var hrmEventReceived = false;
function draw() {
// void
}
function batteryChartOnMag() {
compassEventReceived = true;
// Stop handling events when no longer necessarry
Bangle.removeListener("mag", batteryChartOnMag);
}
function batterChartOnGps() {
gpsEventReceived = true;
Bangle.removeListener("GPS", batterChartOnGps);
}
function batteryChartOnHrm() {
hrmEventReceived = true;
Bangle.removeListener("HRM", batteryChartOnHrm);
}
function getEnabledConsumersValue() {
// Wait for an event from each of the devices to see if they are switched on
var enabledConsumers = switchableConsumers.none;
Bangle.on('mag', batteryChartOnMag);
Bangle.on('GPS', batterChartOnGps);
Bangle.on('HRM', batteryChartOnHrm);
// Wait two seconds, that should be enough for each of the events to get raised once
setTimeout(() => {
Bangle.removeListener('mag', batteryChartOnMag);
Bangle.removeListener('GPS', batterChartOnGps);
Bangle.removeListener('HRM', batteryChartOnHrm);
}, 2000);
if (Bangle.isLCDOn())
enabledConsumers = enabledConsumers | switchableConsumers.lcd;
if (compassEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.compass;
if (gpsEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.gps;
if (hrmEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.hrm;
// Very coarse first approach to check if the BLE device is on.
if (NRF.getSecurityStatus().connected)
enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
// Reset the event registration vars
compassEventReceived = false;
gpsEventReceived = false;
hrmEventReceived = false;
return enabledConsumers.toString();
}
function logBatteryData() {
const previousWriteLogName = "bcprvday";
const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine());
const currentWriteDay = new Date().getDay();
const logFileName = "bclog" + currentWriteDay;
// Change log target on day change
if (!isNaN(previousWriteDay) && previousWriteDay != currentWriteDay) {
//Remove a log file containing data from a week ago
Storage.open(logFileName, "r").erase();
Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay));
}
var bcLogFileA = Storage.open(logFileName, "a");
if (bcLogFileA) {
let logTime = getTime().toFixed(0);
let logPercent = E.getBattery();
let logTemperature = E.getTemperature();
let logConsumers = getEnabledConsumersValue();
let logString = [logTime, logPercent, logTemperature, logConsumers].join(",");
bcLogFileA.write(logString + "\n");
}
}
function reload() {
console.log("Reloading BatteryChart widget");
WIDGETS["batchart"].width = 0;
if (recordingInterval) {
clearInterval(recordingInterval);
recordingInterval = null;
}
recordingInterval = setInterval(logBatteryData, recordingInterval10Min);
}
// add the widget
WIDGETS["batchart"] = {
area: "tl", width: 0, draw: draw, reload: reload
};
reload();
})();

2
apps/beebclock/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Initial commit. Not very efficient, and widgets not working for some reason.
0.02: Fixes; widget support

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkHA4dQAgcFgPyl8QDxgNE0EAggXGAgcFDQ0TBgQXBkcgBQURCw8GBYUj+AEBI430AgI7BBAVgCIU/if0DoMC+UfiwLBgUyEQRyGgEzmPzCIQvCBwMPj4rCAwJECAAUD+MvkEQgMhkRABgEvkaKIJAXzj49BBYMBBIOm+IIBgMVPQxiBn8xkAIDAYMBj6TBSIYyFhUPBoJRCF4RlBAoJRBBggSBIIgAI0qhCFgUB/4WFTIYDDFwJMCCAUSifzDoYsGBQJIBfoM0kIEBn81168CfAwACKwMS+UT+ovC/8gmRRCGQqQBRgUjocyB4YYBI4QrEDwRdCfAQ4EsD5DAA5dCDYbMDCoTPCBAsQaYprHRosR0ICBB4ZtDEYJZHM4X/kMKFAwSGAocBn8hkX/NBMFEAJXDAQMD+IcBkcwBIZ1EHgP/BgIzD17QBDYPwI4kCn8/mcjkUyCAQlCVocB+IqDC4IVBmYWBkVAVAkvaIboDqAGBCwMyIwM/I4IVBoYHBI4qzDI4egLYURiaiCO4UAl4bCMIJLEEAUj//zlVQgynBmNC/5LBcQsA0BXBCoNCeQkDKQX1X5Ef+clTQIkCT4URiJYBXwYlEirHGOAkAJYIvHEAUTNoadBegn/EYUCB4IjDiRtCCoWgEwj8BCQMCCAQkBAoMhkZJDC4kFh/yNAMyifzE4U/kMf+RRGM4beCp/xibLBqERj6EDboQjCT4beDmMhQwRNEQIoACiISCIYILCgKNCXgQXFGYoTBC4a/DgcmBoRLCEAMDCQQPBbwxVBmLDDGwUCj/wHY4ADn8TBwbYD+3xCY8AhQlB+M/JwS3BGIXzj5RENAS1Cj86YQUB+U/KIdvmB6FIw8Qg3yl5KCgcyMAgZFiNPOwYXDAoURL45LCiSLD+YXBAoTXDAAbTIL4oJCCIRdEDA1gI4ooFgAA=="))

397
apps/beebclock/beebclock.js Normal file
View File

@ -0,0 +1,397 @@
/* jshint esversion: 6 */
// Beebclock
// © 2020, Tom Gidden
// https://github.com/tomgidden
const storage = require("Storage");
const filename = 'beebjson';
require('FontTeletext10x18Ascii').add(Graphics);
// Double height text
Graphics.prototype.drawStringDH = function (txt, px, py, align, gw) {
let g2 = Graphics.createArrayBuffer(gw,18,1,{msb:true});
g2.setFontTeletext10x18Ascii();
let w = g2.stringWidth(txt);
let c = (w+3)>>2;
g2.drawString(txt);
let img = {width:w,height:1,transparent:0,buffer:new ArrayBuffer(c)};
let a = new Uint8Array(img.buffer);
let x;
switch (align) {
case 'C': x = px + (gw - w)/2; break;
case 'R': x = gw - w + px; break;
default: x = px;
}
for (var y=0;y<18;y++) {
a.set(new Uint8Array(g2.buffer,gw*y/8,c));
this.drawImage(img,x,py+y*2);
this.drawImage(img,x,py+1+y*2);
}
};
// Fill rectangle rotated around the centre
Graphics.prototype.fillRotRect = function (sina, cosa, cx, cy, x0, x1, y0, y1) {
let fn = Math.ceil;
return this.fillPoly([
fn(cx - x0*cosa + y0*sina), fn(cy - x0*sina - y0*cosa),
fn(cx - x1*cosa + y0*sina), fn(cy - x1*sina - y0*cosa),
fn(cx - x1*cosa + y1*sina), fn(cy - x1*sina - y1*cosa),
fn(cx - x0*cosa + y1*sina), fn(cy - x0*sina - y1*cosa)
]);
};
// Draw a line from r1,a to r2,a relative to cx+cy
Graphics.prototype.drawRotLine = function (sina, cosa, cx, cy, r1, r2) {
return this.drawLine(
cx + r1*sina, cy - r1*cosa,
cx + r2*sina, cy - r2*cosa
);
};
(function(g) {
// Display modes
//
// 0: full-screen
// 1: with widgets
// 2: centred on Bangle (v.1), no widgets or time/date
// 3: centred with time above
// 4: centred with date above
// 5: centred with time and date above
let mode;
// R1, R2: Outer and inner radii of hour marks
// RC1, RC2: Outer and inner radii of hub
// CX, CY: Centre location, relative to buffer (not screen, necessarily)
// HW2, MW2: Half-width of hour and minute hand
// HR, MR: Length of hour and minute hand, relative to CX,CY
// M: Half-width of gap in hour marks
// HSCALE: Half-width of hour mark as function(0<h<13)
let R1, R2, RC1, RC2, CX, CY, HW2, MW2, HR, MR, M, HSCALE;
// Screen size
const GW = g.getWidth();
const GH = g.getHeight();
// Top margin: the gap taken from the top of the buffer, except when
// in mode 0 (full screen)
let TM;
// Buffer image. undefined means it needs regenerating
let faceImg;
// with_seconds flag determines whether the face is updated every
// second or every minute, and to draw the hand or not.
let with_seconds = true;
// Display flags, determined from `mode` by setMode()
let with_widgets = false;
let with_digital_time = true;
let with_digital_date = true;
// Create offscreen buffer for the once-per-minute face draw
const G1 = Graphics.createArrayBuffer(g.getWidth(), g.getHeight(), 1, {msb:true});
// Precalculate sin/cos for the hour marks. Might be premature
// optimisation, but might as well.
let ss = [], cs = [];
for (let h=1; h<=12; h++) {
const a = Math.PI * h / 6;
ss[h] = Math.sin(a);
cs[h] = Math.cos(a);
}
// Draw the face with hour and minute hand. Ideally, we'd separate
// the face from the hands and double-buffer, but memory is limited,
// so we buffer once and minute, and draw the second hand dynamically
// (with a bit of flicker)
const drawFace = (G) => {
const fw = R1 * 2;
const fh = R1 * 2;
const fw2 = R1;
const fh2 = R1;
let hs = [];
// Wipe the image and start with white
G.clear();
G.setColor(1,1,1);
// Draw the hour marks.
for (let h=1; h<=12; h++) {
hs[h] = HSCALE(h);
G.fillRotRect(ss[h], cs[h], CX, CY, -hs[h], hs[h], R2, R1);
}
// Draw the hub
G.fillCircle(CX, CY, RC1);
// Black
G.setColor(0,0,0);
// Clear the centre of the hub
G.fillCircle(CX, CY, RC2);
// Draw the gap in the hour marks
for (let h=1; h<=12; h++) {
G.fillRotRect(ss[h], cs[h], CX, CY, -M, M, R2-1, R1+1);
}
// Back to white for future draw operations
G.setColor(1,1,1);
// While the buffer remains full-screen, we may trim out the
// bottom of the image so we can shift the whole thing down for
// widgets.
const img = {width:GW,height:GH-TM,buffer:G.buffer};
return img;
};
let hours, minutes, seconds, date;
// Schedule event for calling at the start of the next second
const inOneSecond = (cb) => {
let now = new Date();
clearTimeout();
setTimeout(cb, 1000 - now.getMilliseconds());
};
// Schedule event for calling at the start of the next minute
const inOneMinute = (cb) => {
let now = new Date();
clearTimeout();
setTimeout(cb, 60000 - (now.getSeconds() * 1000 + now.getMilliseconds()));
};
// Draw a fat hour/minute hand
const drawHand = (G, a, w2, r1, r2) =>
G.fillRotRect(Math.sin(a), Math.cos(a), CX, CY, -w2, w2, r1, r2);
// Redraw function
const drawAll = (force) => {
let now = new Date();
if (!faceImg) force = true;
let face_changed = force;
let date_changed = false;
tmp = hours;
hours = now.getHours();
if (tmp !== hours)
face_changed = true;
tmp = minutes;
minutes = now.getMinutes();
if (tmp !== minutes)
face_changed = true;
// If the face has been updated and/or needs a redraw,
// face_changed is true.
let time_changed = face_changed;
// If the screen needs an update, regardless of whether the face
// needs a redraw, time_changed is true.
if (with_seconds) {
// If we're going by second, we always need an update.
seconds = now.getSeconds();
time_changed = true;
}
if (with_digital_date) {
// See if the date has changed. If it has, then we need a
// full-blown redraw of the screen and the face, plus text.
tmp = date;
date = now.getDate();
if (tmp !== date) {
date_changed = true;
face_changed = true; // Should have changed anyway with hour/minute rollover
}
}
if (face_changed) {
// Redraw the face and hands onto the buffer G1.
faceImg = drawFace(G1);
drawHand(G1, Math.PI*hours/6, HW2, RC1, HR);
drawHand(G1, Math.PI*minutes/30, MW2, RC1, MR);
}
// Has the time updated? If so, we'll need to draw something.
if (time_changed) {
// Are we adding text?
if (with_digital_date || with_digital_time) {
// Construct the date/time text to add above the face
let d = now.toString();
let da = d.toString().split(" ");
let txt;
if (with_digital_time) {
txt = da[4].substr(0, 5);
if (with_digital_date)
G1.drawStringDH(txt+',', 24, 0, 'L', GW);
else
G1.drawStringDH(txt, 0, 0, 'C', GW);
}
if (with_digital_date) {
let txt = [da[0], da[1], da[2]].join(" ");
if (with_digital_time)
G1.drawStringDH(txt, -24, 0, 'R', GW);
else
G1.drawStringDH(txt, 0, 0, 'C', GW);
}
}
// If the time has updated, we need to _at least_ draw the
// image to the screen.
g.setColor(1,1,1);
g.drawImage({width:GW,
height:GH-TM,
buffer:G1.buffer}, 0, TM);
// and possibly add the second hand
if (with_seconds) {
let a = 2.0 * Math.PI * seconds / 60.0;
g.drawRotLine(Math.sin(a), Math.cos(a), CX, CY+TM, RC1, R1);
}
// Clock chime on the hour.
if (hours >= 0 && minutes === 0)
try {
Bangle.buzz();
} catch (e) { }
// And draw widgets if we're in that mode
if (with_widgets)
Bangle.drawWidgets();
}
// Schedule to repeat this. A `setTimeout(1000)` isn't good
// enough, as all the above might've taken some milliseconds and
// we don't want to drift.
if (with_seconds)
inOneSecond(drawAll);
else
inOneMinute(drawAll);
};
const setButtons = () => {
const opts = { repeat: true, edge:'rising', debounce:30};
// BTN1: enable/disable second hand
setWatch(changeSeconds, BTN1, opts);
// BTN2: return to launcher
setWatch(Bangle.showLauncher, BTN2, { repeat:false, edge:'falling' });
// BTN3: change display mode
setWatch(function () { ++mode; setMode(); drawAll(true); }, BTN3, opts);
};
// Load display parameters based on `mode`
const setMode = () => {
// Normalize mode to 0 <= mode <= 5
mode = (6+mode) % 6;
// [R1, R2, RC1, RC2, HW2, MW3, HR, MR, M, HSCALE] =
const scales = [
[120, 84, 17, 12.4, 4.6, 2.2, 8, 2, 1, h => (3.0 + Math.ceil(h/1.5)) ],
[102, 70, 14.6, 10.7, 3.88, 1.8, 8, 2, 1, h => (2.4 + Math.ceil(h/1.6)) ],
];
if (mode < 3) {
// Face without time/date text. Might have widgets though.
with_digital_time = with_digital_date = false;
with_widgets = (mode == 1);
}
else {
// Face with time/date text, but no widgets
with_digital_time = (mode-2)&1;
with_digital_date = (mode-2)&2;
with_widgets = false;
}
// Destructure the array to the global display parameters
let arr = scales[mode > 0 ? 1 : 0];
R1 = arr[0];
R2 = arr[1];
RC1 = arr[2];
RC2 = arr[3];
HW2 = arr[4];
MW2 = arr[5];
HR = R2 - arr[6];
MR = R1 - arr[7];
M = arr[8];
HSCALE = arr[9];
TM = with_widgets ? 36 : 0;
CX = GW/2;
CY = R1;
// If we're in the small-face + text regime, we're going to buffer
// the full screen but draw the clock face further down to give
// space for the text.
//
// Compare with modes 0 (full-screen) and 1 (with_widgets==true)
// where the face is drawn at the top of the buffer, but drawn
// lower down the screen (so CY doesn't move)
if (mode > 1) {
CY += 36;
}
// We only don't bother redrawing the face from modes 2 to 5, as
// they're the same.
if (!faceImg || mode<3) {
faceImg = undefined;
}
// Store the settings for next time
try {
storage.writeJSON(filename, [mode,with_seconds]);
} catch (e) {
console.log(e);
}
// Clear the screen: we need to make sure all parts are cleaned off.
g.clear();
};
const changeSeconds = () => {
with_seconds = !with_seconds;
drawAll(true);
};
Bangle.loadWidgets();
// Restore mode
try {
conf = storage.readJSON(filename);
mode = conf[0];
with_seconds = conf[1];
} catch (e) {
console.log(e);
mode = 1;
}
setButtons();
setMode();
drawAll();
Bangle.on('lcdPower', (on) => {
if (on) {
Bangle.loadWidgets();
Bangle.drawWidgets();
drawAll();
} else {
clearTimeout();
}
});
})(g);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -196,12 +196,10 @@ Bangle.on('mag', function(m) {
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
g.clear();`;
var icon = `require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))`;
sendCustomizedApp({
storage:[
{name:"beer.app.js", content:app},
{name:"beer.img", content:icon, evaluate:true},
{name:"beer.app.js", content:app}
]
});
});

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

2
apps/bledetect/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Fixed issue with wrong device informations

14
apps/bledetect/README.md Normal file
View File

@ -0,0 +1,14 @@
# BLE Detector
BLE Detector it's an app born for testing purpose that aim to show as informations as possible about near BLE devices.
## Features
BLE Detector shows:
- Device name (if available)
- Received Signal Strength Indication (RSSI)
- Manufacturer
- MAC Address
More informations will coming with future versions.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgJGhGAEKuIxAXXGCoXBGCoXCDCgXDJKYXDGCYUBhAwUFgQwPEogTCGBwNFFYYYNHwoEGJJQlFCIgKCdR4XHJBQNEI6IOFO6IPEDQYGDahoYEa6BJFxBFPJJIuQGAouRGAoWSGAgXTSIoAEgUgL6cCkQACDJCOFGAYWDAAJFLX4gWFGA4sFC40gJQYuHwBEDAQISCMYowEFgoJDCAwYBAwZYEC45AEgIHERAgXMA4i4FC6bPDC4hXFC5B7FC57CHI54XIawgXRVwS/JC5SuDC4wGGC45HBFAQRCAooXIVwYRBAAoXLLIwAFC5IuDGCIuFDAyQLABphKABgwaC6owB"))

View File

@ -0,0 +1,59 @@
let menu = {
"": { "title": "BLE Detector" },
"RE-SCAN": () => scan()
};
function showMainMenu() {
menu["< Back"] = () => load();
return E.showMenu(menu);
}
function showDeviceInfo(device){
const deviceMenu = {
"": { "title": "Device Info" },
"name": {
value: device.name
},
"rssi": {
value: device.rssi
},
"manufacturer": {
value: device.manufacturer
}
};
deviceMenu[device.id] = () => {};
deviceMenu["< Back"] = () => showMainMenu();
return E.showMenu(deviceMenu);
}
function scan() {
menu = {
"": { "title": "BLE Detector" },
"RE-SCAN": () => scan()
};
waitMessage();
NRF.findDevices(devices => {
devices.forEach(device =>{
let deviceName = device.id.substring(0,17);
if (device.name) {
deviceName = device.name;
}
menu[deviceName] = () => showDeviceInfo(device);
});
showMainMenu(menu);
}, { active: true });
}
function waitMessage() {
E.showMenu();
E.showMessage("scanning");
}
scan();
waitMessage();

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -13,3 +13,6 @@
0.13: Now automatically load *.boot.js at startup
Move alarm code into alarm.boot.js
0.14: Move welcome loaders to *.boot.js
0.15: Added BLE HID option for Joystick and bare Keyboard
0.16: Detect out of memory errors and draw them onto the bottom of the screen in red
0.17: Don't modify beep/buzz behaviour if firmware does it automatically

View File

@ -4,7 +4,9 @@ E.setFlags({pretokenise:1});
var s = require('Storage').readJSON('setting.json',1)||{};
if (s.ble!==false) {
if (s.HID) { // Human interface device
Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));
else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));
else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
NRF.setServices({}, {uart:true, hid:Bangle.HID});
}
}
@ -19,24 +21,31 @@ if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
// Don't disconnect if something is already connected to us
if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep();
// Set time, vibrate, beep, etc
if (!s.vibrate) Bangle.buzz=Promise.resolve;
if (s.beep===false) Bangle.beep=Promise.resolve;
else if (s.beep=="vib") Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};
if (!Bangle.F_BEEPSET) {
if (!s.vibrate) Bangle.buzz=Promise.resolve;
if (s.beep===false) Bangle.beep=Promise.resolve;
else if (s.beep=="vib") Bangle.beep = function (time, freq) {
return new Promise(function(resolve) {
if ((0|freq)<=0) freq=4000;
if ((0|time)<=0) time=200;
if (time>5000) time=5000;
analogWrite(D13,0.1,{freq:freq});
setTimeout(function() {
digitalWrite(D13,0);
resolve();
}, time);
});
};
}
Bangle.setLCDTimeout(s.timeout);
if (!s.timeout) Bangle.setLCDPower(1);
E.setTimeZone(s.timezone);
delete s;
// Draw out of memory errors onto the screen
E.on('errorFlag', function(errorFlags) { g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:",errorFlags);
E.getErrorFlags(); // clear flags so we get called next time
});
// stop users doing bad things!
global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }
// Load *.boot.js files

88
apps/boot/hid_info.txt Normal file
View File

@ -0,0 +1,88 @@
## Joystick:
https://github.com/espruino/BangleApps/issues/349#issuecomment-620231524
```
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x04, // Usage (Joystick)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
// Buttons
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Variable, Absolute)
// padding bits
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x03, // Input (Constant)
// Stick
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7f, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data, Variable, Absolute)
0xC0, // End Collection (Physical)
0xC0 // End Collection (Application)
```
## Keyboard
http://www.espruino.com/BLE+Keyboard
```
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0xe0, // Usage Minimum (224)
0x29, 0xe7, // Usage Maximum (231)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Constant) reserved byte(1)
0x95, 0x05, // Report Count (5)
0x75, 0x01, // Report Size (1)
0x05, 0x08, // Usage Page (Page# for LEDs)
0x19, 0x01, // Usage Minimum (1)
0x29, 0x05, // Usage Maximum (5)
0x91, 0x02, // Output (Data, Variable, Absolute), Led report
0x95, 0x01, // Report Count (1)
0x75, 0x03, // Report Size (3)
0x91, 0x01, // Output (Data, Variable, Absolute), Led report padding
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x73, // Logical Maximum (115 - include F13, etc)
0x05, 0x07, // Usage Page (Key codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x73, // Usage Maximum (115 - include F13, etc)
0x81, 0x00, // Input (Data, Array) Key array(6 bytes)
0x09, 0x05, // Usage (Vendor Defined)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Count (2)
0x95, 0x02, // Report Size (8 bit)
0xB1, 0x02, // Feature (Data, Variable, Absolute)
0xC0 // End Collection (Application)
```

View File

@ -0,0 +1,33 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
}

2
apps/buffgym/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Create BuffGym app
0.02: Add web interface for personalising workout

60
apps/buffgym/README.md Normal file
View File

@ -0,0 +1,60 @@
# BuffGym
This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program.
## Configuration
### Setting your start weight values
You will want to set your own starting weight values for your 5x5 training program. To do this is easy! After installing this app, go to the BangleJS app store, connect to your watch, and navigate to the `My Apps` tab. In there you will find this app in the list, and an icon (a down arrow) to the right of the app title. Click that icon to reveal a configuration page. Enter your weights and other details, and click upload. That is it, you are now ready to train!
## Usage
### Start screen
When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start
![](buffgym-scrn1.png)
### Workouts menu
You are then presented with the workouts menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the workout.
![](buffgym-scrn2.png)
### Recording your training
You will now begin moving through the exercises in the workout. You will see the exercise information on the display.
![](buffgym-scrn3.png)
1. At the top is the exercise name, e.g 'Squats'
2. Next is the weight you must train
3. In the center is where you record the number of *reps* you completed (more on that shortly)
4. Below the *reps* value, is the target reps you must try to reach.
5. Below the target reps is the current set you are training, out of the total sets for the exercise.
6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps
### Rest timers
You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise.
![](buffgym-scrn4.png)
![](buffgym-scrn5.png)
### Workout completed
Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are.
![](buffgym-scrn6.png)
## Features
* If you successfully complete all reps and sets for an exercise, it will automatically update your weights for next time
* Has a neat rest timer to make sure you are training optimally
* Doesn't require a mobile phone, most 'smart watches' are just a visual presentation of the mobile phone app, this runs purley on the watch. So why not leave your phone and its distractions out of the gym!
* Clear and simple user interface
## Created by
[Paul Cockrell](https://github.com/paulcockrell) April 2020.

View File

@ -0,0 +1,153 @@
exports = class Exercise {
constructor(params) {
this.completed = false;
this.sets = [];
this.title = params.title;
this.weight = params.weight;
this.weightIncrement = params.weightIncrement;
this.unit = params.unit;
this.restPeriod = params.restPeriod;
this._originalRestPeriod = params.restPeriod;
this._restTimeout = null;
this._restInterval = null;
this._state = null;
}
get humanTitle() {
return `${this.title} ${this.weight}${this.unit}`;
}
get subTitle() {
const totalSets = this.sets.length;
const uncompletedSets = this.sets.filter((set) => !set.isCompleted()).length;
const currentSet = (totalSets - uncompletedSets) + 1;
return `Set ${currentSet} of ${totalSets}`;
}
decRestPeriod() {
this.restPeriod--;
}
addSet(set) {
this.sets.push(set);
}
currentSet() {
return this.sets.filter(set => !set.isCompleted())[0];
}
isLastSet() {
return this.sets.filter(set => !set.isCompleted()).length === 1;
}
isCompleted() {
return !!this.completed;
}
canSetCompleted() {
return this.sets.filter(set => set.isCompleted()).length === this.sets.length;
}
setCompleted() {
if (!this.canSetCompleted()) throw "All sets must be completed";
if (this.canProgress()) this.weight += this.weightIncrement;
this.completed = true;
}
canProgress() {
let completedRepsTotalSum = 0;
let targetRepsTotalSum = 0;
this.sets.forEach(set => completedRepsTotalSum += set.reps);
this.sets.forEach(set => targetRepsTotalSum += set.maxReps);
return (targetRepsTotalSum - completedRepsTotalSum) === 0;
}
startRestTimer(workout) {
this._restTimeout = setTimeout(() => {
this.next(workout);
}, 1000 * this.restPeriod);
this._restInterval = setInterval(() => {
this.decRestPeriod();
if (this.restPeriod < 0) {
this.resetRestTimer();
this.next();
return;
}
workout.emit("redraw");
}, 1000 );
}
resetRestTimer() {
clearTimeout(this._restTimeout);
clearInterval(this._restInterval);
this._restTimeout = null;
this._restInterval = null;
this.restPeriod = this._originalRestPeriod;
}
isRestTimerRunning() {
return this._restTimeout != null;
}
setupStartedButtons(workout) {
clearWatch();
setWatch(() => {
this.currentSet().incReps();
workout.emit("redraw");
}, BTN1, {repeat: true});
setWatch(workout.next.bind(workout), BTN2, {repeat: false});
setWatch(() => {
this.currentSet().decReps();
workout.emit("redraw");
}, BTN3, {repeat: true});
}
setupRestingButtons(workout) {
clearWatch();
setWatch(workout.next.bind(workout), BTN2, {repeat: false});
}
next(workout) {
const STARTED = 1;
const RESTING = 2;
const COMPLETED = 3;
switch(this._state) {
case null:
this._state = STARTED;
this.setupStartedButtons(workout);
break;
case STARTED:
this._state = RESTING;
this.startRestTimer(workout);
this.setupRestingButtons(workout);
break;
case RESTING:
this.resetRestTimer();
this.currentSet().setCompleted();
if (this.canSetCompleted()) {
this._state = COMPLETED;
this.setCompleted();
} else {
this._state = null;
}
// As we are changing state and require it to be reprocessed
// invoke the next step of workout
workout.next();
break;
default:
throw "Exercise: Attempting to move to an unknown state";
}
workout.emit("redraw");
}
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA="))

BIN
apps/buffgym/buffgym-scrn1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
apps/buffgym/buffgym-scrn2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/buffgym/buffgym-scrn3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
apps/buffgym/buffgym-scrn4.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
apps/buffgym/buffgym-scrn5.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
apps/buffgym/buffgym-scrn6.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,28 @@
exports = class Set {
constructor(maxReps) {
this.completed = false;
this.minReps = 0;
this.reps = 0;
this.maxReps = maxReps;
}
isCompleted() {
return !!this.completed;
}
setCompleted() {
this.completed = true;
}
incReps() {
if (this.completed) return;
if (this.reps >= this.maxReps) return;
this.reps++;
}
decReps() {
if (this.completed) return;
if (this.reps <= this.minReps) return;
this.reps--;
}
}

View File

@ -0,0 +1,33 @@
{
"title": "Workout A",
"exercises": [
{
"title": "Squats",
"weight": 40,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Overhead press",
"weight": 20,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Deadlift",
"weight": 20,
"unit": "Kg",
"sets": [5],
"restPeriod": 90
},
{
"title": "Pullups",
"weight": 0,
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
}

View File

@ -0,0 +1,33 @@
{
"title": "Workout B",
"exercises": [
{
"title": "Squats",
"weight": 40,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Bench press",
"weight": 20,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Row",
"weight": 20,
"unit":"Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Tricep extension",
"weight": 20,
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
}

View File

@ -0,0 +1,10 @@
[
{
"title": "Workout A",
"file": "buffgym-workout-a.json"
},
{
"title": "Workout B",
"file": "buffgym-workout-b.json"
}
]

View File

@ -0,0 +1,83 @@
exports = class Workout {
constructor(params) {
this.title = params.title;
this.exercises = [];
this.completed = false;
this.on("redraw", redraw.bind(null, this));
}
addExercises(exercises) {
exercises.forEach(exercise => this.exercises.push(exercise));
}
currentExercise() {
return this.exercises.filter(exercise => !exercise.isCompleted())[0];
}
canComplete() {
return this.exercises.filter(exercise => exercise.isCompleted()).length === this.exercises.length;
}
setCompleted() {
if (!this.canComplete()) throw "All exercises must be completed";
this.completed = true;
}
isCompleted() {
return !!this.completed;
}
static fromJSON(workoutJSON) {
const Set = require("buffgym-set.js");
const Exercise = require("buffgym-exercise.js");
const workout = new this({
title: workoutJSON.title,
});
const exercises = workoutJSON.exercises.map(exerciseJSON => {
const exercise = new Exercise({
title: exerciseJSON.title,
weight: exerciseJSON.weight,
weightIncrement: exerciseJSON.weightIncrement,
unit: exerciseJSON.unit,
restPeriod: exerciseJSON.restPeriod,
});
exerciseJSON.sets.forEach(setJSON => {
exercise.addSet(new Set(setJSON));
});
return exercise;
});
workout.addExercises(exercises);
return workout;
}
toJSON() {
return {
title: this.title,
exercises: this.exercises.map(exercise => {
return {
title: exercise.title,
weight: exercise.weight,
weightIncrement: exercise.weightIncrement,
unit: exercise.unit,
sets: exercise.sets.map(set => set.maxReps),
restPeriod: exercise.restPeriod,
};
}),
};
}
// State machine
next() {
if (this.canComplete()) {
this.setCompleted();
this.emit("redraw");
return;
}
// Call current exercise state machine
this.currentExercise().next(this);
}
}

261
apps/buffgym/buffgym.app.js Executable file
View File

@ -0,0 +1,261 @@
/**
* BangleJS Stronglifts 5x5 training aid
*
* Original Author: Paul Cockrell https://github.com/paulcockrell
* Created: April 2020
*
* Inspired by:
* - Stronglifts 5x5 training workout https://stronglifts.com/5x5/
* - Stronglifts smart watch app
*/
Bangle.setLCDMode("120x120");
const W = g.getWidth();
const H = g.getHeight();
const RED = "#d32e29";
const PINK = "#f05a56";
const WHITE = "#ffffff";
function drawMenu(params) {
const hs = require("heatshrink");
const incImg = hs.decompress(atob("gsFwMAkM+oUA"));
const decImg = hs.decompress(atob("gsFwIEBnwCBA"));
const okImg = hs.decompress(atob("gsFwMAhGFo0A"));
const DEFAULT_PARAMS = {
showBTN1: false,
showBTN2: false,
showBTN3: false,
};
const p = Object.assign({}, DEFAULT_PARAMS, params);
if (p.showBTN1) g.drawImage(incImg, W - 10, 10);
if (p.showBTN2) g.drawImage(okImg, W - 10, 60);
if (p.showBTN3) g.drawImage(decImg, W - 10, 110);
}
function drawSet(exercise) {
const set = exercise.currentSet();
if (set.isCompleted()) return;
g.clear();
// Draw exercise title
g.setColor(PINK);
g.fillRect(15, 0, W - 15, 18);
g.setFontAlign(0, -1);
g.setFont("6x8", 1);
g.setColor(WHITE);
g.drawString(exercise.title, W / 2, 5);
g.setFont("6x8", 1);
g.drawString(exercise.weight + " " + exercise.unit, W / 2, 27);
// Draw completed reps counter
g.setFontAlign(0, 0);
g.setColor(PINK);
g.fillRect(15, 42, W - 15, 80);
g.setColor(WHITE);
g.setFont("6x8", 5);
g.drawString(set.reps, (W / 2) + 2, (H / 2) + 1);
g.setFont("6x8", 1);
const note = `Target reps: ${set.maxReps}`;
g.drawString(note, W / 2, H - 24);
// Draw sets monitor
g.drawString(exercise.subTitle, W / 2, H - 12);
drawMenu({showBTN1: true, showBTN2: true, showBTN3: true});
g.flip();
}
function drawWorkoutDone() {
const title1 = "You did";
const title2 = "GREAT!";
const msg = "That's the workout\ncompleted. Now eat\nsome food and\nget plenty of rest.";
clearWatch();
setWatch(Bangle.showLauncher, BTN2, {repeat: false});
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString(title1, W / 2, 10);
g.drawString(title2, W / 2, 30);
g.setFont("6x8", 1);
g.drawString(msg, (W / 2) + 3, 70);
g.flip();
}
function drawSetComp(exercise) {
const title = "Good work";
const msg1= "No need to rest\nmove straight on\nto the next\nexercise.";
const msg2 = exercise.canProgress()?
"Your\nweight has been\nincreased for\nnext time!":
"You'll\nsmash it next\ntime!";
g.clear();
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString(title, W / 2, 10);
g.setFont("6x8", 1);
g.drawString(msg1 + msg2, (W / 2) - 2, 45);
g.flip();
}
function drawRestTimer(exercise) {
g.clear();
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(PINK);
g.fillRect(15, 42, W - 15, 80);
g.setColor(WHITE);
g.setFont("6x8", 1);
g.drawString("Have a short\nrest period.", W / 2, 10);
g.setFont("6x8", 5);
g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19);
g.flip();
}
function redraw(workout) {
const exercise = workout.currentExercise();
g.clear();
if (workout.isCompleted()) {
saveWorkout(workout);
drawWorkoutDone();
return;
}
if (exercise.isRestTimerRunning()) {
if (exercise.isLastSet()) {
drawSetComp(exercise);
} else {
drawRestTimer(exercise);
}
return;
}
drawSet(exercise);
}
function drawWorkoutMenu(workouts, selWorkoutIdx) {
g.clear();
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString("BuffGym", W / 2, 10);
g.setFont("6x8", 1);
g.setFontAlign(-1, -1);
let selectedWorkout = workouts[selWorkoutIdx].title;
let yPos = 50;
workouts.forEach(workout => {
g.setColor("#f05a56");
g.fillRect(0, yPos, W, yPos + 11);
g.setColor("#ffffff");
if (selectedWorkout === workout.title) {
g.drawRect(0, yPos, W - 1, yPos + 11);
}
g.drawString(workout.title, 10, yPos + 2);
yPos += 15;
});
g.flip();
}
function setupMenu() {
clearWatch();
const workouts = getWorkoutIndex();
let selWorkoutIdx = 0;
drawWorkoutMenu(workouts, selWorkoutIdx);
setWatch(()=>{
selWorkoutIdx--;
if (selWorkoutIdx< 0) selWorkoutIdx = 0;
drawWorkoutMenu(workouts, selWorkoutIdx);
}, BTN1, {repeat: true});
setWatch(()=>{
const workout = buildWorkout(workouts[selWorkoutIdx].file);
workout.next();
}, BTN2, {repeat: false});
setWatch(()=>{
selWorkoutIdx++;
if (selWorkoutIdx > workouts.length - 1) selWorkoutIdx = workouts.length - 1;
drawWorkoutMenu(workouts, selWorkoutIdx);
}, BTN3, {repeat: true});
}
function drawSplash() {
g.reset();
g.setBgColor(RED);
g.clear();
g.setColor(WHITE);
g.setFontAlign(0,-1);
g.setFont("6x8", 2);
g.drawString("BuffGym", W / 2, 10);
g.setFont("6x8", 1);
g.drawString("5x5", W / 2, 42);
g.drawString("training app", W / 2, 55);
g.drawRect(19, 38, 100, 99);
const img = require("heatshrink").decompress(atob("lkdxH+AB/I5ASQACwpB5vNFkwpBAIfNFdZZkFYwskFZAsiFZBZiVYawEFf6ETFUwsIFUYmB54ADAwIskFYoRKBoIroB4grV58kkgCDFRotWFZwqHFiwYMFZIsTC5wLDFjGlCoWlkgJDRQIABCRAsLCwodCFAIABCwIOCFQYABr4RCCQIrMC4gqEAAwpFFZosFC5ArHFQ4rFNYQrGEgosMBxIrFLQwrLAB4sFSw4rFFjYrQFi4rNbASeEFjIoJFQYsGMAgAPEQgAIGwosCRoorbA="));
g.drawImage(img, 40, 70);
g.flip();
let flasher = false;
let bgCol, txtCol;
const i = setInterval(() => {
if (flasher) {
bgCol = WHITE;
txtCol = RED;
} else {
bgCol = RED;
txtCol = WHITE;
}
flasher = !flasher;
g.setColor(bgCol);
g.fillRect(0, 108, W, 120);
g.setColor(txtCol);
g.drawString("Press btn to begin", W / 2, 110);
g.flip();
}, 250);
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN1, {repeat: false});
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN2, {repeat: false});
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN3, {repeat: false});
}
function getWorkoutIndex() {
const workoutIdx = require("Storage").readJSON("buffgym-workout-index.json");
return workoutIdx;
}
function buildWorkout(fName) {
const Workout = require("buffgym-workout.js");
const workoutJSON = require("Storage").readJSON(fName);
const workout = Workout.fromJSON(workoutJSON);
return workout;
}
function saveWorkout(workout) {
const fName = getWorkoutIndex().find(w => w.title === workout.title).file;
require("Storage").writeJSON(fName, workout.toJSON());
}
drawSplash();

250
apps/buffgym/buffgym.html Normal file
View File

@ -0,0 +1,250 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<h1>BuffGym</h1>
<p>
Enter in your weights for each exercise, start light and keep consistent with your training. The weight increment field is how much the app will increase your weights for an exercise if you successfully complete all the reps and sets for an exercise. Make sure its a value that matches the weights in your gym.
</p>
<p>
For more information on how to train this program refer the <a href="https://stronglifts.com/5x5/" target="_BLANK">Stronglifts website</a>
</p>
<form id="workouts-form">
<h4>Workout A</h4>
<table class="table">
<thead>
<tr>
<th>Exercise</th>
<th>Sets / Reps</th>
<th>Weight</th>
<th>Weight increment</th>
</tr>
</thead>
<tbody id="workout-a-exercises">
<tr>
<td>
Squats
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-squats-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-squats-weight-increment" />
</td>
</tr>
<tr>
<td>
Overhead press
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-overhead-press-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-overhead-press-weight-increment" />
</td>
</tr>
<tr>
<td>
Deadlift
</td>
<td>
1x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-deadlift-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-deadlift-weight-increment" />
</td>
</tr>
<tr>
<td>
Pullups
</td>
<td>
3x10
</td>
<td>
<input type="number" value="0" id="buffgym-workout-a-pullups-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-a-pullups-weight-increment" />
</td>
</tr>
</tbody>
</table>
<h4>Workout B</h4>
<table class="table">
<thead>
<tr>
<th>Exercise</th>
<th>Sets / Reps</th>
<th>Weight</th>
<th>Weight increment</th>
</tr>
</thead>
<tbody id="workout-b-exercises">
<tr>
<td>
Squats
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-squats-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-squats-weight-increment" />
</td>
</tr>
<tr>
<td>
Bench press
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-bench-press-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-bench-press-weight-increment" />
</td>
</tr>
<tr>
<td>
Row
</td>
<td>
5x5
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-row-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-row-weight-increment" />
</td>
</tr>
<tr>
<td>
Tricep extension
</td>
<td>
3x10
</td>
<td>
<input type="number" value="0" id="buffgym-workout-b-triceps-weight" />
</td>
<td>
<input type="number" value="2.5" id="buffgym-workout-b-triceps-weight-increment" />
</td>
</tr>
</tbody>
</table>
</form>
<br><br>
<button id="upload" class="btn btn-primary">Upload</button>
<script src="../../lib/interface.js"></script>
<script>
function workoutA() {
return {
"title": "Workout A",
"exercises": [
{
"title": "Squats",
"weight": Number(document.getElementById("buffgym-workout-a-squats-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-squats-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Overhead press",
"weight": Number(document.getElementById("buffgym-workout-a-overhead-press-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-overhead-press-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Deadlift",
"weight": Number(document.getElementById("buffgym-workout-a-deadlift-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-deadlift-weight-increment").value),
"unit": "Kg",
"sets": [5],
"restPeriod": 90
},
{
"title": "Pullups",
"weight": Number(document.getElementById("buffgym-workout-a-pullups-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-a-pullups-weight-increment").value),
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
};
}
function workoutB() {
return {
"title": "Workout B",
"exercises": [
{
"title": "Squats",
"weight": Number(document.getElementById("buffgym-workout-b-squats-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-squats-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Bench press",
"weight": Number(document.getElementById("buffgym-workout-b-bench-press-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-bench-press-weight-increment").value),
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Row",
"weight": Number(document.getElementById("buffgym-workout-b-row-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-row-weight-increment").value),
"unit":"Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Tricep extension",
"weight": Number(document.getElementById("buffgym-workout-b-triceps-weight").value),
"weightIncrement": Number(document.getElementById("buffgym-workout-b-triceps-weight-increment").value),
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
};
}
document.getElementById("upload").addEventListener("click", function() {
Puck.eval(`require("Storage").writeJSON("buffgym-workout-a.json",${JSON.stringify(workoutA())})`, ()=>{
Puck.eval(`require("Storage").writeJSON("buffgym-workout-b.json",${JSON.stringify(workoutB())})`, ()=>{
Puck.eval(`Bangle.buzz()`, () => {
console.log("all done");
})
})
});
});
</script>
</body>
</html>

BIN
apps/buffgym/buffgym.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -0,0 +1,2 @@
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>

392
apps/calculator/app.js Normal file
View File

@ -0,0 +1,392 @@
/**
* BangleJS Calculator
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*/
g.clear();
Graphics.prototype.setFont7x11Numeric7Seg = function() {
this.setFontCustom(atob("ACAB70AYAwBgC94AAAAAAAAAAB7wAAPQhhDCGELwAAAAhDCGEMIXvAAeACAEAIAQPeAA8CEMIYQwhA8AB70IYQwhhCB4AAAIAQAgBAB7wAHvQhhDCGEL3gAPAhDCGEMIXvAAe9CCEEIIQPeAA94EIIQQghA8AB70AYAwBgCAAAAHgQghBCCF7wAHvQhhDCGEIAAAPehBCCEEIAAAAA=="), 46, atob("AgAHBwcHBwcHBwcHAAAAAAAAAAcHBwcHBw=="), 11);
};
var DEFAULT_SELECTION = '5';
var BOTTOM_MARGIN = 10;
var RIGHT_MARGIN = 20;
var COLORS = {
// [normal, selected]
DEFAULT: ['#7F8183', '#A6A6A7'],
OPERATOR: ['#F99D1C', '#CA7F2A'],
SPECIAL: ['#65686C', '#7F8183']
};
var keys = {
'0': {
xy: [0, 200, 120, 240],
trbl: '2.00'
},
'.': {
xy: [120, 200, 180, 240],
trbl: '3=.0'
},
'=': {
xy: [181, 200, 240, 240],
trbl: '+==.',
color: COLORS.OPERATOR
},
'1': {
xy: [0, 160, 60, 200],
trbl: '4201'
},
'2': {
xy: [60, 160, 120, 200],
trbl: '5301'
},
'3': {
xy: [120, 160, 180, 200],
trbl: '6+.2'
},
'+': {
xy: [181, 160, 240, 200],
trbl: '-+=3',
color: COLORS.OPERATOR
},
'4': {
xy: [0, 120, 60, 160],
trbl: '7514'
},
'5': {
xy: [60, 120, 120, 160],
trbl: '8624'
},
'6': {
xy: [120, 120, 180, 160],
trbl: '9-35'
},
'-': {
xy: [181, 120, 240, 160],
trbl: '*-+6',
color: COLORS.OPERATOR
},
'7': {
xy: [0, 80, 60, 120],
trbl: 'R847'
},
'8': {
xy: [60, 80, 120, 120],
trbl: 'N957'
},
'9': {
xy: [120, 80, 180, 120],
trbl: '%*68'
},
'*': {
xy: [181, 80, 240, 120],
trbl: '/*-9',
color: COLORS.OPERATOR
},
'R': {
xy: [0, 40, 60, 79],
trbl: 'RN7R',
color: COLORS.SPECIAL,
val: 'AC'
},
'N': {
xy: [60, 40, 120, 79],
trbl: 'N%8R',
color: COLORS.SPECIAL,
val: '+/-'
},
'%': {
xy: [120, 40, 180, 79],
trbl: '%/9N',
color: COLORS.SPECIAL
},
'/': {
xy: [181, 40, 240, 79],
trbl: '//*%',
color: COLORS.OPERATOR
}
};
var selected = DEFAULT_SELECTION;
var prevSelected = DEFAULT_SELECTION;
var prevNumber = null;
var currNumber = null;
var operator = null;
var results = null;
var isDecimal = false;
var hasPressedEquals = false;
function drawKey(name, k, selected) {
var rMargin = 0;
var bMargin = 0;
var color = k.color || COLORS.DEFAULT;
g.setColor(color[selected ? 1 : 0]);
g.setFont('Vector', 20);
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
g.setColor(-1);
// correct margins to center the texts
if (name == '0') {
rMargin = (RIGHT_MARGIN * 2) - 7;
} else if (name === '/') {
rMargin = 5;
} else if (name === '*') {
bMargin = 5;
rMargin = 3;
} else if (name === '-') {
rMargin = 3;
} else if (name === 'R' || name === 'N') {
rMargin = k.val === 'C' ? 0 : -9;
} else if (name === '%') {
rMargin = -3;
}
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) {
switch (operator) {
case '/':
return divide(x, y);
case '*':
return multiply(x, y);
case '+':
return sum(x, y);
case '-':
return subtract(x, y);
}
}
function displayOutput(num) {
var len;
var minusMarge = 0;
g.setColor(0);
g.fillRect(0, 0, 240, 39);
g.setColor(-1);
if (num === Infinity || num === -Infinity || isNaN(num)) {
// handle division by 0
if (num === Infinity) {
num = 'INFINITY';
} else if (num === -Infinity) {
num = '-INFINITY';
} else {
num = 'NOT A NUMBER';
minusMarge = -25;
}
len = (num + '').length;
currNumber = null;
results = null;
isDecimal = false;
hasPressedEquals = false;
prevNumber = null;
operator = null;
keys.R.val = 'AC';
drawKey('R', keys.R);
g.setFont('Vector', 22);
} else {
// might not be a number due to display of dot "."
var numNumeric = Number(num);
if (typeof num === 'string') {
if (num.indexOf('.') !== -1) {
// display a 0 before a lonely dot
if (numNumeric == 0) {
num = '0.';
}
} else {
// remove preceding 0
while (num.length > 1 && num[0] === '0')
num = num.substr(1);
}
}
len = (num + '').length;
if (numNumeric < 0 || (numNumeric === 0 && 1/numNumeric === -Infinity)) {
// minus is not available in font 7x11Numeric7Seg, we use Vector
g.setFont('Vector', 20);
g.drawString('-', 220 - (len * 15), 10);
minusMarge = 15;
}
g.setFont('7x11Numeric7Seg', 2);
}
g.drawString(num, 220 - (len * 15) + minusMarge, 10);
}
var wasPressedEquals = false;
var hasPressedNumber = false;
function calculatorLogic(x) {
if (wasPressedEquals && hasPressedNumber !== false) {
prevNumber = null;
currNumber = hasPressedNumber;
wasPressedEquals = false;
hasPressedNumber = false;
return;
}
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
results = doMath(prevNumber, currNumber, operator);
operator = x;
prevNumber = results;
currNumber = null;
displayOutput(results);
} else if (prevNumber == null && currNumber != null && operator == null) {
// no operator yet, save the current number for later use when an operator is pressed
operator = x;
prevNumber = currNumber;
currNumber = null;
displayOutput(prevNumber);
} else if (prevNumber == null && currNumber == null && operator == null) {
displayOutput(0);
}
}
function buttonPress(val) {
switch (val) {
case 'R':
currNumber = null;
results = null;
isDecimal = false;
hasPressedEquals = false;
if (keys.R.val == 'AC') {
prevNumber = null;
operator = null;
} else {
keys.R.val = 'AC';
drawKey('R', keys.R, true);
}
wasPressedEquals = false;
hasPressedNumber = false;
displayOutput(0);
break;
case '%':
if (results != null) {
displayOutput(results /= 100);
} else if (currNumber != null) {
displayOutput(currNumber /= 100);
}
hasPressedNumber = false;
break;
case 'N':
if (results != null) {
displayOutput(results *= -1);
} else {
displayOutput(currNumber *= -1);
}
break;
case '/':
case '*':
case '-':
case '+':
calculatorLogic(val);
hasPressedNumber = false;
break;
case '.':
keys.R.val = 'C';
drawKey('R', keys.R);
isDecimal = true;
displayOutput(currNumber == null ? 0 + '.' : currNumber + '.');
break;
case '=':
if (prevNumber != null && currNumber != null && operator != null) {
results = doMath(prevNumber, currNumber, operator);
prevNumber = results;
displayOutput(results);
hasPressedEquals = 1;
}
hasPressedNumber = false;
break;
default:
keys.R.val = 'C';
drawKey('R', keys.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
if (isDecimal) {
currNumber = currNumber == null || hasPressedEquals === 1 ? 0 + '.' + val : currNumber + '.' + val;
isDecimal = false;
} else {
currNumber = currNumber == null || hasPressedEquals === 1 ? val : (is0Negative ? '-' + val : currNumber + val);
}
if (hasPressedEquals === 1) {
hasPressedEquals = 2;
}
hasPressedNumber = currNumber;
displayOutput(currNumber);
break;
}
}
for (var k in keys) {
if (keys.hasOwnProperty(k)) {
drawKey(k, keys[k], k == '5');
}
}
g.setFont('7x11Numeric7Seg', 2.8);
g.drawString('0', 205, 10);
function moveDirection(d) {
drawKey(selected, keys[selected]);
prevSelected = selected;
selected = (d === 0 && selected == '0' && prevSelected === '1') ? '1' : keys[selected].trbl[d];
drawKey(selected, keys[selected], true);
}
setWatch(_ => moveDirection(0), BTN1, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(2), BTN3, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(3), BTN4, {repeat: true, debounce: 100});
setWatch(_ => moveDirection(1), BTN5, {repeat: true, debounce: 100});
setWatch(_ => buttonPress(selected), BTN2, {repeat: true, debounce: 100});

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhBC/AC8r6/XlYvr64CEF9UrMIIv/R/7vTMwIAmlUklQGDroAFqwHGBRgJBqwMDq+k5nNABAWDC4QZFERAvGBQOBF5I0FCYNW1mImWs6+sDoQsDAYIJEAAeB2eB1mBA4QvF43P6/GF4mB6+BAQYlEro3BAAI3FDAezBYgvE43O64DBF4hbCAAMrGAIiFBYRUEHogaBxA6CF4vXLwPHF4giEDIIkDDgI2BFoI6FBgYWCF5PPF4rSBKwVWI4bAFFgdcYAykBX5HX53NFwfNfwIkDAQYAGBBAKCIIYABd4y9DAAJ9CAD9dF4gAGCIi8BABLXBBRQLEF4vHRwgvEERQ6DHpgvH66PB65fUBpZfJ4/G6wxBMIaPbL5QvB6/WF6hqNF5KPDF6jkGd6JeBF5AAdF4oAGDBeH1mHAAwIBF8esABQvdWQonDX4YvIYAq/GXobvNF4hfKCwwvF43GF5AXGL44vJLwgvE453DMIYuFR5JiHI4yPHRoaREIwpIFF7TvbR5BJCX5IvMADgvcroABF6vG4wvIX46DKBZYvEFwPHGAgZHERALRF4YuBHYIwEFxxfPF5CDDF6ZfLDAyPFFwovFKRYvV47vDAgIvRR5aOFL4orCFwbvHADYvEAA4YLdRYvQ45eBR5C6UF5vHX4LvJF8PGZYXXGAYvnLYYvfZ4xfXd6AvKGAK/RDAKNTF4wAG44="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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>

1
apps/calendar/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Basic calendar

8
apps/calendar/README.md Normal file
View File

@ -0,0 +1,8 @@
# Calendar
Basic calendar
## Usage
- Use `BTN4` (left screen tap) to go to the previous month
- Use `BTN5` (right screen tap) to go to the next month

View File

@ -0,0 +1,5 @@
require("heatshrink").decompress(
atob(
"mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA=="
)
)

160
apps/calendar/calendar.js Normal file
View File

@ -0,0 +1,160 @@
const maxX = 240;
const maxY = 240;
const rowN = 7;
const colN = 7;
const headerH = maxY / 7;
const rowH = (maxY - headerH) / rowN;
const colW = maxX / colN;
const color1 = "#035AA6";
const color2 = "#4192D9";
const color3 = "#026873";
const color4 = "#038C8C";
const color5 = "#03A696";
const black = "#000000";
const white = "#ffffff";
const gray1 = "#444444";
const gray2 = "#888888";
const gray3 = "#bbbbbb";
const red = "#d41706";
function drawCalendar(date) {
g.setBgColor(color4);
g.clearRect(0, 0, maxX, maxY);
g.setBgColor(color1);
g.clearRect(0, 0, maxX, headerH);
g.setBgColor(color2);
g.clearRect(0, headerH, maxX, headerH + rowH);
g.setBgColor(color3);
g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
for (let y = headerH; y < maxY; y += rowH) {
g.drawLine(0, y, maxX, y);
}
for (let x = 0; x < maxX; x += colW) {
g.drawLine(x, headerH, x, maxY);
}
const month = date.getMonth();
const year = date.getFullYear();
const monthMap = {
0: "January",
1: "February",
2: "March",
3: "April",
4: "May",
5: "June",
6: "July",
7: "August",
8: "September",
9: "October",
10: "November",
11: "December"
};
g.setFontAlign(0, 0);
g.setFont("6x8", 2);
g.setColor(white);
g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2);
g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true);
g.drawPoly(
[maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10],
true
);
g.setFont("6x8", 2);
const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
dowLbls.forEach((lbl, i) => {
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
});
date.setDate(1);
const dow = date.getDay();
const dowNorm = dow === 0 ? 7 : dow;
const monthMaxDayMap = {
0: 31,
1: (2020 - year) % 4 === 0 ? 29 : 28,
2: 31,
3: 30,
4: 31,
5: 30,
6: 31,
7: 31,
8: 30,
9: 31,
10: 30,
11: 31
};
let days = [];
let nextMonthDay = 1;
let thisMonthDay = 51;
let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm;
for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
if (i < dowNorm) {
days.push(prevMonthDay);
prevMonthDay++;
} else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
days.push(thisMonthDay);
thisMonthDay++;
} else {
days.push(nextMonthDay);
nextMonthDay++;
}
}
let i = 0;
for (y = 0; y < rowN - 1; y++) {
for (x = 0; x < colN; x++) {
i++;
const day = days[i];
const isToday =
today.year === year && today.month === month && today.day === day - 50;
if (isToday) {
g.setColor(red);
g.drawRect(
x * colW,
y * rowH + headerH + rowH,
x * colW + colW - 1,
y * rowH + headerH + rowH + rowH
);
}
g.setColor(day < 50 ? gray3 : white);
g.drawString(
(day > 50 ? day - 50 : day).toString(),
x * colW + colW / 2,
headerH + rowH + y * rowH + rowH / 2
);
}
}
}
const date = new Date();
const today = {
day: date.getDate(),
month: date.getMonth(),
year: date.getFullYear()
};
drawCalendar(date);
clearWatch();
setWatch(
() => {
const month = date.getMonth();
const prevMonth = month > 0 ? month - 1 : 11;
if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
date.setMonth(prevMonth);
drawCalendar(date);
},
BTN4,
{ repeat: true }
);
setWatch(
() => {
const month = date.getMonth();
const prevMonth = month < 11 ? month + 1 : 0;
if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1);
date.setMonth(month + 1);
drawCalendar(date);
},
BTN5,
{ repeat: true }
);
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });

BIN
apps/calendar/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

3
apps/chronowid/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: New widget and app!
0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme)
0.03: Display only minutes:seconds when less than 1 hour left

38
apps/chronowid/README.md Normal file
View File

@ -0,0 +1,38 @@
# Chronometer Widget
Chronometer (timer) that runs as a widget.
The advantage is, that you can still see your normal watchface and other widgets when the timer is running.
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.
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
TBD
## Features
* Using other apps does not interrupt the timer, no need to keep the widget open (BUT: there will be no buzz when the time is up, for that the widget has to be loaded)
* Target time is saved to a file and timer picks up again when widget is loaded again.
## Settings
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
* Minutes: Set the minutes 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 to load the widget which starts the timer. The widget is always there, but only visible when timer is on.
## Releases
* 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#)
* Development: https://github.com/Purple-Tentacle/BangleAppsDev/tree/master/apps/chronowid
## Requests
If you have any feature requests, please write here: http://forum.espruino.com/conversations/345972/

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwIFCn/8BYYFRABcD4AFFgIFCh/wgeAAoP//8HCYMDAoPD8EAg4FB8PwgEf+EP/H4HQOAgP8uEAvwfBv0ggBFCn4CB/EBwEfgEB+AFBh+AgfgAoI1BIoQJB4AHBAoXgg4uBAIIFCCYQFGh5rDJQJUBK4IFCNYIFVDoopDGoJiBHYYFKVYRZBWIYDBA4IFBNIQzBG4IbBToKkBAQKVFUIYICVoQUCXIQmCYoIsCaITqDAoLvDNYUAA="))

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