pull/309/head
msdeibel 2020-04-14 14:02:32 +02:00
commit 123d53aeb4
45 changed files with 1456 additions and 59 deletions

View File

@ -7,3 +7,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* 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

View File

@ -119,7 +119,7 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
"version":"0.12",
"version":"0.13",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"storage": [
@ -915,7 +915,7 @@
{ "id": "marioclock",
"name": "Mario Clock",
"icon": "marioclock.png",
"version":"0.09",
"version":"0.12",
"description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.",
"tags": "clock,mario,retro",
"type": "clock",
@ -1108,6 +1108,20 @@
{"name":"openstmap.app.js","url":"app.js"},
{"name":"openstmap.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "activepedom",
"name": "Active Pedometer",
"shortName":"Active Pedometer",
"icon": "app.png",
"version":"0.01",
"description": "Pedometer that filters out arm movement and displays a step goal progress.",
"tags": "outdoors,widget",
"type":"widget",
"storage": [
{"name":"activepedom.wid.js","url":"widget.js"},
{"name":"activepedom.settings.js","url":"settings.js"},
{"name":"activepedom.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "tabata",
"name": "Tabata",
@ -1158,5 +1172,72 @@
{"name":"batchart.app.js","url":"app.js"},
{"name":"batchart.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "nato",
"name": "NATO Alphabet",
"shortName" : "NATOAlphabet",
"icon": "nato.png",
"version":"0.01",
"type": "app",
"description": "Learn the NATO Phonetic alphabet plus some numbers.",
"tags": "app,learn,visual",
"allow_emulator":true,
"storage": [
{"name":"nato.app.js","url":"nato.js"},
{"name":"nato.img","url":"nato-icon.js","evaluate":true}
]
},
{ "id": "numerals",
"name": "Numerals Clock",
"shortName": "Numerals Clock",
"icon": "numerals.png",
"version":"0.01",
"description": "A simple big numerals clock",
"tags": "numerals,clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"numerals.app.js","url":"numerals.app.js"},
{"name":"numerals.img","url":"numerals-icon.js","evaluate":true},
{"name":"numerals.settings.js","url":"numerals.settings.js"}
]
},
{ "id": "bledetect",
"name": "BLE Detector",
"shortName":"BLE Detector",
"icon": "bledetect.png",
"version":"0.02",
"description": "Detect BLE devices and show some informations.",
"tags": "app,bluetooth,tool",
"readme": "README.md",
"storage": [
{"name":"bledetect.app.js","url":"bledetect.js"},
{"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true}
]
},
{ "id": "snake",
"name": "Snake",
"shortName":"Snake",
"icon": "snake.png",
"version":"0.01",
"description": "The classic snake game. Eat apples and don't bite your tail:",
"tags": "game,fun",
"readme": "README.md",
"storage": [
{"name":"snake.app.js","url":"snake.js"},
{"name":"snake.img","url":"snake-icon.js","evaluate":true}
]
},
{ "id": "calculator",
"name": "Calculator",
"shortName":"Calculator",
"icon": "calculator.png",
"version":"0.01",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus. Push button1 and 3 to navigate up/down, tap right or left to navigate the sides, push button 2 to select.",
"tags": "app,tool",
"storage": [
{"name":"calculator.app.js","url":"app.js"},
{"name":"calculator.img","url":"calculator-icon.js","evaluate":true}
]
}
]

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 @@
0.01: New Widget!

View File

@ -0,0 +1,38 @@
# Improved 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.
## Screenshots
* 600 steps
![](600.png)
* 1600 steps
![](1600.png)
* 10600 steps
![](10600.png)
## Features
* Two line display
* Large number for good readability
* Small number with the exact steps counted
* 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
## Development version
* 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="))

BIN
apps/activepedom/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,81 @@
// 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';
// initialize with default settings...
let s = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
};
// ...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'),
},
};
E.showMenu(menu);
});

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

@ -0,0 +1,180 @@
(() => {
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 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();
var width = 45;
var stepsTooShort = 0;
var stepsTooLong = 0;
var stepsOutsideTime = 0;
//define default settings
const DEFAULTS = {
'cMaxTime' : 1100,
'cMinTime' : 240,
'stepThreshold' : 30,
'intervalResetActive' : 30000,
'stepSensitivity' : 80,
'stepGoal' : 10000,
};
const SETTINGS_FILE = 'activepedom.settings.json';
const PEDOMFILE = "activepedom.steps.json";
let settings;
//load settings
function loadSettings() {
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
}
//return setting
function setting(key) {
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 kFormatter(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 note 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 note counted, because time too short
steps--;
}
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 reac 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++;
}
}
function draw() {
var height = 23; //width is deined globally
var stepsDisplayLarge = kFormatter(stepsCounted);
//Check if same day
let date = new Date();
if (lastUpdate.getDate() == date.getDate()){ //if same day
}
else {
stepsCounted = 1; //set stepcount to 1
}
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);
g.drawString(stepsDisplayLarge,this.x+1,this.y); //first line, big number
g.setFont("6x8", 1);
g.setColor(0xFFFF); //white
g.drawString(stepsCounted,this.x+1,this.y+14); //second line, small number
//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
}
//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
};
require("Storage").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 = require("Storage").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;
}
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
//Add widget
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw};
})();

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

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

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

