Merge pull request #2 from ps-igel/master

update 20200415
pull/305/head
ps-igel 2020-04-15 21:30:32 +02:00 committed by GitHub
commit c6010c9f7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1641 additions and 126 deletions

View File

@ -6,5 +6,7 @@ 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 * `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 * Added optional `README.md` file for apps
* Remove 2v04 version warning, add links in About to official/developer versions * Remove 2v04 version warning, add links in About to official/developer versions
* Fix issue removing an app that was just installed (Fix #253) * Fix issue removing an app that was just installed (fix #253)
* Add `Favourite` functionality * Add `Favourite` functionality
* Version number now clickable even when you're at the latest version (fix #291)
* Rewrite 'getInstalledApps' to minimize RAM usage

View File

@ -78,7 +78,7 @@
{ "id": "welcome", { "id": "welcome",
"name": "Welcome", "name": "Welcome",
"icon": "app.png", "icon": "app.png",
"version":"0.06", "version":"0.07",
"description": "Appears at first boot and explains how to use Bangle.js", "description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome", "tags": "start,welcome",
"allow_emulator":true, "allow_emulator":true,
@ -86,13 +86,14 @@
{"name":"welcome.boot.js","url":"boot.js"}, {"name":"welcome.boot.js","url":"boot.js"},
{"name":"welcome.app.js","url":"app.js"}, {"name":"welcome.app.js","url":"app.js"},
{"name":"welcome.settings.js","url":"settings.js"}, {"name":"welcome.settings.js","url":"settings.js"},
{"name":"welcome.settings.json","url":"settings-default.json","evaluate":true},
{"name":"welcome.img","url":"app-icon.js","evaluate":true} {"name":"welcome.img","url":"app-icon.js","evaluate":true}
] ]
}, },
{ "id": "gbridge", { "id": "gbridge",
"name": "Gadgetbridge", "name": "Gadgetbridge",
"icon": "app.png", "icon": "app.png",
"version":"0.08", "version":"0.09",
"description": "The default notification handler for Gadgetbridge notifications from Android", "description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget", "tags": "tool,system,android,widget",
"type":"widget", "type":"widget",
@ -119,7 +120,7 @@
{ "id": "setting", { "id": "setting",
"name": "Settings", "name": "Settings",
"icon": "settings.png", "icon": "settings.png",
"version":"0.13", "version":"0.15",
"description": "A menu for setting up Bangle.js", "description": "A menu for setting up Bangle.js",
"tags": "tool,system", "tags": "tool,system",
"storage": [ "storage": [
@ -279,7 +280,7 @@
{ "id": "gpsrec", { "id": "gpsrec",
"name": "GPS Recorder", "name": "GPS Recorder",
"icon": "app.png", "icon": "app.png",
"version":"0.06", "version":"0.07",
"interface": "interface.html", "interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background", "description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget", "tags": "tool,outdoors,gps,widget",
@ -329,7 +330,7 @@
{ "id": "widbat", { "id": "widbat",
"name": "Battery Level Widget", "name": "Battery Level Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.04", "version":"0.05",
"description": "Show the current battery level and charging status in the top right of the clock", "description": "Show the current battery level and charging status in the top right of the clock",
"tags": "widget,battery", "tags": "widget,battery",
"type":"widget", "type":"widget",
@ -341,7 +342,7 @@
"name": "Battery Level Widget (with percentage)", "name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget", "shortName": "Battery Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.08", "version":"0.09",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery", "tags": "widget,battery",
"type":"widget", "type":"widget",
@ -354,7 +355,7 @@
{ "id": "widbt", { "id": "widbt",
"name": "Bluetooth Widget", "name": "Bluetooth Widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.03", "version":"0.04",
"description": "Show the current Bluetooth connection status in the top right of the clock", "description": "Show the current Bluetooth connection status in the top right of the clock",
"tags": "widget,bluetooth", "tags": "widget,bluetooth",
"type":"widget", "type":"widget",
@ -362,6 +363,18 @@
{"name":"widbt.wid.js","url":"widget.js"} {"name":"widbt.wid.js","url":"widget.js"}
] ]
}, },
{ "id": "widram",
"name": "RAM Widget",
"shortName":"RAM Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Display your Bangle's available RAM percentage in a widget",
"tags": "widget",
"type": "widget",
"storage": [
{"name":"widram.wid.js","url":"widget.js"}
]
},
{ "id": "hrm", { "id": "hrm",
"name": "Heart Rate Monitor", "name": "Heart Rate Monitor",
"icon": "heartrate.png", "icon": "heartrate.png",
@ -504,13 +517,14 @@
"id": "ncstart", "id": "ncstart",
"name": "NCEU Startup", "name": "NCEU Startup",
"icon": "start.png", "icon": "start.png",
"version":"0.03", "version":"0.04",
"description": "NodeConfEU 2019 'First Start' Sequence", "description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome", "tags": "start,welcome",
"storage": [ "storage": [
{"name":"ncstart.app.js","url":"start.js"}, {"name":"ncstart.app.js","url":"start.js"},
{"name":"ncstart.boot.js","url":"boot.js"}, {"name":"ncstart.boot.js","url":"boot.js"},
{"name":"ncstart.settings.js","url":"settings.js"}, {"name":"ncstart.settings.js","url":"settings.js"},
{"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true},
{"name":"ncstart.img","url":"start-icon.js","evaluate":true}, {"name":"ncstart.img","url":"start-icon.js","evaluate":true},
{"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true}, {"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
{"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true}, {"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
@ -775,7 +789,7 @@
{ "id": "widclk", { "id": "widclk",
"name": "Digital clock widget", "name": "Digital clock widget",
"icon": "widget.png", "icon": "widget.png",
"version":"0.03", "version":"0.04",
"description": "A simple digital clock widget", "description": "A simple digital clock widget",
"tags": "widget,clock", "tags": "widget,clock",
"type":"widget", "type":"widget",
@ -915,7 +929,7 @@
{ "id": "marioclock", { "id": "marioclock",
"name": "Mario Clock", "name": "Mario Clock",
"icon": "marioclock.png", "icon": "marioclock.png",
"version":"0.09", "version":"0.12",
"description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.",
"tags": "clock,mario,retro", "tags": "clock,mario,retro",
"type": "clock", "type": "clock",
@ -1108,6 +1122,20 @@
{"name":"openstmap.app.js","url":"app.js"}, {"name":"openstmap.app.js","url":"app.js"},
{"name":"openstmap.img","url":"app-icon.js","evaluate":true} {"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", { "id": "tabata",
"name": "Tabata", "name": "Tabata",
@ -1148,9 +1176,9 @@
}, },
{ "id": "batchart", { "id": "batchart",
"name": "Battery Chart", "name": "Battery Chart",
"shortName":"BatChart", "shortName":"Battery Chart",
"icon": "app.png", "icon": "app.png",
"version":"0.03", "version":"0.07",
"description": "A widget and an app for recording and visualizing battery percentage over time.", "description": "A widget and an app for recording and visualizing battery percentage over time.",
"tags": "app,widget,battery,time,record,chart,tool", "tags": "app,widget,battery,time,record,chart,tool",
"storage": [ "storage": [
@ -1159,11 +1187,25 @@
{"name":"batchart.img","url":"app-icon.js","evaluate":true} {"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", { "id": "numerals",
"name": "Numerals Clock", "name": "Numerals Clock",
"shortName": "Numerals Clock", "shortName": "Numerals Clock",
"icon": "numerals.png", "icon": "numerals.png",
"version":"0.01", "version":"0.02",
"description": "A simple big numerals clock", "description": "A simple big numerals clock",
"tags": "numerals,clock", "tags": "numerals,clock",
"type":"clock", "type":"clock",
@ -1186,5 +1228,30 @@
{"name":"bledetect.app.js","url":"bledetect.js"}, {"name":"bledetect.app.js","url":"bledetect.js"},
{"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true} {"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true}
] ]
},
{ "id": "snake",
"name": "Snake",
"shortName":"Snake",
"icon": "snake.png",
"version":"0.02",
"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};
})();

View File

@ -1,3 +1,7 @@
0.01: New app and widget 0.01: New app and widget
0.02: Widget stores data to file (1 dataset/10min) 0.02: Widget stores data to file (1 dataset/10min)
0.03: Rotate log files once a week. 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

View File

@ -1,20 +1,213 @@
// place your const, vars, functions or classes here const GraphXZero = 40;
const GraphYZero = 180;
const GraphY100 = 80;
function renderBatteryChart(){ const GraphMarkerOffset = 5;
g.drawString("t", 215, 175); const MaxValueCount = 144;
g.drawLine(40,190,40,80); const GraphXMax = GraphXZero + MaxValueCount;
g.drawString("%", 39, 70); const GraphLcdY = GraphYZero + 10;
g.drawString("100", 15, 75); const GraphCompassY = GraphYZero + 16;
g.drawLine(35,80,40,80); // const GraphBluetoothY = GraphYZero + 22;
const GraphGpsY = GraphYZero + 28;
const GraphHrmY = GraphYZero + 34;
g.drawString("50", 20,125); const Storage = require("Storage");
g.drawLine(35,130,40,130);
g.drawString("0", 25, 175); function renderCoordinateSystem() {
g.drawLine(35,180,210,180); g.setFont("6x8", 1);
g.drawString("Chart not yet functional", 60, 125); // 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;
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 scaledTemp = Math.floor(((parseFloat(dataInfo[temperatureIndex]) * 100) - 2000)/20) + ((((parseFloat(dataInfo[temperatureIndex]) * 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 (switchables & switchableConsumers.lcd == switchableConsumers.lcd) {
// 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 // special function to handle display switch on
@ -22,13 +215,20 @@ Bangle.on('lcdPower', (on) => {
if (on) { if (on) {
// call your app function here // call your app function here
// If you clear the screen, do Bangle.drawWidgets(); // If you clear the screen, do Bangle.drawWidgets();
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
renderBatteryChart(); renderBatteryChart();
} }
}); });
setWatch(switchOffApp, BTN2, {edge:"rising", debounce:50, repeat:true});
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
// call your app function here // call your app function here
renderHomeIcon();
renderBatteryChart(); renderBatteryChart();

View File

@ -1,5 +1,7 @@
(() => { (() => {
var switchableConsumers = { const Storage = require("Storage");
const switchableConsumers = {
none: 0, none: 0,
lcd: 1, lcd: 1,
compass: 2, compass: 2,
@ -8,59 +10,106 @@
hrm: 16 hrm: 16
}; };
var settings = {};
var batChartFile; // file for battery percentage recording var batChartFile; // file for battery percentage recording
const recordingInterval10Min = 60 * 10 * 1000; const recordingInterval10Min = 60 * 10 * 1000;
const recordingInterval1Min = 60*1000; //For testing const recordingInterval1Min = 60*1000; //For testing
const recordingInterval10S = 10*1000; //For testing const recordingInterval10S = 10*1000; //For testing
var recordingInterval = null; var recordingInterval = null;
var compassEventReceived = false;
var gpsEventReceived = false;
var hrmEventReceived = false;
// draw your widget // draw your widget
function draw() { function draw() {
let x = this.x;
let y = this.y;
g.setColor(0, 1, 0);
g.fillPoly([x+5, y, x+5, y+4, x+1, y+4, x+1, y+20, x+18, y+20, x+18, y+4, x+13, y+4, x+13, y], true);
g.setColor(0,0,0);
g.drawPoly([x+5, y+6, x+8, y+12, x+13, y+12, x+16, y+18], false);
g.reset(); g.reset();
g.drawString("BC", this.x, this.y); }
function onMag(){
compassEventReceived = true;
// Stop handling events when no longer necessarry
Bangle.removeListener("mag", onMag);
}
function onGps() {
gpsEventReceived = true;
Bangle.removeListener("GPS", onGps);
}
function onHrm() {
hrmEventReceived = true;
Bangle.removeListener("HRM", onHrm);
} }
function getEnabledConsumersValue() { function getEnabledConsumersValue() {
var enabledConsumers = switchableConsumers.none; var enabledConsumers = switchableConsumers.none;
Bangle.on('mag', onMag);
Bangle.on('GPS', onGps);
Bangle.on('HRM', onHrm);
// Wait two seconds, that should be enough for each of the events to get raised once
setTimeout(() => {
Bangle.removeAllListeners();
}, 2000);
if (Bangle.isLCDOn()) if (Bangle.isLCDOn())
enabledConsumers = enabledConsumers | switchableConsumers.lcd; enabledConsumers = enabledConsumers | switchableConsumers.lcd;
// Already added in the hope they will be available soon to get more details // Already added in the hope they will be available soon to get more details
// if (Bangle.isCompassOn()) if (compassEventReceived)
// enabledConsumers = enabledConsumers | switchableConsumers.compass; enabledConsumers = enabledConsumers | switchableConsumers.compass;
// if (Bangle.isBluetoothOn()) if (gpsEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.gps;
if (hrmEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.hrm;
//if (Bangle.isBluetoothOn())
// enabledConsumers = enabledConsumers | switchableConsumers.bluetooth; // enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
// if (Bangle.isGpsOn())
// enabledConsumers = enabledConsumers | switchableConsumers.gps;
// if (Bangle.isHrmOn())
// enabledConsumers = enabledConsumers | switchableConsumers.hrm;
return enabledConsumers; // Reset the event registration vars
compassEventReceived = false;
gpsEventReceived = false;
hrmEventReceived = false;
return enabledConsumers.toString();
} }
function logBatteryData() { function logBatteryData() {
const previousWriteLogName = "bcprvday"; const previousWriteLogName = "bcprvday";
const previousWriteDay = require("Storage").read(previousWriteLogName); const previousWriteDay = parseInt(Storage.open(previousWriteLogName, "r").readLine());
const currentWriteDay = new Date().getDay(); const currentWriteDay = new Date().getDay();
const logFileName = "bclog" + currentWriteDay; const logFileName = "bclog" + currentWriteDay;
// Change log target on day change // Change log target on day change
if (previousWriteDay != currentWriteDay) { if (!isNaN(previousWriteDay)
&& previousWriteDay != currentWriteDay) {
//Remove a log file containing data from a week ago //Remove a log file containing data from a week ago
require("Storage").erase(logFileName); Storage.open(logFileName, "r").erase();
require("Storage").write(previousWriteLogName, currentWriteDay); Storage.open(previousWriteLogName, "w").write(parseInt(currentWriteDay));
} }
var bcLogFileA = require("Storage").open(logFileName, "a"); var bcLogFileA = Storage.open(logFileName, "a");
if (bcLogFileA) { if (bcLogFileA) {
console.log([getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")); let logTime = getTime().toFixed(0);
bcLogFileA.write([[getTime().toFixed(0), E.getBattery(), E.getTemperature(), getEnabledConsumersValue()].join(",")].join(",")+"\n"); let logPercent = E.getBattery();
let logTemperature = E.getTemperature();
let logConsumers = getEnabledConsumersValue();
let logString = [logTime, logPercent, logTemperature, logConsumers].join(",");
bcLogFileA.write(logString + "\n");
} }
} }
// Called by the heart app to reload settings and decide what's
function reload() { function reload() {
WIDGETS["batchart"].width = 24; WIDGETS["batchart"].width = 24;
@ -74,6 +123,7 @@
reload(); reload();
Bangle.drawWidgets(); // relayout all widgets Bangle.drawWidgets(); // relayout all widgets
}}; }};
// load settings, set correct widget width // load settings, set correct widget width
reload(); reload();
})() })()

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

@ -7,3 +7,4 @@
0.06: Gadgetbridge App 'Connected' state is no longer toggleable 0.06: Gadgetbridge App 'Connected' state is no longer toggleable
0.07: Move configuration to settings menu 0.07: Move configuration to settings menu
0.08: Don't turn on LCD at start of every song 0.08: Don't turn on LCD at start of every song
0.09: Update Bluetooth connection state automatically

View File

@ -189,8 +189,8 @@
g.flip(); // turns screen on g.flip(); // turns screen on
} }
NRF.on("connected", changedConnectionState); NRF.on("connect", changedConnectionState);
NRF.on("disconnected", changedConnectionState); NRF.on("disconnect", changedConnectionState);
WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw }; WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw };

View File

@ -4,3 +4,4 @@
0.04: Properly Fix GPS time display in gpsrec app 0.04: Properly Fix GPS time display in gpsrec app
0.05: Tweaks for variable size widget system 0.05: Tweaks for variable size widget system
0.06: Ensure widget update itself (fix #118) and change to using icons 0.06: Ensure widget update itself (fix #118) and change to using icons
0.07: Added @jeffmer's awesome track viewer

View File

@ -70,27 +70,65 @@ function viewTracks() {
return E.showMenu(menu); return E.showMenu(menu);
} }
function getTrackInfo(fn) {
var filename = getFN(fn);
var minLat = 90;
var maxLat = -90;
var minLong = 180;
var maxLong = -180;
var starttime, duration=0;
var f = require("Storage").open(filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
var nl = 0, c, n;
if (l!==undefined) {
c = l.split(",");
starttime = parseInt(c[0]);
}
// pushed this loop together to try and bump loading speed a little
while(l!==undefined) {
++nl;c=l.split(",");
n = parseFloat(c[1]);if(n>maxLat)maxLat=n;if(n<minLat)minLat=n;
n = parseFloat(c[2]);if(n>maxLong)maxLong=n;if(n<minLong)minLong=n;
l = f.readLine(f);
}
if (c) duration = parseInt(c[0]) - starttime;
var lfactor = Math.cos(minLat*Math.PI/180);
var ylen = (maxLat-minLat);
var xlen = (maxLong-minLong)* lfactor;
var scale = xlen>ylen ? 200/xlen : 200/ylen;
return {
fn : fn,
filename : filename,
time : new Date(starttime),
records : nl,
minLat : minLat, maxLat : maxLat,
minLong : minLong, maxLong : maxLong,
lfactor : lfactor,
scale : scale,
duration : Math.round(duration/1000)
};
}
function asTime(v){
var mins = Math.floor(v/60);
var secs = v-mins*60;
return ""+mins.toString()+"m "+secs.toString()+"s";
}
function viewTrack(n) { function viewTrack(n) {
E.showMessage("Loading...","GPS Track "+n);
var info = getTrackInfo(n);
const menu = { const menu = {
'': { 'title': 'GPS Track '+n } '': { 'title': 'GPS Track '+n }
}; };
var trackCount = 0; if (info.time)
var trackTime; menu[info.time.toISOString().substr(0,16).replace("T"," ")] = function(){};
var f = require("Storage").open(getFN(n),"r"); menu["Duration"] = { value : asTime(info.duration)};
var l = f.readLine(); menu["Records"] = { value : ""+info.records };
if (l!==undefined) { menu['Plot'] = function() {
var c = l.split(","); plotTrack(info);
trackTime = new Date(parseInt(c[0])); };
}
while (l!==undefined) {
trackCount++;
// TODO: min/max/length of track?
l = f.readLine();
}
if (trackTime)
menu[" "+trackTime.toISOString().substr(0,16).replace("T"," ")] = function(){};
menu[trackCount+" records"] = function(){};
// TODO: option to draw it? Just scan through, project using min/max
menu['Erase'] = function() { menu['Erase'] = function() {
E.showPrompt("Delete Track?").then(function(v) { E.showPrompt("Delete Track?").then(function(v) {
if (v) { if (v) {
@ -107,4 +145,80 @@ function viewTrack(n) {
return E.showMenu(menu); return E.showMenu(menu);
} }
function plotTrack(info) {
function xcoord(long){
return 30 + Math.round((long-info.minLong)*info.lfactor*info.scale);
}
function ycoord(lat){
return 210 - Math.round((lat - info.minLat)*info.scale);
}
function radians(a) {
return a*Math.PI/180;
}
function distance(lat1,long1,lat2,long2){
var x = radians(long1-long2) * Math.cos(radians((lat1+lat2)/2));
var y = radians(lat2-lat1);
return Math.sqrt(x*x + y*y) * 6371000;
}
E.showMenu(); // remove menu
g.setColor(1,0.5,0.5);
g.setFont("Vector",16);
g.fillRect(9,80,11,120);
g.fillPoly([9,60,19,80,0,80]);
g.setColor(1,1,1);
g.drawString("N",2,40);
g.drawString("Track"+info.fn.toString()+" - Loading",10,220);
g.setColor(0,0,0);
g.fillRect(0,220,239,239);
g.setColor(1,1,1);
g.drawString(asTime(info.duration),10,220);
var f = require("Storage").open(info.filename,"r");
if (f===undefined) return;
var l = f.readLine(f);
var ox=0;
var oy=0;
var olat,olong,dist=0;
var first = true;
var i=0;
while(l!==undefined) {
var c = l.split(",");
var lat = parseFloat(c[1]);
var long = parseFloat(c[2]);
var x = xcoord(long);
var y = ycoord(lat);
if (first) {
g.moveTo(x,y);
g.setColor(0,1,0);
g.fillCircle(x,y,5);
g.setColor(1,1,1);
first = false;
} else if (x!=ox || y!=oy) {
g.lineTo(x,y);
}
if (!first) {
var d = distance(olat,olong,lat,long);
if (!isNaN(d)) dist+=d;
}
olat = lat;
olong = long;
ox = x;
oy = y;
l = f.readLine(f);
}
g.setColor(1,0,0);
g.fillCircle(ox,oy,5);
g.setColor(1,1,1);
g.drawString(require("locale").distance(dist),120,220);
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
g.drawString("Back",230,200);
setWatch(function() {
viewTrack(info.fn);
}, BTN3);
}
showMainMenu(); showMainMenu();

View File

@ -7,3 +7,6 @@
0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode 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.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 ## Features
* Multiple characters - swipe the screen right to change the character between `Mario`, `Toad`, and `Daisy` * 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 * Smooth animation
* Awesome 8-bit style grey-scale graphics * Awesome 8-bit style grey-scale graphics
* Mario jumps to change the time, every minute * Mario jumps to change the time, every minute

View File

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

View File

@ -2,3 +2,6 @@
Renamed as nodeconf-specific Renamed as nodeconf-specific
0.03: Move configuration into App/widget settings 0.03: Move configuration into App/widget settings
Move loader into welcome.boot.js Move loader into welcome.boot.js
0.04: Run again when updated
Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Use BTN2 for settings menu like other clocks

View File

@ -70,7 +70,7 @@ function draw(drawMode){
Bangle.setLCDMode(); Bangle.setLCDMode();
clearWatch(); clearWatch();
setWatch(Bangle.showLauncher, BTN1, {repeat:false,edge:"falling"}); setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
g.clear(); g.clear();
clearInterval(); clearInterval();

View File

@ -12,4 +12,8 @@
0.12: Fix memory leak (#206) 0.12: Fix memory leak (#206)
Bring App settings nearer the top Bring App settings nearer the top
Move LCD Timeout to wakeup menu Move LCD Timeout to wakeup menu
0.13: Move LCD Brightness menu into more general LCD menu 0.13: Fix memory leak for App settings
Make capitalization more consistent
Move LCD Brightness menu into more general LCD menu
0.14: Reduce memory usage when running app settings page
0.15: Reduce memory usage when running default clock chooser (#294)

View File

@ -64,7 +64,7 @@ function showMainMenu() {
const mainmenu = { const mainmenu = {
'': { 'title': 'Settings' }, '': { 'title': 'Settings' },
'Make Connectable': ()=>makeConnectable(), 'Make Connectable': ()=>makeConnectable(),
'App/widget settings': ()=>showAppSettingsMenu(), 'App/Widget Settings': ()=>showAppSettingsMenu(),
'BLE': { 'BLE': {
value: settings.ble, value: settings.ble,
format: boolFormat, format: boolFormat,
@ -81,7 +81,7 @@ function showMainMenu() {
updateSettings(); updateSettings();
} }
}, },
'Debug info': { 'Debug Info': {
value: settings.log, value: settings.log,
format: v => v ? "Show" : "Hide", format: v => v ? "Show" : "Hide",
onchange: () => { onchange: () => {
@ -296,10 +296,10 @@ function makeConnectable() {
}); });
} }
function showClockMenu() { function showClockMenu() {
var clockApps = require("Storage").list(/\.info$/).map(app => { var clockApps = require("Storage").list(/\.info$/)
try { return require("Storage").readJSON(app); } .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined})
catch (e) { } .filter(app => app) // filter out any undefined apps
}).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); .sort((a, b) => a.sortorder - b.sortorder);
const clockMenu = { const clockMenu = {
'': { '': {
'title': 'Select Clock', 'title': 'Select Clock',
@ -325,8 +325,6 @@ function showClockMenu() {
return E.showMenu(clockMenu); return E.showMenu(clockMenu);
} }
function showSetTimeMenu() { function showSetTimeMenu() {
d = new Date(); d = new Date();
const timemenu = { const timemenu = {
@ -419,8 +417,8 @@ function showAppSettingsMenu() {
'< Back': ()=>showMainMenu(), '< Back': ()=>showMainMenu(),
} }
const apps = storage.list(/\.info$/) const apps = storage.list(/\.info$/)
.map(app => storage.readJSON(app, 1)) .map(app => {var a=storage.readJSON(app, 1);return (a&&a.settings)?a:undefined})
.filter(app => app && app.settings) .filter(app => app) // filter out any undefined apps
.sort((a, b) => a.sortorder - b.sortorder) .sort((a, b) => a.sortorder - b.sortorder)
if (apps.length === 0) { if (apps.length === 0) {
appmenu['No app has settings'] = () => { }; appmenu['No app has settings'] = () => { };
@ -450,7 +448,7 @@ function showAppSettings(app) {
} }
try { try {
// pass showAppSettingsMenu as "back" argument // pass showAppSettingsMenu as "back" argument
appSettings(showAppSettingsMenu); appSettings(()=>showAppSettingsMenu());
} catch (e) { } catch (e) {
console.log(`${app.name} settings error:`, e) console.log(`${app.name} settings error:`, e)
return showError('Error in settings'); return showError('Error in settings');

2
apps/snake/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Performance and graphic improvements, game pause, beep and buzz

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

@ -0,0 +1,14 @@
# Snake
![Screenshot](https://i.imgur.com/bXQjxhB.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
- PAUSE: BTN2

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

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

@ -0,0 +1,155 @@
Bangle.setLCDMode("120x120");
const H = g.getWidth();
const W = g.getHeight();
let running = true;
let score = 0;
let d;
const gridSize = 20;
const tileSize = 6;
let nextX = 0;
let nextY = 0;
const defaultTailSize = 3;
let tailSize = defaultTailSize;
const snakeTrail = [];
const snake = { x: 10, y: 10 };
const apple = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
function drawBackground(){
g.setColor("#000000");
g.fillRect(0, 0, H, W);
}
function drawApple(){
g.setColor("#FF0000");
g.fillCircle((apple.x * tileSize) + tileSize/2, (apple.y * tileSize) + tileSize/2, tileSize/2);
}
function drawSnake(){
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 === snake.x && snakeTrail[i].y === snake.y && tailSize > defaultTailSize) {
Bangle.buzz(1000);
gameOver();
}
}
}
function drawScore(){
g.setColor("#FFFFFF");
g.setFont("6x8");
g.setFontAlign(0, 0);
g.drawString("Score:" + score, W / 2, 10);
}
function gameStart() {
running = true;
score = 0;
}
function gameOver() {
g.clear();
g.setColor("#FFFFFF");
g.setFont("6x8");
g.drawString("GAME OVER!", W / 2, H / 2 - 10);
g.drawString("Tap to Restart", W / 2, H / 2 + 10);
running = false;
tailSize = defaultTailSize;
}
function draw() {
if (!running) {
return;
}
g.clear();
// move snake in next pos
snake.x += nextX;
snake.y += nextY;
// snake over game world
if (snake.x < 0) {
snake.x = gridSize - 1;
}
if (snake.x > gridSize - 1) {
snake.x = 0;
}
if (snake.y < 0) {
snake.y = gridSize - 1;
}
if (snake.y > gridSize - 1) {
snake.y = 0;
}
//snake bite apple
if (snake.x === apple.x && snake.y === apple.y) {
Bangle.beep(20);
tailSize++;
score++;
apple.x = Math.floor(Math.random() * gridSize);
apple.y = Math.floor(Math.random() * gridSize);
drawApple();
}
drawBackground();
drawApple();
drawSnake();
drawScore();
//set snake trail
snakeTrail.push({ x: snake.x, y: snake.y });
while (snakeTrail.length > tailSize) {
snakeTrail.shift();
}
g.flip();
}
// 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 });
setWatch(() => {// Pause
running = !running;
}, BTN2, { repeat: true });
Bangle.on('touch', button => {
if (!running) {
gameStart();
}
});
// render X times per second
const 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

@ -4,3 +4,6 @@
0.04: Fix regression after tweaks to Storage.readJSON 0.04: Fix regression after tweaks to Storage.readJSON
0.05: Move configuration into App/widget settings 0.05: Move configuration into App/widget settings
0.06: Move loader into welcome.boot.js 0.06: Move loader into welcome.boot.js
0.07: Run again when updated
Don't run again when settings app is updated (or absent)
Add "Run Now" option to settings

View File

@ -288,6 +288,13 @@ setWatch(()=>{
}, BTN2, {repeat:true,edge:"rising"}); }, BTN2, {repeat:true,edge:"rising"});
setWatch(()=>move(-1), BTN1, {repeat:true}); setWatch(()=>move(-1), BTN1, {repeat:true});
(function migrateSettings(){
let global_settings = require('Storage').readJSON('setting.json', 1)
if (global_settings) {
delete global_settings.welcomed
require('Storage').write('setting.json', global_settings)
}
})()
Bangle.setLCDTimeout(0); Bangle.setLCDTimeout(0);
Bangle.setLCDPower(1); Bangle.setLCDPower(1);

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
0.02: Now refresh battery monitor every minute if LCD on 0.02: Now refresh battery monitor every minute if LCD on
0.03: Tweaks for variable size widget system 0.03: Tweaks for variable size widget system
0.04: Ensure redrawing works with variable size widget system 0.04: Ensure redrawing works with variable size widget system
0.05: Fix regression stopping correct widget updates

View File

@ -30,7 +30,7 @@ Bangle.on('lcdPower', function(on) {
WIDGETS["bat"].draw(); WIDGETS["bat"].draw();
// refresh once a minute if LCD on // refresh once a minute if LCD on
if (!batteryInterval) if (!batteryInterval)
batteryInterval = setInterval(draw, 60000); batteryInterval = setInterval(()=>WIDGETS["bat"].draw(), 60000);
} else { } else {
if (batteryInterval) { if (batteryInterval) {
clearInterval(batteryInterval); clearInterval(batteryInterval);

View File

@ -5,3 +5,4 @@
0.06: Show battery percentage as text 0.06: Show battery percentage as text
0.07: Add settings: percentage/color/charger icon 0.07: Add settings: percentage/color/charger icon
0.08: Draw percentage as inverted on monochrome battery 0.08: Draw percentage as inverted on monochrome battery
0.09: Fix regression stopping correct widget updates

View File

@ -110,7 +110,7 @@ Bangle.on('lcdPower', function(on) {
WIDGETS["batpc"].draw(); WIDGETS["batpc"].draw();
// refresh once a minute if LCD on // refresh once a minute if LCD on
if (!batteryInterval) if (!batteryInterval)
batteryInterval = setInterval(draw, 60000); batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000);
} else { } else {
if (batteryInterval) { if (batteryInterval) {
clearInterval(batteryInterval); clearInterval(batteryInterval);

View File

@ -1,2 +1,3 @@
0.02: Tweaks for variable size widget system 0.02: Tweaks for variable size widget system
0.03: Ensure redrawing works with variable size widget system 0.03: Ensure redrawing works with variable size widget system
0.04: Fix automatic update of Bluetooth connection status

View File

@ -13,7 +13,7 @@ function changed() {
WIDGETS["bluetooth"].draw(); WIDGETS["bluetooth"].draw();
g.flip();// turns screen on g.flip();// turns screen on
} }
NRF.on('connected',changed); NRF.on('connect',changed);
NRF.on('disconnected',changed); NRF.on('disconnect',changed);
WIDGETS["bluetooth"]={area:"tr",width:24,draw:draw}; WIDGETS["bluetooth"]={area:"tr",width:24,draw:draw};
})() })()

View File

@ -1,2 +1,3 @@
0.02: Now refresh battery monitor every minute if LCD on 0.02: Now refresh battery monitor every minute if LCD on
0.03: Ensure redrawing works with variable size widget system 0.03: Ensure redrawing works with variable size widget system
0.04: Fix regression stopping correct widget updates

View File

@ -14,7 +14,7 @@
} }
} }
function startTimers(){ function startTimers(){
intervalRef = setInterval(draw, 60*1000); intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000);
WIDGETS["wdclk"].draw(); WIDGETS["wdclk"].draw();
} }
Bangle.on('lcdPower', (on) => { Bangle.on('lcdPower', (on) => {
@ -23,5 +23,5 @@
}); });
WIDGETS["wdclk"]={area:"tr",width:width,draw:draw}; WIDGETS["wdclk"]={area:"tr",width:width,draw:draw};
if (Bangle.isLCDOn) intervalRef = setInterval(draw, 60*1000); if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdclk"].draw(), 60*1000);
})() })()

1
apps/widram/ChangeLog Normal file
View File

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

23
apps/widram/widget.js Normal file
View File

@ -0,0 +1,23 @@
(() => {
function draw() {
g.reset();
var m = process.memory();
var pc = Math.round(m.usage*100/m.total);
g.drawImage(atob("BwgBqgP////AVQ=="), this.x+(24-7)/2, this.y+4);
g.setColor(pc>70 ? "#ff0000" : (pc>50 ? "#ffff00" : "#ffffff"));
g.setFont("6x8").setFontAlign(0,0).drawString(pc+"%", this.x+12, this.y+20, true/*solid*/);
}
var ramInterval;
Bangle.on('lcdPower', function(on) {
if (on) {
WIDGETS["ram"].draw();
if (!ramInterval) ramInterval = setInterval(()=>WIDGETS["ram"].draw(), 10000);
} else {
if (ramInterval) {
clearInterval(ramInterval);
ramInterval = undefined;
}
}
});
WIDGETS["ram"]={area:"tl",width: 24,draw:draw};
})()

BIN
apps/widram/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

View File

@ -37,7 +37,13 @@ try{
ERROR("apps.json not valid JSON"); 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`); if (!app.id) ERROR(`App ${appIdx} has no id`);
//console.log(`Checking ${app.id}...`); //console.log(`Checking ${app.id}...`);
var appDir = APPSDIR+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`); 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); //console.log(fileNames);
if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`);
if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`);
if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); 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

@ -40,7 +40,7 @@ uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `s
currentBytes += f.content.length; currentBytes += f.content.length;
// Chould check CRC here if needed instead of returning 'OK'... // Chould check CRC here if needed instead of returning 'OK'...
// E.CRC32(require("Storage").read(${JSON.stringify(app.name)})) // E.CRC32(require("Storage").read(${JSON.stringify(app.name)}))
Puck.write(`\x10${f.cmd};Bluetooth.println("OK")\n`,(result) => { Puck.write(`${f.cmd};Bluetooth.println("OK")\n`,(result) => {
if (!result || result.trim()!="OK") { if (!result || result.trim()!="OK") {
Progress.hide({sticky:true}); Progress.hide({sticky:true});
return reject("Unexpected response "+(result||"")); return reject("Unexpected response "+(result||""));
@ -75,12 +75,21 @@ getInstalledApps : () => {
Progress.hide({sticky:true}); Progress.hide({sticky:true});
return reject(""); return reject("");
} }
Puck.eval('require("Storage").list(/\.info$/).map(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);return j})', (appList,err) => { Puck.write('\x10Bluetooth.print("[");require("Storage").list(/\.info$/).forEach(f=>{var j=require("Storage").readJSON(f,1)||{};j.id=f.slice(0,-5);Bluetooth.print(JSON.stringify(j)+",")});Bluetooth.println("0]")\n', (appList,err) => {
Progress.hide({sticky:true}); Progress.hide({sticky:true});
try {
appList = JSON.parse(appList);
// remove last element since we added a final '0'
// to make things easy on the Bangle.js side
appList = appList.slice(0,-1);
} catch (e) {
appList = null;
err = e.toString();
}
if (appList===null) return reject(err || ""); if (appList===null) return reject(err || "");
console.log("getInstalledApps", appList); console.log("getInstalledApps", appList);
resolve(appList); resolve(appList);
}); }, true /* callback on newline */);
}); });
}); });
}, },

View File

@ -56,7 +56,7 @@ function getVersionInfo(appListing, appInstalled) {
if (appListing.version) if (appListing.version)
versionText = clicky("v"+appListing.version); versionText = clicky("v"+appListing.version);
} else { } else {
versionText = (appInstalled.version ? ("v"+appInstalled.version) : "Unknown version"); versionText = (appInstalled.version ? (clicky("v"+appInstalled.version)) : "Unknown version");
if (appListing.version != appInstalled.version) { if (appListing.version != appInstalled.version) {
if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version); if (appListing.version) versionText += ", latest "+clicky("v"+appListing.version);
canUpdate = true; canUpdate = true;