@ -0,0 +1,352 @@
/**
* 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 doMath(x, y, operator) {
// might not be a number due to display of dot "." algo
x = Number(x);
y = Number(y);
switch (operator) {
case '/':
return x / y;
case '*':
return x * y;
case '+':
return x + y;
case '-':
return 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) {
// 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);
}
function calculatorLogic(x) {
if (hasPressedEquals) {
currNumber = results;
prevNumber = null;
operator = null;
results = null;
isDecimal = null;
displayOutput(currNumber);
hasPressedEquals = false;
}
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);
}
displayOutput(0);
break;
case '%':
if (results != null) {
displayOutput(results /= 100);
} else if (currNumber != null) {
displayOutput(currNumber /= 100);
}
break;
case 'N':
if (results != null) {
displayOutput(results *= -1);
} else if (currNumber != null) {
displayOutput(currNumber *= -1);
}
break;
case '/':
case '*':
case '-':
case '+':
calculatorLogic(val);
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 = true;
}
break;
default:
keys.R.val = 'C';
drawKey('R', keys.R);
if (isDecimal) {
currNumber = currNumber == null ? 0 + '.' + val : currNumber + '.' + val;
isDecimal = false;
} else {
currNumber = currNumber == null ? val : currNumber + val;
}
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);
setWatch(function() {
drawKey(selected, keys[selected]);
// key 0 is 2 keys wide, go up to 1 if it was previously selected
if (selected == '0' && prevSelected === '1') {
prevSelected = selected;
selected = '1';
} else {
prevSelected = selected;
selected = keys[selected].trbl[0];
}
drawKey(selected, keys[selected], true);
}, BTN1, {repeat: true, debounce: 100});
setWatch(function() {
drawKey(selected, keys[selected]);
prevSelected = selected;
selected = keys[selected].trbl[2];
drawKey(selected, keys[selected], true);
}, BTN3, {repeat: true, debounce: 100});
Bangle.on('touch', function(direction) {
drawKey(selected, keys[selected]);
prevSelected = selected;
if (direction == 1) {
selected = keys[selected].trbl[3];
} else if (direction == 2) {
selected = keys[selected].trbl[1];
}
drawKey(selected, keys[selected], true);
});
setWatch(function() {
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

View File

@ -6,4 +6,7 @@
0.06: Performance refactor, and enhanced graphics!
0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode
0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy
0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel
0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel
0.10: Swiping left to enable night-mode now also reduces LCD brightness through 3 levels before returning to day-mode.
0.11: User settings persisted and read to file.
0.12: Add info banner message when phone (dis)connects. Display low-battery warning (<=10%)

View File

@ -8,7 +8,7 @@ Enjoy watching Mario, or one of the other game characters run through a level wh
## Features
* Multiple characters - swipe the screen right to change the character between `Mario`, `Toad`, and `Daisy`
* Night and Day modes - swipe left to toggle mode
* Night and Day modes - swipe left to enter night mode, with 3 levels of darkness before returning to day mode.
* Smooth animation
* Awesome 8-bit style grey-scale graphics
* Mario jumps to change the time, every minute

View File

@ -16,6 +16,8 @@ const is12Hour = settings["12hour"] || false;
// Screen dimensions
let W, H;
// Screen brightness
let brightness = 1;
let intervalRef, displayTimeoutRef = null;
@ -79,6 +81,16 @@ const phone = {
messageType: null,
};
const SETTINGS_FILE = "marioclock.json";
function readSettings() {
return require('Storage').readJSON(SETTINGS_FILE, 1) || {};
}
function writeSettings(newSettings) {
require("Storage").writeJSON(SETTINGS_FILE, newSettings);
}
function phoneOutbound(msg) {
Bluetooth.println(JSON.stringify(msg));
}
@ -164,7 +176,17 @@ function switchCharacter() {
}
function toggleNightMode() {
nightMode = !nightMode;
if (!nightMode) {
nightMode = true;
return;
}
brightness -= 0.30;
if (brightness <= 0) {
brightness = 1;
nightMode = false;
}
Bangle.setLCDBrightness(brightness);
}
function incrementTimer() {
@ -324,16 +346,20 @@ function drawToadFrame(idx, x, y) {
function drawNotice(x, y) {
if (phone.message === null) return;
let img;
switch (phone.messageType) {
case "call":
const callImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo="));
g.drawImage(callImg, characterSprite.x, characterSprite.y - 16);
img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo="));
break;
case "notify":
const msgImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA="));
g.drawImage(msgImg, characterSprite.x, characterSprite.y - 16);
img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA="));
break;
case "lowBatt":
img = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INFrgABB4oOEBoQPFBwwDGB0uHAAIOLJRB3OSpApDBoQAHB4INLByANNAwo"));
break;
}
if (img) g.drawImage(img, characterSprite.x, characterSprite.y - 16);
}
function drawCharacter(date, character) {
@ -555,8 +581,39 @@ function startTimers(){
redraw();
}
function loadSettings() {
const settings = readSettings();
if (!settings) return;
if (settings.character) characterSprite.character = settings.character;
if (settings.nightMode) nightMode = settings.nightMode;
if (settings.brightness) {
brightness = settings.brightness;
Bangle.setLCDBrightness(brightness);
}
}
function updateSettings() {
const newSettings = {
character: characterSprite.character,
nightMode: nightMode,
brightness: brightness,
};
writeSettings(newSettings);
}
function checkBatteryLevel() {
if (Bangle.isCharging()) return;
if (E.getBattery() > 10) return;
if (phone.message !== null) return;
phoneNewMessage("lowBatt", "Warning, battery is low");
}
// Main
function init() {
loadSettings();
clearInterval();
// Initialise display
@ -606,23 +663,31 @@ function init() {
default:
toggleNightMode();
}
updateSettings();
});
// Phone connectivity
try { NRF.wake(); } catch (e) {}
NRF.on('disconnect', () => Bangle.buzz());
NRF.on('disconnect', () => {
phoneNewMessage(null, "Phone disconnected");
});
NRF.on('connect', () => {
setTimeout(() => {
phoneOutbound({ t: "status", bat: E.getBattery() });
}, ONE_SECOND * 2);
Bangle.buzz();
phoneNewMessage(null, "Phone connected");
});
GB = (evt) => phoneInbound(evt);
startTimers();
setInterval(checkBatteryLevel, ONE_SECOND * 60 * 10);
checkBatteryLevel();
}
// Initialise!
init()
init();

1
apps/nato/changelog.txt Normal file
View File

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

1
apps/nato/nato-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgFCiIABiAGFiINJAAUS///CAgGEgMT//zBoYXFmIiCC40fEooXF+QXJn4lCC5ARDC4oFC//xMAoXDJAQXFBgY9DC4wKCC4p2CPA4XDCQQXEOwXxPA4XBEQJICC4p2BmICCC44KBJAIXEiIJBkMvPAwXCWgYXFAgQMBPAoXCBwUxC4jtDeI4XDJAQXDFYXxHAoXGJAYXDLYPykUieIwXDJAYXDG4IAEPAgXCRgJICPYoAEPAgXDZ4TcDmYXGMAgXDUAZiEPwIABCALEBC5BZC+YQCRwRsEC45ID+S5BCAkBEYJ4DC4hID+IbCIAYjCCIYXGEgMxXoJwEgI3CA4JQDAAwaBmQGDFIQ3CC5UzkSLBdwIIDmYXCWY4jBCAJBCPYQ0EC5bXGkLuDC5QtEAAXzPoZMCmZwB+YFCbYkykQFCVoZMDWALnDQwRjDeoZIDZAgJCWwYeBFATWFC5LuHawgXKdwyJDD4YXIOAMzH4gICmIXKEwQXXkQXFKAKQFC85HNO64XDU44XMX48Sa5zvCmJICA4YXLE4fziIACJ4PyM4gXHCAQwBCwI2GC5JADAApGFC5ERmYWFFwwXHDARJCMgYWFB4MTmYiFLgMjCwMyiIuGE4QABNIyPDBQgA=="))

106
apps/nato/nato.js Normal file
View File

@ -0,0 +1,106 @@
// Teach a user the NATO Phonetic Alphabet + numbers
// Based on the Morse Code app
const FONT_NAME = 'Vector12';
const FONT_SIZE = 80;
const SCREEN_PIXELS = 240;
const UNIT = 100;
const NATO_MAP = {
A: 'ALFA',
B: 'BRAVO',
C: 'CHARLIE',
D: 'DELTA',
E: 'ECHO',
F: 'FOXTROT',
G: 'GOLF',
H: 'HOTEL',
I: 'INDIA',
J: 'JULIETT',
K: 'KILO',
L: 'LIMA',
M: 'MIKE',
N: 'NOVEMBER',
O: 'OSCAR',
P: 'PAPA',
Q: 'QUEBEC',
R: 'ROMEO',
S: 'SIERRA',
T: 'TANGO',
U: 'UNIFORM',
V: 'VICTOR',
W: 'WHISKEY',
X: 'X-RAY',
Y: 'YANKEE',
Z: 'ZULU',
'0': 'ZE-RO',
'1': 'WUN',
'2': 'TOO',
'3': 'TREE',
'4': 'FOW-ER',
'5': 'FIFE',
'6': 'SIX',
'7': 'SEV-EN',
'8': 'AIT',
'9': 'NIN-ER',
};
let INDEX = 0;
let showLetter = true;
const writeText = (txt) => {
g.clear();
g.setFont(FONT_NAME, FONT_SIZE);
var width = g.stringWidth(txt);
// Fit text to screen
var fontFix = FONT_SIZE;
while(width > SCREEN_PIXELS-10){
fontFix--;
g.setFont(FONT_NAME, fontFix);
width = g.stringWidth(txt);
}
g.drawString(txt, (SCREEN_PIXELS / 2) - (width / 2), SCREEN_PIXELS / 2);
};
const writeLetter = () => {
writeText(Object.keys(NATO_MAP)[INDEX]);
};
const writeCode = () => {
writeText(NATO_MAP[Object.keys(NATO_MAP)[INDEX]]);
};
const toggle = () => {
showLetter = !showLetter;
if(showLetter){
writeLetter();
}else {
writeCode();
}
};
// Bootstrapping
g.clear();
g.setFont(FONT_NAME, FONT_SIZE);
g.setColor(0, 1, 0);
g.setFontAlign(-1, 0, 0);
const step = (positive) => () => {
if (positive) {
INDEX = INDEX + 1;
if (INDEX > Object.keys(NATO_MAP).length - 1) INDEX = 0;
} else {
INDEX = INDEX - 1;
if (INDEX < 0) INDEX = Object.keys(NATO_MAP).length - 1;
}
showLetter = true; // for toggle()
writeLetter();
};
writeLetter();
// Press the middle button to see the NATO Phonetic wording
setWatch(toggle, BTN2, { repeat: true });
// Allow user to switch between letters
setWatch(step(true), BTN1, { repeat: true });
setWatch(step(false), BTN3, { repeat: true });

BIN
apps/nato/nato.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

1
apps/numerals/ChangeLog Normal file
View File

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

17
apps/numerals/README.md Normal file
View File

@ -0,0 +1,17 @@
# Numerals Clock
This is a simple big numerals clock.
Settings can be accessed through the app/widget settings menu of the Bangle.js
## Settings available
### color:
* rnd - shows numerals in different color combinations every time the watches wakes
* r/g - red/green
* y/w - yellow/white
* o/c - orange/cyan
* b/y - blue/yellow'ish
### draw mode
* fill - fill numerals
* frame - only shows outline of numerals

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/ABMBzIADyAJIAAkQBoMZBIoXCBIwADyIkIGAIuKGAQkIBJIwEEKQANC/4XWR58RiIHFWpAXFe4QRFcpAXFewQRFcxAXEFwQwGA4QXKiAXDGAgX/C/4X/C/4X/C7uQCwcBBwYXNBwYuEC54wCFwgXPzMRiIHFC54AHC/4XiCAoXRhIHDyK3GAAwOBJA0QG45VGC4YwCD4YwKFwgABcgIfEAwIAHBwgA/AAgA=="))

View File

@ -0,0 +1,93 @@
/**
* Bangle.js Numerals Clock
*
* + Original Author: Raik M. https://github.com/ps-igel
* + Created: April 2020
* + see README.md for details
*/
var numerals = {
0:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[30,21,61,21,69,29,69,61,61,69,30,69,22,61,22,29,30,21]],
1:[[59,1,82,1,90,9,90,82,82,90,73,90,65,82,65,27,59,27,51,19,51,9,59,1]],
2:[[9,1,82,1,90,9,90,47,82,55,21,55,21,64,82,64,90,72,90,82,82,90,9,90,1,82,1,43,9,35,70,35,70,25,9,25,1,17,1,9,9,1]],
3:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,74,9,66,70,66,70,57,9,57,1,49,1,41,9,33,70,33,70,25,9,25,1,17,1,9,9,1]],
4:[[9,1,14,1,22,9,22,34,69,34,69,9,77,1,82,1,90,9,90,82,82,90,78,90,70,82,70,55,9,55,1,47,1,9,9,1]],
5:[[9,1,82,1,90,9,90,17,82,25,21,25,21,35,82,35,90,43,90,82,82,90,9,90,1,82,1,72,9,64,71,64,71,55,9,55,1,47,1,9,9,1]],
6:[[9,1,82,1,90,9,90,14,82,22,22,22,22,36,82,36,90,44,90,82,82,90,9,90,1,82,1,9,9,1],[22,55,69,55,69,69,22,69,22,55]],
7:[[9,1,82,1,90,9,90,15,15,90,9,90,1,82,1,76,54,23,9,23,1,15,1,9,9,1]],
8:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,9,9,1],[22,22,69,22,69,36,22,36,22,22],[22,55,69,55,69,69,22,69,22,55]],
9:[[9,1,82,1,90,9,90,82,82,90,9,90,1,82,1,77,9,69,69,69,69,55,9,55,1,47,1,9,9,1],[22,22,69,22,69,36,22,36,22,22]],
};
var _hCol = ["#ff5555","#ffff00","#FF9901","#2F00FF"];
var _mCol = ["#55ff55","#ffffff","#00EFEF","#FFBF00"];
var _rCol = 0;
var interval = 0;
const REFRESH_RATE = 10E3;
function translate(tx, ty, p) {
return p.map((x, i)=> x+((i%2)?ty:tx));
}
function fill(poly){
return g.fillPoly(poly);
}
function frame(poly){
return g.drawPoly(poly);
}
let settings = require('Storage').readJSON('numerals.json',1);
if (!settings) {
settings = {
color: 0,
drawMode: "fill"
};
}
function drawNum(num,col,x,y,func){
g.setColor(col);
let tx = x*100+35;
let ty = y*100+35;
for (let i=0;i<numerals[num].length;i++){
if (i>0) g.setColor((func==fill)?"#000000":col);
func(translate(tx, ty,numerals[num][i]));
}
}
function draw(drawMode){
let d = new Date();
let h1 = Math.floor(d.getHours()/10);
let h2 = d.getHours()%10;
let m1 = Math.floor(d.getMinutes()/10);
let m2 = d.getMinutes()%10;
g.clearRect(0,24,240,240);
drawNum(h1,_hCol[_rCol],0,0,eval(drawMode));
drawNum(h2,_hCol[_rCol],1,0,eval(drawMode));
drawNum(m1,_mCol[_rCol],0,1,eval(drawMode));
drawNum(m2,_mCol[_rCol],1,1,eval(drawMode));
}
Bangle.setLCDMode();
clearWatch();
setWatch(Bangle.showLauncher, BTN1, {repeat:false,edge:"falling"});
g.clear();
clearInterval();
if (settings.color>0) _rCol=settings.color-1;
interval=setInterval(draw, REFRESH_RATE, settings.drawMode);
draw(settings.drawMode);
Bangle.on('lcdPower', function(on) {
if (on) {
if (settings.color==0) _rCol = Math.floor(Math.random()*_hCol.length);
draw(settings.drawMode);
interval=setInterval(draw, REFRESH_RATE, settings.drawMode);
}else
{
clearInterval(interval);
}
});
Bangle.loadWidgets();
Bangle.drawWidgets();

BIN
apps/numerals/numerals.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,33 @@
(function(back) {
function updateSettings() {
storage.write('numerals.json', numeralsSettings);
};
function resetSettings() {
numeralsSettings = {
color: 0,
drawMode: "fill"
};
updateSettings();
}
let numeralsSettings = storage.readJSON('numerals.json',1);
if (!numeralsSettings) resetSettings();
let dm = ["fill","frame"];
let col = ["rnd","r/g","y/w","o/c","b/y"]
var menu={
"" : { "title":"Numerals"},
"Colors": {
value: 0|numeralsSettings.color,
min:0,max:4,
format: v=>col[v],
onchange: v=> { numeralsSettings.color=v; updateSettings();}
},
"Draw mode": {
value: 0|dm.indexOf(numeralsSettings.drawMode),
min:0,max:1,
format: v=>dm[v],
onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();}
},
"< back": back
};
E.showMenu(menu);
})

View File

@ -12,3 +12,6 @@
0.12: Fix memory leak (#206)
Bring App settings nearer the top
Move LCD Timeout to wakeup menu
0.13: Fix memory leak for App settings
Make capitalization more consistent
Move LCD Brightness menu into more general LCD menu

View File

@ -64,7 +64,7 @@ function showMainMenu() {
const mainmenu = {
'': { 'title': 'Settings' },
'Make Connectable': ()=>makeConnectable(),
'App/widget settings': ()=>showAppSettingsMenu(),
'App/Widget Settings': ()=>showAppSettingsMenu(),
'BLE': {
value: settings.ble,
format: boolFormat,
@ -81,7 +81,7 @@ function showMainMenu() {
updateSettings();
}
},
'Debug info': {
'Debug Info': {
value: settings.log,
format: v => v ? "Show" : "Hide",
onchange: () => {
@ -89,17 +89,6 @@ function showMainMenu() {
updateSettings();
}
},
'LCD Brightness': {
value: settings.brightness,
min: 0.1,
max: 1,
step: 0.1,
onchange: v => {
settings.brightness = v || 1;
updateSettings();
Bangle.setLCDBrightness(settings.brightness);
}
},
'Beep': {
value: 0 | beepV.indexOf(settings.beep),
min: 0, max: 2,
@ -134,7 +123,7 @@ function showMainMenu() {
}
},
'Set Time': ()=>showSetTimeMenu(),
'LCD Wake-Up': ()=>showWakeUpMenu(),
'LCD': ()=>showLCDMenu(),
'Reset Settings': ()=>showResetMenu(),
'Turn Off': ()=>Bangle.off(),
'< Back': ()=>load()
@ -142,10 +131,21 @@ function showMainMenu() {
return E.showMenu(mainmenu);
}
function showWakeUpMenu() {
const wakeUpMenu = {
'': { 'title': 'LCD Wake-Up' },
function showLCDMenu() {
const lcdMenu = {
'': { 'title': 'LCD' },
'< Back': ()=>showMainMenu(),
'LCD Brightness': {
value: settings.brightness,
min: 0.1,
max: 1,
step: 0.1,
onchange: v => {
settings.brightness = v || 1;
updateSettings();
Bangle.setLCDBrightness(settings.brightness);
}
},
'LCD Timeout': {
value: settings.timeout,
min: 0,
@ -157,7 +157,7 @@ function showWakeUpMenu() {
Bangle.setLCDTimeout(settings.timeout);
}
},
'Wake On BTN1': {
'Wake on BTN1': {
value: settings.options.wakeOnBTN1,
format: boolFormat,
onchange: () => {
@ -165,7 +165,7 @@ function showWakeUpMenu() {
updateOptions();
}
},
'Wake On BTN2': {
'Wake on BTN2': {
value: settings.options.wakeOnBTN2,
format: boolFormat,
onchange: () => {
@ -173,7 +173,7 @@ function showWakeUpMenu() {
updateOptions();
}
},
'Wake On BTN3': {
'Wake on BTN3': {
value: settings.options.wakeOnBTN3,
format: boolFormat,
onchange: () => {
@ -197,7 +197,7 @@ function showWakeUpMenu() {
updateOptions();
}
},
'Wake On Twist': {
'Wake on Twist': {
value: settings.options.wakeOnTwist,
format: boolFormat,
onchange: () => {
@ -236,7 +236,7 @@ function showWakeUpMenu() {
}
}
}
return E.showMenu(wakeUpMenu)
return E.showMenu(lcdMenu)
}
function showLocaleMenu() {
@ -450,7 +450,7 @@ function showAppSettings(app) {
}
try {
// pass showAppSettingsMenu as "back" argument
appSettings(showAppSettingsMenu);
appSettings(()=>showAppSettingsMenu());
} catch (e) {
console.log(`${app.name} settings error:`, e)
return showError('Error in settings');

1
apps/snake/ChangeLog Normal file
View File

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

13
apps/snake/README.md Normal file
View File

@ -0,0 +1,13 @@
# Snake
![Screenshot](https://i.ibb.co/XzWrvPL/screenshot.png)
The legentary classic game is now available on Bangle.js!
Eat apples and don't bite your tail.
## Controls
- UP: BTN1
- DOWN: BTN3
- LEFT: BTN4
- RIGHT: BTN5

1
apps/snake/snake-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ADE3m9hsIusrdhGIM3LtU3g0GAgQxlEwIqBmEAgEGF4QwkF4c3F4MxF4dbF8qLDrYHDre74IABF8QwBLoaPDF8wPKF96/jF/4v/F/4vrrc3AIQsnsIAKF94wiFxgv/R//+m4ABrYALBwIpYFwwAQLC4v/F7gXGF91hACovWFqwwUF4VbF7IwUFzSRVF1gwCF9wwZFyoA/AH4A/AH4A/AGg="))

144
apps/snake/snake.js Normal file
View File

@ -0,0 +1,144 @@
const H = g.getWidth();
const W = g.getHeight();
let running = true;
let score = 0;
let d;
// game world
const gridSize = 40;
const tileSize = 6;
let nextX = 0;
let nextY = 0;
// snake
const defaultTailSize = 3;
let tailSize = defaultTailSize;
const snakeTrail = [];
let snakeX = 10;
let snakeY = 10;
// apple
let appleX = Math.floor(Math.random() * gridSize);
let appleY = Math.floor(Math.random() * gridSize);
function gameStart() {
running = true;
score = 0;
}
function gameStop() {
g.clear();
g.setColor("#FFFFFF");
g.setFont("6x8", 2);
g.drawString("GAME OVER!", W / 2, H / 2 - 20);
g.drawString("Tap to Restart", W / 2, H / 2 + 20);
running = false;
tailSize = defaultTailSize;
}
function draw() {
if (!running) {
return;
}
// move snake in next pos
snakeX += nextX;
snakeY += nextY;
// snake over game world?
if (snakeX < 0) {
snakeX = gridSize - 1;
}
if (snakeX > gridSize - 1) {
snakeX = 0;
}
if (snakeY < 0) {
snakeY = gridSize - 1;
}
if (snakeY > gridSize - 1) {
snakeY = 0;
}
//snake bite apple?
if (snakeX === appleX && snakeY === appleY) {
tailSize++;
score++;
appleX = Math.floor(Math.random() * gridSize);
appleY = Math.floor(Math.random() * gridSize);
}
//paint background
g.setColor("#000000");
g.fillRect(0, 0, H, W);
// paint snake
g.setColor("#008000");
for (let i = 0; i < snakeTrail.length; i++) {
g.fillRect(snakeTrail[i].x * tileSize, snakeTrail[i].y * tileSize, snakeTrail[i].x * tileSize + tileSize, snakeTrail[i].y * tileSize + tileSize);
//snake bites it's tail?
if (snakeTrail[i].x === snakeX && snakeTrail[i].y === snakeY && tailSize > defaultTailSize) {
gameStop();
}
}
// paint apple
g.setColor("#FF0000");
g.fillRect(appleX * tileSize, appleY * tileSize, appleX * tileSize + tileSize, appleY * tileSize + tileSize);
// paint score
g.setColor("#FFFFFF");
g.setFont("6x8");
g.setFontAlign(0, 0);
g.drawString("Score:" + score, W / 2, 10);
//set snake trail
snakeTrail.push({ x: snakeX, y: snakeY });
while (snakeTrail.length > tailSize) {
snakeTrail.shift();
}
}
// input
setWatch(() => {// Up
if (d !== 'd') {
nextX = 0;
nextY = -1;
d = 'u';
}
}, BTN1, { repeat: true });
setWatch(() => {// Down
if (d !== 'u') {
nextX = 0;
nextY = 1;
d = 'd';
}
}, BTN3, { repeat: true });
setWatch(() => {// Left
if (d !== 'r') {
nextX = -1;
nextY = 0;
d = 'l';
}
}, BTN4, { repeat: true });
setWatch(() => {// Right
if (d !== 'l') {
nextX = 1;
nextY = 0;
d = 'r';
}
}, BTN5, { repeat: true });
Bangle.on('touch', button => {
if (!running) {
gameStart();
}
});
// render X times per second
var x = 5;
setInterval(draw, 1000 / x);

BIN
apps/snake/snake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -37,7 +37,13 @@ try{
ERROR("apps.json not valid JSON");
}
apps.forEach((app,addIdx) => {
const APP_KEYS = [
'id', 'name', 'shortName', 'version', 'icon', 'description', 'tags', 'type',
'sortorder', 'readme', 'custom', 'interface', 'storage', 'allow_emulator',
];
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate'];
apps.forEach((app,appIdx) => {
if (!app.id) ERROR(`App ${appIdx} has no id`);
//console.log(`Checking ${app.id}...`);
var appDir = APPSDIR+app.id+"/";
@ -105,9 +111,15 @@ apps.forEach((app,addIdx) => {
ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`);
}
}
for (const key in file) {
if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id}'s ${file.name} has unknown key ${key}`);
}
});
//console.log(fileNames);
if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`);
if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`);
if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`);
for (const key in app) {
if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`);
}
});

View File

@ -96,6 +96,7 @@
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
</div>
<div class="panel">
<div class="panel-header">
@ -134,7 +135,8 @@
<h3>Utilities</h3>
<p><button class="btn" id="settime">Set Bangle.js Time</button>
<button class="btn" id="removeall">Remove all Apps</button>
<button class="btn" id="installdefault">Install default apps</button></p>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite">Install favourite apps</button></p>
</div>
</div>

View File

@ -1,6 +1,8 @@
var appJSON = []; // List of apps and info from apps.json
var appsInstalled = []; // list of app JSON
var files = []; // list of files on Bangle
var favourites = []; // list of user favourite app
const FAVOURITE = "favouriteapps.json";
httpGet("apps.json").then(apps=>{
try {
@ -18,7 +20,7 @@ httpGet("apps.json").then(apps=>{
function showChangeLog(appid) {
var app = appNameToApp(appid);
function show(contents) {
showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});;
showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});
}
httpGet(`apps/${appid}/ChangeLog`).
then(show).catch(()=>show("No Change Log available"));
@ -142,6 +144,20 @@ function handleAppInterface(app) {
});
}
function handleAppFavourite(favourite, app){
if (favourite) {
favourites = favourites.concat([app.id]);
} else {
if ([ "boot","setting"].includes(app.id)) {
showToast(app.name + ' is required, can\'t remove it' , 'warning');
}else {
favourites = favourites.filter(e => e != app.id);
}
}
localStorage.setItem("favouriteapps.json", JSON.stringify(favourites));
refreshLibrary();
}
// =========================================== Top Navigation
function showTab(tabname) {
htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => {
@ -156,7 +172,7 @@ function showTab(tabname) {
// =========================================== Library
var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value)
var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value);
var hash = window.location.hash ? window.location.hash.slice(1) : '';
var activeFilter = !!~chips.indexOf(hash) ? hash : '';
@ -165,27 +181,34 @@ var currentSearch = '';
function refreshFilter(){
var filtersContainer = document.querySelector("#librarycontainer .filter-nav");
filtersContainer.querySelector('.active').classList.remove('active');
if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active')
else filtersContainer.querySelector('.chip[filterid]').classList.add('active')
if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active');
else filtersContainer.querySelector('.chip[filterid]').classList.add('active');
}
function refreshLibrary() {
var panelbody = document.querySelector("#librarycontainer .panel-body");
var visibleApps = appJSON;
if (activeFilter) {
visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter));
if ( activeFilter == "favourites" ) {
visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length));
}else{
visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter));
}
}
if (currentSearch) {
visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch));
}
favourites = (localStorage.getItem(FAVOURITE)) === null ? JSON.parse('["boot","launch","setting"]') : JSON.parse(localStorage.getItem("favouriteapps.json"));
panelbody.innerHTML = visibleApps.map((app,idx) => {
var appInstalled = appsInstalled.find(a=>a.id==app.id);
var version = getVersionInfo(app, appInstalled);
var versionInfo = version.text;
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
var readme = `<a href="#" onclick="showReadme('${app.id}')">Read more...</a>`;
var favourite = favourites.find(e => e == app.id);
return `<div class="tile column col-6 col-sm-12 col-xs-12">
<div class="tile-icon">
<figure class="avatar"><img src="apps/${app.icon?`${app.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure><br/>
@ -195,7 +218,8 @@ function refreshLibrary() {
<p class="tile-subtitle">${escapeHtml(app.description)}${app.readme?`<br/>${readme}`:""}</p>
<a href="https://github.com/espruino/BangleApps/tree/master/apps/${app.id}" target="_blank" class="link-github"><img src="img/github-icon-sml.png" alt="See the code on GitHub"/></a>
</div>
<div class="tile-action">
<div class="tile-action">
<button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"&#x2665;":"&#x2661;"}</button>
<button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button>
<button class="btn btn-link btn-action btn-lg ${app.allow_emulator?"":"d-hide"}" appid="${app.id}" title="Try in Emulator"><i class="icon icon-share"></i></button>
<button class="btn btn-link btn-action btn-lg ${version.canUpdate?"":"d-hide"}" appid="${app.id}" title="Update App"><i class="icon icon-refresh"></i></button>
@ -232,7 +256,7 @@ function refreshLibrary() {
// upload
icon.classList.remove("icon-upload");
icon.classList.add("loading");
uploadApp(app)
uploadApp(app);
} else if (icon.classList.contains("icon-menu")) {
// custom HTML update
icon.classList.remove("icon-menu");
@ -250,6 +274,10 @@ function refreshLibrary() {
updateApp(app);
} else if (icon.classList.contains("icon-download")) {
handleAppInterface(app);
} else if ( button.innerText == String.fromCharCode(0x2661)) {
handleAppFavourite(true, app);
} else if ( button.innerText == String.fromCharCode(0x2665) ) {
handleAppFavourite(false, app);
}
});
});
@ -262,17 +290,17 @@ refreshLibrary();
function uploadApp(app) {
return getInstalledApps().then(()=>{
if (appsInstalled.some(i => i.id === app.id)) {
return updateApp(app)
return updateApp(app);
}
Comms.uploadApp(app).then((appJSON) => {
Progress.hide({ sticky: true })
Progress.hide({ sticky: true });
if (appJSON) {
appsInstalled.push(appJSON)
appsInstalled.push(appJSON);
}
showToast(app.name + ' Uploaded!', 'success')
showToast(app.name + ' Uploaded!', 'success');
}).catch(err => {
Progress.hide({ sticky: true })
showToast('Upload failed, ' + err, 'error')
Progress.hide({ sticky: true });
showToast('Upload failed, ' + err, 'error');
}).finally(()=>{
refreshMyApps();
refreshLibrary();
@ -286,8 +314,8 @@ function removeApp(app) {
return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => {
return getInstalledApps().then(()=>{
// a = from appid.info, app = from apps.json
return Comms.removeApp(appsInstalled.find(a => a.id === app.id))
})
return Comms.removeApp(appsInstalled.find(a => a.id === app.id));
});
}).then(()=>{
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
showToast(app.name+" removed successfully","success");
@ -315,13 +343,13 @@ function updateApp(app) {
if (app.custom) return customApp(app);
return getInstalledApps().then(() => {
// a = from appid.info, app = from apps.json
let remove = appsInstalled.find(a => a.id === app.id)
let remove = appsInstalled.find(a => a.id === app.id);
// no need to remove files which will be overwritten anyway
remove.files = remove.files.split(',')
.filter(f => f !== app.id + '.info')
.filter(f => !app.storage.some(s => s.name === f))
.join(',')
return Comms.removeApp(remove)
.join(',');
return Comms.removeApp(remove);
}).then(()=>{
showToast(`Updating ${app.name}...`);
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
@ -397,7 +425,7 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
// check icon to figure out what we should do
if (icon.classList.contains("icon-delete")) removeApp(app);
if (icon.classList.contains("icon-refresh")) updateApp(app);
if (icon.classList.contains("icon-download")) handleAppInterface(app)
if (icon.classList.contains("icon-download")) handleAppInterface(app);
});
});
}
@ -405,7 +433,7 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
let haveInstalledApps = false;
function getInstalledApps(refresh) {
if (haveInstalledApps && !refresh) {
return Promise.resolve(appsInstalled)
return Promise.resolve(appsInstalled);
}
showLoadingIndicator("myappscontainer");
// Get apps and files
@ -453,7 +481,7 @@ filtersContainer.addEventListener('click', ({ target }) => {
activeFilter = target.getAttribute('filterid') || '';
refreshFilter();
refreshLibrary();
window.location.hash = activeFilter
window.location.hash = activeFilter;
});
var librarySearchInput = document.querySelector("#searchform input");
@ -526,7 +554,7 @@ document.getElementById("installdefault").addEventListener("click",event=>{
upload();
}).catch(function() {
Progress.hide({sticky:true});
reject()
reject();
});
}
upload();
@ -541,3 +569,48 @@ document.getElementById("installdefault").addEventListener("click",event=>{
showToast("App Install failed, "+err,"error");
});
});
// Install all favoutrie apps in one go
document.getElementById("installfavourite").addEventListener("click",event=>{
var defaultApps, appCount;
asyncLocalStorage.getItem(FAVOURITE).then(json=>{
defaultApps = JSON.parse(json);
defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) );
if (defaultApps.some(x=>x===undefined))
throw "Not all apps found";
appCount = defaultApps.length;
return showPrompt("Install Defaults","Remove everything and install favourite apps?");
}).then(() => {
return Comms.removeAllApps();
}).then(()=>{
Progress.hide({sticky:true});
appsInstalled = [];
showToast(`Existing apps removed. Installing ${appCount} apps...`);
return new Promise((resolve,reject) => {
function upload() {
var app = defaultApps.shift();
if (app===undefined) return resolve();
Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true});
Comms.uploadApp(app,"skip_reset").then((appJSON) => {
Progress.hide({sticky:true});
if (appJSON) appsInstalled.push(appJSON);
showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`);
upload();
}).catch(function() {
Progress.hide({sticky:true});
reject();
});
}
upload();
});
}).then(()=>{
return Comms.setTime();
}).then(()=>{
showToast("Favourites apps successfully installed!","success");
return getInstalledApps(true);
}).catch(err=>{
Progress.hide({sticky:true});
showToast("App Install failed, "+err,"error");
});
});

View File

@ -86,6 +86,7 @@ function showToast(message, type) {
var style = "toast-primary";
if (type=="success") style = "toast-success";
else if (type=="error") style = "toast-error";
else if (type=="warning") style = "toast-warning";
else if (type!==undefined) console.log("showToast: unknown toast "+type);
var toastcontainer = document.getElementById("toastcontainer");
var msgDiv = htmlElement(`<div class="toast ${style}"></div>`);

View File

@ -67,3 +67,16 @@ function getVersionInfo(appListing, appInstalled) {
canUpdate : canUpdate
}
}
const asyncLocalStorage = {
setItem: function (key, value) {
return Promise.resolve().then(function () {
localStorage.setItem(key, value);
});
},
getItem: function (key) {
return Promise.resolve().then(function () {
return localStorage.getItem(key);
});
}
};