Merge pull request #1 from bengwalker/patch-1

Patch 1
pull/419/head
bengwalker 2020-04-28 09:27:33 +02:00 committed by GitHub
commit b2905a7a12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 4117 additions and 523 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.htaccess
node_modules
package-lock.json
.DS_Store

215
apps.json
View File

@ -53,7 +53,7 @@
{ "id": "about",
"name": "About",
"icon": "app.png",
"version":"0.04",
"version":"0.05",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"tags": "tool,system",
"allow_emulator":true,
@ -78,7 +78,7 @@
{ "id": "welcome",
"name": "Welcome",
"icon": "app.png",
"version":"0.07",
"version":"0.08",
"description": "Appears at first boot and explains how to use Bangle.js",
"tags": "start,welcome",
"allow_emulator":true,
@ -86,8 +86,10 @@
{"name":"welcome.boot.js","url":"boot.js"},
{"name":"welcome.app.js","url":"app.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}
],
"data": [
{"name":"welcome.json"}
]
},
{ "id": "gbridge",
@ -106,7 +108,7 @@
{ "id": "mclock",
"name": "Morphing Clock",
"icon": "clock-morphing.png",
"version":"0.03",
"version":"0.04",
"description": "7 segment clock that morphs between minutes and hours",
"tags": "clock",
"type":"clock",
@ -120,13 +122,12 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
"version":"0.17",
"version":"0.18",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"storage": [
{"name":"setting.app.js","url":"settings.js"},
{"name":"setting.boot.js","url":"boot.js"},
{"name":"setting.json","url":"settings-default.json","evaluate":true},
{"name":"setting.img","url":"settings-icon.js","evaluate":true}
],
"sortorder" : -2
@ -135,16 +136,18 @@
"name": "Default Alarm",
"shortName":"Alarms",
"icon": "app.png",
"version":"0.06",
"version":"0.07",
"description": "Set and respond to alarms",
"tags": "tool,alarm,widget",
"storage": [
{"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.boot.js","url":"boot.js"},
{"name":"alarm.js","url":"alarm.js"},
{"name":"alarm.json","content":"[]"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"}
],
"data": [
{"name":"alarm.json"}
]
},
{ "id": "wclock",
@ -160,6 +163,19 @@
{"name":"wclock.img","url":"clock-word-icon.js","evaluate":true}
]
},
{ "id": "impwclock",
"name": "Imprecise Word Clock",
"icon": "clock-impword.png",
"version":"0.01",
"description": "Imprecise word clock for vacations, weekends, and those who never need accurate time.",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"impwclock.app.js","url":"clock-impword.js"},
{"name":"impwclock.img","url":"clock-impword-icon.js","evaluate":true}
]
},
{ "id": "aclock",
"name": "Analog Clock",
"icon": "clock-analog.png",
@ -280,15 +296,18 @@
{ "id": "gpsrec",
"name": "GPS Recorder",
"icon": "app.png",
"version":"0.07",
"version":"0.08",
"interface": "interface.html",
"description": "Application that allows you to record a GPS track. Can run in background",
"tags": "tool,outdoors,gps,widget",
"storage": [
{"name":"gpsrec.app.js","url":"app.js"},
{"name":"gpsrec.json","url":"app-settings.json","evaluate":true},
{"name":"gpsrec.img","url":"app-icon.js","evaluate":true},
{"name":"gpsrec.wid.js","url":"widget.js"}
],
"data": [
{"name":"gpsrec.json"},
{"wildcard":".gpsrc?","storageFile": true}
]
},
{ "id": "gpsnav",
@ -297,6 +316,7 @@
"version":"0.01",
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"storage": [
{"name":"gpsnav.app.js","url":"app.js"},
{"name":"waypoints.json","url":"waypoints.json","evaluate":false},
@ -306,15 +326,18 @@
{ "id": "heart",
"name": "Heart Rate Recorder",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"interface": "interface.html",
"description": "Application that allows you to record your heart rate. Can run in background",
"tags": "tool,health,widget",
"storage": [
{"name":"heart.app.js","url":"app.js"},
{"name":"heart.json","url":"app-settings.json","evaluate":true},
{"name":"heart.img","url":"app-icon.js","evaluate":true},
{"name":"heart.wid.js","url":"widget.js"}
],
"data": [
{"name":"heart.json"},
{"wildcard":".heart?","storageFile": true}
]
},
{ "id": "slevel",
@ -339,6 +362,23 @@
{"name":"files.img","url":"files-icon.js","evaluate":true}
]
},
{ "id": "weather",
"name": "Weather",
"icon": "icon.png",
"version":"0.01",
"description": "Show Gadgetbridge weather report",
"readme": "readme.md",
"tags": "widget,outdoors",
"storage": [
{"name":"weather.app.js","url":"app.js"},
{"name":"weather.wid.js","url":"widget.js"},
{"name":"weather","url":"lib.js"},
{"name":"weather.img","url":"icon.js","evaluate":true}
],
"data": [
{"name": "weather.json"}
]
},
{ "id": "widbat",
"name": "Battery Level Widget",
"icon": "widget.png",
@ -354,14 +394,16 @@
"name": "Battery Level Widget (with percentage)",
"shortName": "Battery Widget",
"icon": "widget.png",
"version":"0.09",
"version":"0.11",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery",
"type":"widget",
"storage": [
{"name":"widbatpc.wid.js","url":"widget.js"},
{"name":"widbatpc.settings.js","url":"settings.js"},
{"name":"widbatpc.settings.json","content": "{}"}
{"name":"widbatpc.settings.js","url":"settings.js"}
],
"data": [
{"name":"widbatpc.json"}
]
},
{ "id": "widbt",
@ -423,7 +465,7 @@
{ "id": "swatch",
"name": "Stopwatch",
"icon": "stopwatch.png",
"version":"0.06",
"version":"0.07",
"interface": "interface.html",
"description": "Simple stopwatch with Lap Time logging to a JSON file",
"tags": "health",
@ -529,20 +571,22 @@
"id": "ncstart",
"name": "NCEU Startup",
"icon": "start.png",
"version":"0.04",
"version":"0.05",
"description": "NodeConfEU 2019 'First Start' Sequence",
"tags": "start,welcome",
"storage": [
{"name":"ncstart.app.js","url":"start.js"},
{"name":"ncstart.boot.js","url":"boot.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":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
{"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
{"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true},
{"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true},
{"name":"nc-tf.img","url":"start-tf.js","evaluate":true}
],
"data": [
{"name":"ncstart.json"}
]
},
{ "id": "ncfrun",
@ -890,8 +934,8 @@
"name": "Torch",
"shortName":"Torch",
"icon": "app.png",
"version":"0.01",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN3 four times in quick succession to start when in normal clock mode",
"version":"0.02",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets",
"tags": "tool,torch",
"storage": [
{"name":"torch.app.js","url":"app.js"},
@ -981,7 +1025,7 @@
{ "id": "barclock",
"name": "Bar Clock",
"icon": "clock-bar.png",
"version":"0.04",
"version":"0.05",
"description": "A simple digital clock showing seconds as a bar",
"tags": "clock",
"type":"clock",
@ -1061,14 +1105,18 @@
},
{ "id": "toucher",
"name": "Touch Launcher",
"shortName":"Menu",
"shortName":"Toucher",
"icon": "app.png",
"version":"0.06",
"description": "Touch enable left to right launcher.",
"tags": "tool,system,launcher",
"type":"launch",
"data": [
{"name":"toucher.json"}
],
"storage": [
{"name":"toucher.app.js","url":"app.js"}
{"name":"toucher.app.js","url":"app.js"},
{"name":"toucher.settings.js","url":"settings.js"}
],
"sortorder" : -10
},
@ -1113,7 +1161,7 @@
{ "id": "minionclk",
"name": "Minion clock",
"icon": "minionclk.png",
"version": "0.01",
"version": "0.02",
"description": "Minion themed clock.",
"tags": "clock,minion",
"type": "clock",
@ -1140,22 +1188,22 @@
"name": "Active Pedometer",
"shortName":"Active Pedometer",
"icon": "app.png",
"version":"0.02",
"description": "Pedometer that filters out arm movement and displays a step goal progress.",
"version":"0.04",
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
"tags": "outdoors,widget",
"type":"widget",
"readme": "README.md",
"storage": [
{"name":"activepedom.wid.js","url":"widget.js"},
{"name":"activepedom.settings.js","url":"settings.js"},
{"name":"activepedom.img","url":"app-icon.js","evaluate":true}
{"name":"activepedom.img","url":"app-icon.js","evaluate":true},
{"name":"activepedom.app.js","url":"app.js"}
]
},
{ "id": "chronowid",
"name": "Chrono Widget",
"shortName":"Chrono Widget",
"icon": "app.png",
"version":"0.02",
"version":"0.03",
"description": "Chronometer (timer) which runs as widget.",
"tags": "tools,widget",
"readme": "README.md",
@ -1234,7 +1282,7 @@
"name": "Numerals Clock",
"shortName": "Numerals Clock",
"icon": "numerals.png",
"version":"0.03",
"version":"0.04",
"description": "A simple big numerals clock",
"tags": "numerals,clock",
"type":"clock",
@ -1242,8 +1290,10 @@
"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"},
{"name":"numerals.json","url":"numerals-default.json","evaluate":true}
{"name":"numerals.settings.js","url":"numerals.settings.js"}
],
"data":[
{"name":"numerals.json"}
]
},
{ "id": "bledetect",
@ -1306,11 +1356,55 @@
}
]
},
{
"id": "buffgym",
"name": "BuffGym",
"icon": "buffgym.png",
"version":"0.02",
"description": "BuffGym is the famous 5x5 workout program for the BangleJS",
"tags": "tool,outdoors,gym,exercise",
"type": "app",
"interface": "buffgym.html",
"allow_emulator": false,
"readme": "README.md",
"storage": [
{"name":"buffgym.app.js", "url": "buffgym.app.js"},
{"name":"buffgym-set.js","url":"buffgym-set.js"},
{"name":"buffgym-exercise.js","url":"buffgym-exercise.js"},
{"name":"buffgym-workout.js","url":"buffgym-workout.js"},
{"name":"buffgym-workout-a.json","url":"buffgym-workout-a.json"},
{"name":"buffgym-workout-b.json","url":"buffgym-workout-b.json"},
{"name":"buffgym-workout-index.json","url":"buffgym-workout-index.json"},
{"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true}
]
},
{
"id": "banglerun",
"name": "BangleRun",
"shortName": "BangleRun",
"icon": "banglerun.png",
"version": "0.01",
"description": "An app for running sessions.",
"tags": "run,running,fitness,outdoors",
"allow_emulator": false,
"storage": [
{
"name": "banglerun.app.js",
"url": "app.js"
},
{
"name": "banglerun.img",
"url": "app-icon.js",
"evaluate": true
}
]
},
{
"id": "metronome",
"name": "Metronome",
"icon": "metronome_icon.png",
"version": "0.03",
"readme": "README.md",
"description": "Makes the watch blinking and vibrating with a given rate",
"tags": "tool",
"allow_emulator": true,
@ -1351,7 +1445,7 @@
{"name":"hidcam.img","url":"app-icon.js","evaluate":true}
]
},
{
{
"id": "rclock",
"name": "Round clock with seconds, minutes and date",
"shortName":"Round Clock",
@ -1364,5 +1458,60 @@
{"name":"rclock.app.js","url":"rclock.app.js"},
{"name":"rclock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "hamloc",
"name": "QTH Locator / Maidenhead Locator System",
"shortName": "QTH Locator",
"icon": "app.png",
"version":"0.01",
"description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators",
"tags": "tool,outdoors,gps",
"storage": [
{"name":"hamloc.app.js","url":"app.js"},
{"name":"hamloc.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "osmpoi",
"name": "POI Compass",
"icon": "app.png",
"version":"0.02",
"description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.",
"tags": "tool,outdoors,gps",
"custom": "osmpoi.html",
"storage": [
{"name":"osmpoi.app.js"},
{"name":"osmpoi.img"}
]
},
{ "id": "pong",
"name": "Pong",
"shortName": "Pong",
"icon": "pong.png",
"version": "0.02",
"description": "A clone of the Atari game Pong",
"tags": "game",
"type": "app",
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"pong.app.js","url":"app.js"},
{"name":"pong.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "ballmaze",
"name": "Ball Maze",
"icon": "icon.png",
"version": "0.01",
"description": "Navigate a ball through a maze by tilting your watch.",
"readme": "README.md",
"tags": "game",
"type": "app",
"storage": [
{"name": "ballmaze.app.js","url":"app.js"},
{"name": "ballmaze.img","url":"icon.js","evaluate": true}
],
"data": [
{"name": "ballmaze.json"}
]
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,4 +1,4 @@
# Active Pedometer
# Active Pedometer
Pedometer that filters out arm movement and displays a step goal progress.
I changed the step counting algorithm completely.
@ -6,6 +6,8 @@ Now every step is counted when in status 'active', if the time difference betwee
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
When you reach the step threshold, the steps needed to reach the threshold are counted as well.
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
## Screenshots
* 600 steps
![](600.png)
@ -16,7 +18,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c
* 10600 steps
![](10600.png)
## Features
## Features Widget
* Two line display
* Can display distance (in km) or steps in each line
@ -30,6 +32,23 @@ When you reach the step threshold, the steps needed to reach the threshold are c
* Steps are saved to a file and read-in at start (to not lose step progress)
* Settings can be changed in Settings - App/widget settings - Active Pedometer
## Features App
* The app accesses the data stored for the current day
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
## Data storage
* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data)
* One file is created for each day
* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime
* 'now' is UNIX timestamp in ms
* You can use the app to watch a steps graph
* You can import the file into Excel
* The file does not include a header
* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400)
* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss
## Settings
* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100

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

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

View File

@ -3,13 +3,14 @@
var startTimeStep = new Date(); //set start time
var stopTimeStep = 0; //Time after one step
var timerResetActive = 0; //timer to reset active
var timerStoreData = 0; //timer to store data
var steps = 0; //steps taken
var stepsCounted = 0; //active steps counted
var active = 0; //x steps in y seconds achieved
var stepGoalPercent = 0; //percentage of step goal
var stepGoalBarLength = 0; //length og progress bar
var lastUpdate = new Date(); //used to reset counted steps on new day
var width = 45; //width of widget
var width = 46; //width of widget
//used for statistics and debugging
var stepsTooShort = 0;
@ -18,13 +19,42 @@
var distance = 0; //distance travelled
const s = require('Storage');
const SETTINGS_FILE = 'activepedom.settings.json';
const PEDOMFILE = "activepedom.steps.json";
var dataFile;
var storeDataInterval = 5*60*1000; //ms
let settings;
//load settings
function loadSettings() {
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
settings = s.readJSON(SETTINGS_FILE, 1) || {};
}
function storeData() {
now = new Date();
month = now.getMonth() + 1; //month is 0-based
if (month < 10) month = "0" + month; //leading 0
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data"; //new file for each day
dataFile = s.open(filename,"a");
if (dataFile) { //check if filen already exists
if (dataFile.getLength() == 0) {
//new day, set steps to 0
stepsCounted = 0;
stepsTooShort = 0;
stepsTooLong = 0;
stepsOutsideTime = 0;
}
dataFile.write([
now.getTime(),
stepsCounted,
active,
stepsTooShort,
stepsTooLong,
stepsOutsideTime,
].join(",")+"\n");
}
dataFile = undefined; //save memory
}
//return setting
@ -77,20 +107,20 @@
//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
stepsTooLong++; //count steps which are not counted, because time too long
steps--;
}
//Remove step if time between first and second step is too short
if (stepTimeDiff <= setting('cMinTime')) { //milliseconds
stepsTooShort++; //count steps which are note counted, because time too short
stepsTooShort++; //count steps which are not counted, because time too short
steps--;
}
//Step threshold reached
if (steps >= setting('stepThreshold')) {
if (active == 0) {
stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1
stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status
stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reach active status
}
active = 1;
clearInterval(timerResetActive); //stop timer which resets active
@ -109,14 +139,17 @@
function draw() {
var height = 23; //width is deined globally
distance = (stepsCounted * setting('stepLength')) / 100 /1000 //distance in km
distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km
//Check if same day
let date = new Date();
if (lastUpdate.getDate() == date.getDate()){ //if same day
}
else {
stepsCounted = 1; //set stepcount to 1
else { //different day, set all steps to 0
stepsCounted = 0;
stepsTooShort = 0;
stepsTooLong = 0;
stepsOutsideTime = 0;
}
lastUpdate = date;
@ -166,7 +199,7 @@
stepsTooLong : stepsTooLong,
stepsOutsideTime : stepsOutsideTime
};
require("Storage").write(PEDOMFILE,d); //write array to file
s.write(PEDOMFILE,d); //write array to file
});
//When Step is registered by firmware
@ -182,8 +215,7 @@
});
//Read data from file and set variables
let pedomData = require("Storage").readJSON(PEDOMFILE,1);
let pedomData = s.readJSON(PEDOMFILE,1);
if (pedomData) {
if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate);
stepsCounted = pedomData.stepsToday|0;
@ -191,12 +223,10 @@
stepsTooLong = pedomData.stepsTooLong;
stepsOutsideTime = pedomData.stepsOutsideTime;
}
pedomdata = 0; //reset pedomdata to save memory
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly
//Add widget
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw};
})();

View File

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

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

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

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

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

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

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

BIN
apps/ballmaze/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
apps/ballmaze/maze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

1
apps/banglerun/ChangeLog Executable file
View File

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

View File

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

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

View File

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

2
apps/buffgym/ChangeLog Normal file
View File

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

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

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

View File

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

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

BIN
apps/buffgym/buffgym.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

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

View File

@ -1,93 +1,93 @@
(() => {
const storage = require('Storage');
settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file
var height = 23;
var width = 58;
var interval = 0; //used for the 1 second interval timer
var now = new Date();
var time = 0;
var diff = settingsChronowid.goal - now;
//Convert ms to time
function getTime(t) {
var milliseconds = parseInt((t % 1000) / 100),
seconds = Math.floor((t / 1000) % 60),
minutes = Math.floor((t / (1000 * 60)) % 60),
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
}
function printDebug() {
print ("Nowtime: " + getTime(now));
print ("Now: " + now);
print ("Goaltime: " + getTime(settingsChronowid.goal));
print ("Goal: " + settingsChronowid.goal);
print("Difftime: " + getTime(diff));
print("Diff: " + diff);
print ("Started: " + settingsChronowid.started);
print ("----");
}
//counts down, calculates and displays
function countDown() {
now = new Date();
diff = settingsChronowid.goal - now; //calculate difference
WIDGETS["chronowid"].draw();
//time is up
if (settingsChronowid.started && diff < 1000) {
Bangle.buzz(1500);
//write timer off to file
settingsChronowid.started = false;
storage.writeJSON('chronowid.json', settingsChronowid);
clearInterval(interval); //stop interval
}
//printDebug();
}
// draw your widget
function draw() {
if (!settingsChronowid.started) {
width = 0;
return; //do not draw anything if timer is not started
}
g.reset();
if (diff >= 0) {
if (diff < 600000) { //less than 1 hour left
width = 58;
g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.setFont("6x8", 2);
g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00
}
if (diff >= 600000) { //one hour or more left
width = 48;
g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.setFont("6x8", 1);
g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00
}
}
// not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed.
// else {
// width = 58;
// g.clearRect(this.x,this.y,this.x+width,this.y+height);
// g.setFont("6x8", 2);
// g.drawString("END", this.x+15, this.y+5);
// }
}
if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second
// add the widget
WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() {
reload();
Bangle.drawWidgets(); // relayout all widgets
}};
//printDebug();
countDown();
(() => {
const storage = require('Storage');
settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file
var height = 23;
var width = 58;
var interval = 0; //used for the 1 second interval timer
var now = new Date();
var time = 0;
var diff = settingsChronowid.goal - now;
//Convert ms to time
function getTime(t) {
var milliseconds = parseInt((t % 1000) / 100),
seconds = Math.floor((t / 1000) % 60),
minutes = Math.floor((t / (1000 * 60)) % 60),
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
}
function printDebug() {
print ("Nowtime: " + getTime(now));
print ("Now: " + now);
print ("Goaltime: " + getTime(settingsChronowid.goal));
print ("Goal: " + settingsChronowid.goal);
print("Difftime: " + getTime(diff));
print("Diff: " + diff);
print ("Started: " + settingsChronowid.started);
print ("----");
}
//counts down, calculates and displays
function countDown() {
now = new Date();
diff = settingsChronowid.goal - now; //calculate difference
WIDGETS["chronowid"].draw();
//time is up
if (settingsChronowid.started && diff < 1000) {
Bangle.buzz(1500);
//write timer off to file
settingsChronowid.started = false;
storage.writeJSON('chronowid.json', settingsChronowid);
clearInterval(interval); //stop interval
}
//printDebug();
}
// draw your widget
function draw() {
if (!settingsChronowid.started) {
width = 0;
return; //do not draw anything if timer is not started
}
g.reset();
if (diff >= 0) {
if (diff < 3600000) { //less than 1 hour left
width = 58;
g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.setFont("6x8", 2);
g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00
}
if (diff >= 3600000) { //one hour or more left
width = 48;
g.clearRect(this.x,this.y,this.x+width,this.y+height);
g.setFont("6x8", 1);
g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00
}
}
// not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed.
// else {
// width = 58;
// g.clearRect(this.x,this.y,this.x+width,this.y+height);
// g.setFont("6x8", 2);
// g.drawString("END", this.x+15, this.y+5);
// }
}
if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second
// add the widget
WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() {
reload();
Bangle.drawWidgets(); // relayout all widgets
}};
//printDebug();
countDown();
})();

View File

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

1
apps/hamloc/ChangeLog Normal file
View File

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

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

@ -0,0 +1,14 @@
# QTH Locator
Convert your current GPS location to the [Maidenhead](https://en.wikipedia.org/wiki/Maidenhead_Locator_System) locator system used by HAM amateur radio operators.
## Description
A Maidenhead locator compresses latitude and longitude into a short string of characters, which is similar in concept to the World Geographic Reference System or GEOREF. This position information is presented in a limited level of precision to limit the number of characters needed for its transmission using voice, Morse code, or any other operating mode.
The chosen coding uses alternating pairs of letters and digits, like so:
* BL11bh
##
* support Paul Brewer KI6CQ HamGridSquare.js
* support Chris Veness 2002-2012 LatLon library

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4ACwVeAAM6nQECJsYqDFYItCAgQ0DFrxWE2ZhBxOJA4ICBGwhbbxGtAYOz2etnWt2YwBnQCBGgWtxBjXJQQpBLgQABEQIDBMAgwE2YYBwRcUxOtDwgABGgYvGTQQLCMSRNGEQaODF4SQDHgYwUFIlfAgY2DYQQGDsouCGAQSEGBouEKANl1onEwQvGA4QACA4IwQaIISFFwIwDXwI2ExL2BB4YABJgwwKnQAkRpzYDAAbuBA4oABe4aaDD44uGxCNEQwStEdwgAFR4IHGF4iRBxJeLBwIdCcwgvJxLwFFIRgLBo4dBcwj2CF45xIKIxeLAww4FAAuINIYbKMAteso8FAwovPCQllQQtlF4gLFUYwvDsq/HfIShEDhCQDIgIAFnQHGBBITRnWIXwdfAAYGFSYblBNI5KBsobEDo4GCdxyFDr2tR4+tDQrwNF5YlEnQvJaZIvKr4RIEoovaSAIvyXArbBF6OJR6mCXAgvBDwIlFd5IfBd6tfSQQRGCgeIBJE6DIIAFDoIGGF4OCAgIAEnQHGBBITRnWIF4P+V4ZMCWxBoBRw5PBNI7IGnQuCSAIfE1oSB1oADF4WIXxAvHsoIFH4IvEdIJWEDg4vICRTuJSAwABxAcJF5A5C1ovKRwhgCwSQGF6CpEXxBeGMA59JZIIvGA4hhBLw+JF4xRFSBBNDFIhHFe4VfLxhgCAEguIMAJSB1oABSA4HEB4RwBAgQXHss6LxKRDPQTxBsovIRIdexCOGRpwwIr5gFAwbuEAoheBFyQwFRIrwDLwZuBdwgTBC4IuRYYowGRAq+BAoeCAoRABFyIABD4IsCGAgpFGoguBd4eIFyRiEFoIwCEoKJDRwYqCCAJcUGJICBK4JgDKgOtOIQtce4s6AAI2EBAgteAAizBGgYECwQsiAD4"))

21
apps/hamloc/app.js Normal file
View File

@ -0,0 +1,21 @@
latLonToGridSquare=function(o,a){var t,e,n,s,l,i,r,h,M,f=-100,g=0,u="ABCDEFGHIJKLMNOPQRSTUVWX",d=u.toLowerCase();function N(o){return"number"==typeof o?o:"string"==typeof o?parseFloat(o):"function"==typeof o?parseFloat(o()):void E.showMessage("can't convert \ninput: "+o)}return"object"==typeof o?2===o.length?(f=N(o[0]),g=N(o[1])):"lat"in o&&"lon"in o?(f=N(o.lat),g=N(o.lon)):"latitude"in o&&"longitude"in o?(f=N(o.latitude),g=N(o.longitude)):E.showMessage("can't convert \nobject "+o):(f=N(o),g=N(a)),isNaN(f)&&E.showMessage("lat is NaN"),isNaN(g)&&E.showMessage("lon is NaN"),90===Math.abs(f)&&E.showMessage("grid invalid \nat N/S"),90<Math.abs(f)&&E.showMessage("invalid lat: \n"+f),180<Math.abs(g)&&E.showMessage("invalid lon: \n"+g),t=f+90,e=g+180,n=u[Math.floor(t/10)],s=u[Math.floor(e/20)],l=""+Math.floor(t%10),i=""+Math.floor(e/2%10),h=60*(t-Math.floor(t)),M=60*(e-2*Math.floor(e/2)),r=d[Math.floor(h/2.5)],s+n+i+l+d[Math.floor(M/5)]+r};
Bangle.setGPSPower(1);
var fix;
Bangle.removeAllListeners();
Bangle.on('GPS',function(f) {
fix=f;
g.clear();
g.setFontAlign(0,0);
if (!f.fix) {
g.setFont("6x8",3);
g.drawString("Waiting for",120,70);
g.drawString("GPS Fix",120,110);
g.setFont("6x8",2);
g.drawString(f.satellites+" satellites",120,170);
} else {
g.setFont("6x8",5);
var maidenhead = latLonToGridSquare(fix.lat,fix.lon);
g.drawString(maidenhead,120,120);
}
});

BIN
apps/hamloc/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

4
apps/impwclock/README.md Normal file
View File

@ -0,0 +1,4 @@
# Imprecise Word Clock
This clock tells time in very rough approximation, as in "Late morning" or "Early afternoon." Good for vacations and weekends. Press button 1 to see the time in accurate, digital form. But do you really need to know the exact time?

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEIf4A3iIBEn8ggP//8wgX/+cQl8Agc/BQPyCokQgHzmEB+ET+EfmMj+AXCmABBF4MBiIABiEC+PxC4Uwn4NB+QXMBAMzI4UxmYOBC5sfCgIvBgPzF4cfC5BgCFAMPkPwiXzL4cPmMvkAXDPAnzEgMxR4wDCGITl/AH4ApgUQbIICBAgXwBYMD+UAYoP/l4CBiUhd4QXFgIXCh73BfQUfAgIPBC4cQiIACC4cvj4PBC5AuCC48zgcwC4ZHBC5sBCAIEBF5EAC4RgDCQItCPAIXLCoQBBFgM/IoZHER4QA/AH4Anj8wgXzgX/+cQWoPyYQK9Bn/zj/wb4MTCAMf+MDAYMxkfwj8BmYXBmEzCYMf+cDmPzkMvj8zAIM/eoPyC4fy+IXDl8TmfwI4UvmYABAwIXB//xgPwBIIXCgYFBmEP/8fh/yF4sDC4QjBC4RvBF4UPB4JUBL4kAn8ROIJbBC4IIBL4hDBmaPEgBuB+EB+aPCUQUjCALn/AH4A/A"))

View File

@ -0,0 +1,160 @@
/* Imprecise Word Clock - A. Blanton
A remix of word clock
by Gordon Williams https://github.com/gfwilliams
- Changes the representation of time to be more general
- Shows accurate digital time when button 1 is pressed
*/
/* jshint esversion: 6 */
const allWords = [
"AEARLYDN",
"LATEYRZO",
"MORNINGO",
"KMIDDLEN",
"AFTERDAY",
"OFDZTHEC",
"EVENINGR",
"ORMNIGHT"
];
const timeOfDay = {
0: ["", 0, 0],
1: ["EARLYMORNING", 10, 20, 30, 40, 50, 02, 12, 22, 32, 42, 52, 62],
2: ["MORNING", 02, 12, 22, 32, 42, 52, 62],
3: ["LATEMORNING", 01, 11, 21, 31, 02, 12, 22, 32, 42, 52, 62],
4: ["MIDDAY", 13, 23, 33, 54, 64, 74],
5: ["EARLYAFTERNOON", 10, 20, 30, 40, 50, 04, 14, 24, 34, 44, 70, 71, 72, 73],
6: ["AFTERNOON", 04, 14, 24, 34, 44, 70, 71, 72, 73],
7: ["LATEAFTERNOON", 01, 11, 21, 31, 04, 14, 24, 34, 44, 70, 71, 72, 73],
8: ["EARLYEVENING", 10, 20, 30, 40, 50, 06, 16, 26, 36, 46, 56, 66],
9: ["EVENING", 06, 16, 26, 36, 46, 56, 66],
10: ["NIGHT", 37, 47, 57, 67, 77],
11: ["MIDDLEOFTHENIGHT", 13, 23, 33, 43, 53, 63, 05, 15, 45, 55, 65, 37,47,57,67,77 ],
};
// offsets and increments
const xs = 35;
const ys = 31;
const dy = 22;
const dx = 25;
// font size and color
const fontSize = 3; // "6x8"
const passivColor = 0x3186 /*grey*/ ;
const activeColorNight = 0xF800 /*red*/ ;
const activeColorDay = 0xFFFF /* white */;
function drawWordClock() {
// get time
var t = new Date();
var h = t.getHours();
var m = t.getMinutes();
var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2);
var day = t.getDay();
var hidx;
var activeColor = activeColorDay;
if(h < 7 || h > 19) {activeColor = activeColorNight;}
g.setFont("6x8",fontSize);
g.setColor(passivColor);
g.setFontAlign(0, -1, 0);
// draw allWords
var c;
var y = ys;
var x = xs;
allWords.forEach((line) => {
x = xs;
for (c in line) {
g.drawString(line[c], x, y);
x += dx;
}
y += dy;
});
// Switch case isn't good for this in Js apparently so...
if(h < 3){
// Middle of the Night
hidx = 11;
}
else if (h < 7){
// Early Morning
hidx = 1;
}
else if (h < 10){
// Morning
hidx = 2;
}
else if (h < 12){
// Late Morning
hidx = 3;
}
else if (h < 13){
// Midday
hidx = 4;
}
else if (h < 14){
// Early afternoon
hidx = 5;
}
else if (h < 16){
// Afternoon
hidx = 6;
}
else if (h < 17){
// Late Afternoon
hidx = 7;
}
else if (h < 19){
// Early evening
hidx = 8;
}
else if (h < 21){
// evening
hidx = 9;
}
else if (h < 24){
// Night
hidx = 10;
}
// write hour in active color
g.setColor(activeColor);
timeOfDay[hidx][0].split('').forEach((c, pos) => {
x = xs + (timeOfDay[hidx][pos + 1] / 10 | 0) * dx;
y = ys + (timeOfDay[hidx][pos + 1] % 10) * dy;
g.drawString(c, x, y);
});
// Display digital time while button 1 is pressed
if (BTN1.read()){
g.setColor(activeColor);
g.clearRect(0, 215, 240, 240);
g.drawString(time, 120, 215);
} else { g.clearRect(0, 215, 240, 240); }
}
Bangle.on('lcdPower', function(on) {
if (on) drawWordClock();
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
setInterval(drawWordClock, 1E4);
drawWordClock();
// Show digital time while top button is pressed
setWatch(drawWordClock, BTN1, {repeat:true,edge:"both"});
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,2 +1,3 @@
0.02: Modified for use with new bootloader and firmware
0.03: Added Locale based date
0.04: Improve performance, attempt to remove occasional glitch when LCD on (fix #279)

View File

@ -1,14 +1,15 @@
var locale = require("locale");
var CHARW = 34; // how tall are digits?
var CHARP = 2; // how chunky are digits?
var Y = 50; // start height
// Offscreen buffer
var buf = Graphics.createArrayBuffer(240,86,1,{msb:true});
function flip() {
g.setColor(1,1,1);
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,50);
}
var buf = Graphics.createArrayBuffer(CHARW+CHARP*2,CHARW*2 + CHARP*2,1,{msb:true});
var bufimg = {width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer};
// The last time that we displayed
var lastTime = " ";
// If animating, this is the interval's id
var animInterval;
var timeInterval;
/* Get array of lines from digit d to d+1.
n is the amount (0..1)
@ -49,7 +50,7 @@ const DIGITS = {
[0,1,1,1],
[1,1,1,2],
[1-n,2,1,2]],
"5": (n,maxFive)=>maxFive ? [ // 5 -> 0
"5to0": n=>[ // 5 -> 0
[0,0,0,1],
[0,0,1,0],
[n,1,1,1],
@ -57,7 +58,8 @@ const DIGITS = {
[0,2,1,2],
[0,2,0,2],
[1,1-n,1,1],
[0,1,0,1+n]] : [ // 5 -> 6
[0,1,0,1+n]],
"5to6": n=>[ // 5 -> 6
[0,0,0,1],
[0,0,1,0],
[0,1,1,1],
@ -109,59 +111,66 @@ const DIGITS = {
/* Draw a transition between lastText and thisText.
'n' is the amount - 0..1 */
function draw(lastText,thisText,n) {
buf.clear();
var x = 1; // x offset
const p = 2; // padding around digits
var y = p; // y offset
const s = 34; // character size
function drawDigits(lastText,thisText,n) {
const p = CHARP; // padding around digits
const s = CHARW; // character size
var x = 0; // x offset
g.reset();
for (var i=0;i<lastText.length;i++) {
var lastCh = lastText[i];
var thisCh = thisText[i];
if (thisCh==":") x-=4;
var ch, chn = n;
if (lastCh!==undefined &&
(thisCh-1==lastCh ||
(thisCh==0 && lastCh==5) ||
(thisCh==0 && lastCh==9)))
ch = lastCh;
else {
ch = thisCh;
chn = 0;
if (lastCh!=thisCh) {
var ch, chn = n;
if ((thisCh-1==lastCh ||
(thisCh==0 && lastCh==5) ||
(thisCh==0 && lastCh==9)))
ch = lastCh;
else {
ch = thisCh;
chn = 0;
}
buf.clear();
if (ch=="5") ch = (lastCh==5 && thisCh==0)?"5to0":"5to6";
var l = DIGITS[ch](chn);
l.forEach(c=>{
if (c[0]!=c[2]) // horiz
buf.fillRect(p+c[0]*s,c[1]*s,p+c[2]*s,2*p+c[3]*s);
else if (c[1]!=c[3]) // vert
buf.fillRect(c[0]*s,p+c[1]*s,2*p+c[2]*s,p+c[3]*s);
});
g.drawImage(bufimg,x,Y);
}
var l = DIGITS[ch](chn,lastCh==5 && thisCh==0);
l.forEach(c=>{
if (c[0]!=c[2]) // horiz
buf.fillRect(x+c[0]*s,y+c[1]*s-p,x+c[2]*s,y+c[3]*s+p);
else if (c[1]!=c[3]) // vert
buf.fillRect(x+c[0]*s-p,y+c[1]*s,x+c[2]*s+p,y+c[3]*s);
});
if (thisCh==":") x-=4;
x+=s+p+7;
}
y += 2*s;
}
function drawSeconds() {
var x = (CHARW + CHARP + 6)*5;
var y = Y + 2*CHARW + CHARP;
var d = new Date();
buf.setFont("6x8");
buf.setFontAlign(-1,-1);
buf.drawString(("0"+d.getSeconds()).substr(-2), x, y-8);
g.reset();
g.setFont("6x8");
g.setFontAlign(-1,-1);
g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true);
// date
buf.setFontAlign(0,-1);
g.setFontAlign(0,-1);
var date = locale.date(d,false);
buf.drawString(date, buf.getWidth()/2, y+8);
flip();
g.drawString(date, g.getWidth()/2, y+8, true);
}
/* Show the current time, and animate if needed */
function showTime() {
if (!Bangle.isLCDOn()) return;
if (animInterval) return; // in animation - quit
var d = new Date();
var t = (" "+d.getHours()).substr(-2)+":"+
("0"+d.getMinutes()).substr(-2);
var l = lastTime;
// same - don't animate
if (t==l) {
draw(t,l,0);
if (t==l || l==" ") {
drawDigits(l,t,0);
drawSeconds();
lastTime = t;
return;
}
var n = 0;
@ -170,23 +179,35 @@ function showTime() {
if (n>=1) {
n=1;
clearInterval(animInterval);
animInterval=0;
animInterval = undefined;
}
draw(l,t,n);
drawDigits(l,t,n);
}, 20);
lastTime = t;
}
Bangle.on('lcdPower',function(on) {
if (on)
if (animInterval) {
clearInterval(animInterval);
animInterval = undefined;
}
if (timeInterval) {
clearInterval(timeInterval);
timeInterval = undefined;
}
if (on) {
showTime();
timeInterval = setInterval(showTime, 1000);
} else {
lastTime = " ";
}
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
// Update time once a second
setInterval(showTime, 1000);
timeInterval = setInterval(showTime, 1000);
showTime();
// Show launcher when middle button pressed

View File

@ -1 +1,2 @@
0.01: First release
0.02: Improved date readability, fixed drawing of widgets

View File

@ -1,5 +1,3 @@
const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA"));
const locale = require("locale");
const black = 0x0000;
@ -9,6 +7,8 @@ let hour;
let minute;
let date;
let timer;
function draw() {
const d = new Date();
@ -37,7 +37,7 @@ function draw() {
}
if (newDate !== date) {
g.setFontVector(12);
g.setFont('6x8', 2);
g.setColor(black);
g.drawString(date, 120, 228);
g.setColor(0xFFFF);
@ -46,23 +46,32 @@ function draw() {
}
}
function drawAll() {
function startDrawing() {
hour = '';
minute = '';
date = '';
var bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ultr7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBttJttPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNtAAIDGtwGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMOHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1tzw0BDYI6B0R3DAAJ1BvMOp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw8zgAAiFYivEPgoSEAYo9jGgY4EO5Q7kVwiyFGggBFhASBHkhsKAAcqqtOAgMzd8o6HO495vF5QgMzrw7lhBoBvMqG4g"));
g.drawImage(bob, 0, 0, { scale: 4 });
Bangle.drawWidgets();
draw();
timer = setInterval(draw, 1000);
}
function stopDrawing() {
if (timer) {
clearInterval(timer);
timer = undefined;
}
}
Bangle.on('lcdPower', function(on) {
stopDrawing();
if (on) {
drawAll();
startDrawing();
}
});
Bangle.loadWidgets();
Bangle.drawWidgets();
setInterval(draw, 1000);
drawAll();
startDrawing();
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' });

View File

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

View File

@ -1,11 +1,11 @@
(function() {
let s = require('Storage').readJSON('ncstart.settings.json', 1)
let s = require('Storage').readJSON('ncstart.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')) {
setTimeout(() => {
s.welcomed = true
require('Storage').write('ncstart.settings.json', s)
require('Storage').write('ncstart.json', s)
load('ncstart.app.js')
})
}

View File

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

View File

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

View File

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

View File

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

2
apps/osmpoi/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Change img when no fix

5
apps/osmpoi/README.md Normal file
View File

@ -0,0 +1,5 @@
# Points Of Interest Compass
## Description
Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.

BIN
apps/osmpoi/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

228
apps/osmpoi/osmpoi.html Normal file
View File

@ -0,0 +1,228 @@
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" />
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<style>
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100%;
}
#map { z-index: 1; }
#controls {
padding: 10px;
margin: 10px;
border: 1px solid black;
position:absolute;
right:0px;top:0px;
background-color: rgb(255, 255, 255);
z-index: 100;
}
</style>
</head>
<body>
<div id="map">
</div>
<div id="controls">
<select id="amenity">
<option value="drinking_water">Drinking Water</option>
<option value="bicycle_parking">Bicycle Park</option>
<option value="post_box">Post Box</option>
<option value="bar">Bars</option>
<option value="bbq">Barbecue</option>
<option value="cafe">Café</option>
<option value="fast_food">Fast food</option>
<option value="restaurant">restaurant</option>
<option value="bicycle_repair_station">Bicycle Repair</option>
<option value="bicycle_rental">Bicycle rental</option>
<option value="bus_station">Bus station</option>
<option value="atm">Bank</option>
<option value="pharmacy">Pharmacy</option>
<option value="interner_cafe">Cybercafé</option>
</select>
<p>Click <button id="poi" class="btn btn-primary">Find P.O.I.</button></p>
<p>If ok, Click <button id="upload" class="btn btn-primary">Upload</button></p>
</div>
<script src="../../lib/customize.js"></script>
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/osmtogeojson@2.2.12/osmtogeojson.js"></script>
<script>
/*
https://overpass-turbo.eu/ to check queries.
Run:
node
({{bbox}});
out;
Over the area you're interested in, click on the POI and find out
what you need to search for, eg.
node
[amenity=pub]
({{bbox}});
out;
*/
var map = L.map('map').locate({setView: true, maxZoom: 16});
var features = [];
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map data &copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
}).addTo(map);
function buildOverpassApiUrl(map, overpassQuery) {
var bounds = map.getBounds().getSouth() + ',' + map.getBounds().getWest() + ',' + map.getBounds().getNorth() + ',' + map.getBounds().getEast();
var nodeQuery = 'node[' + overpassQuery + '](' + bounds + ');';
var wayQuery = 'way[' + overpassQuery + '](' + bounds + ');';
var relationQuery = 'relation[' + overpassQuery + '](' + bounds + ');';
var query = '?data=[out:json][timeout:15];(' + nodeQuery + wayQuery + relationQuery + ');out body geom;';
var baseUrl = 'https://overpass-api.de/api/interpreter';
var resultUrl = baseUrl + query;
return resultUrl;
}
document.getElementById("poi").addEventListener("click", function() {
//var overpassApiUrl = buildOverpassApiUrl(map, "highway=bus_stop");
var poi = document.getElementById("amenity").options[document.getElementById("amenity").selectedIndex].value;
var overpassApiUrl = buildOverpassApiUrl(map, "amenity="+poi);
var req = new XMLHttpRequest();
req.responseType = 'json';
req.onreadystatechange = function() {
console.log("XMLHttpRequest", this.readyState);
if (this.readyState == 4) {
var osmDataAsJson = req.response;
features = [];
if (osmDataAsJson.elements) {
osmDataAsJson.elements.forEach(function(el) {
if (el.type!="node") return;
features.push({lat:el.lat, lon:el.lon,name:el.tags.name});
});
}
var resultAsGeojson = osmtogeojson(osmDataAsJson);
var resultLayer = L.geoJson(resultAsGeojson, {
style: function (feature) {
return {color: "#ff0000"};
},
filter: function (feature, layer) {
var isPolygon = (feature.geometry) && (feature.geometry.type !== undefined) && (feature.geometry.type === "Polygon");
if (isPolygon) {
feature.geometry.type = "Point";
var polygonCenter = L.latLngBounds(feature.geometry.coordinates[0]).getCenter();
feature.geometry.coordinates = [ polygonCenter.lat, polygonCenter.lng ];
}
return true;
},
onEachFeature: function (feature, layer) {
var popupContent = "";
popupContent = popupContent + "<dt>@id</dt><dd>" + feature.properties.type + "/" + feature.properties.id + "</dd>";
var keys = Object.keys(feature.properties.tags);
keys.forEach(function (key) {
popupContent = popupContent + "<dt>" + key + "</dt><dd>" + feature.properties.tags[key] + "</dd>";
});
popupContent = popupContent + "</dl>"
layer.bindPopup(popupContent);
}
}).addTo(map);
}
};
req.open("GET", overpassApiUrl, true);
req.send();
});
document.getElementById("upload").addEventListener("click", function() {
var app = `var features = ${JSON.stringify(features)};
var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AC2swMllcrBQoHBktWwIEBlYEBqwAEgAICF6FWldbrgDBAIMrFIIhBrlbAAIIBwOsAAgsClcxF6ErrmG2GGw1cqsxrmAwuG2OxxkxFoOCq1VFANVLgIyBPI4vLq2F2OwAAQrCAgPVw0xEgVVMoQ7BwsxmOm1iUCMCIuD2GM2AvC2IuDFQIACwADCrZrCGCOBqovI2NcEAIuBTwQAHlZhCL6FVYAKQG2NV1i3BZYIpDrZiFlb1BYSFWwAvG6tbwOCmIuFF4YAC2FVwOBMCFWraBCRwReDBYIpELQOAAQQ5CqpgBF57RBFYK+C2IcBw4vBEQIwDrYEDrlbGYOMmMr64vQd4hcBX4VcF4LsGOIYGCxhMBYAKORGAhhBF4TtBAAzvFwuAwIvPlbuBXIQAE2BMBHgIoBE4QvEricBqtWF4KPOF4KICAAReCd4WBdoYAJmKPCF50lrhYCYIuxreBwUxR5RrBrmCwIABF54pCFwrkBJoMxFAKGBF4lcFwNbR4JxBX6GGFYeHSoOGSQJgBGARVBFoIqCMIWMlS+ClbvRMIbFEJ4PXwMrmIqBAARjDleB67ABFxowCYAgDBQQVbFwIAB00xGAQMDA4OCNoJdOAAWBYAex6uGNAIcBFwSvB1mCq1VFYMxqwICA4IvRDgKKDw0qwQrBPgIuBX4IoCAAmBDILQCF6AwBwBgBFwgwCldVTQKJBqtcwB1BSgXV6uAXyAvCrYuHAAMrrYuBGIQABwoGBOgWxF6SEBFwWmFwpgBF4IADxgyC2IFCR6Z5BLpHX1krLoQADLYQHEqrvRWQMrFxKOEF4eMxgyBGgVWkovRgCMH1kqZIInBLwwtCRwZfQqxdLEYQuFAQYHCraORq2BXZPVKwIABEoIGBFoYCCRwNWRyIuIquGrgfBldWwGAwqMBFYVbrlcqovSL42sksrwCsBD4QDBkoyBqsxFQINBmItRL4UrGAZdBLIIABCQ8lgAsBH4IsSGA4uBDq4wTq0xmIuqAH4A/AH4A/AH4AHA="));
var img_fix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AE0rg4vtq8PF1kHxM7gJesF4Jgrg+IF4M6MFReBF4JgqXoIvDMFJeCF4RgoLwYvDnZgmLwYvEMEpeEF4jBlmYvIMEheFF4pgjXogvGMEReGF407MEBeGF45gfg+IF5rBfLw4vHMDxeIF5BgdLxAvIMDkHFxAvJMDZeJF5JgaLxQvKnZgYLxQvLMC5eLF5bBXmYvWMCxeMF5hgVXpYvNMCheNF5s7MCReNF5xgRg+IFZNersylcHhEPmjBbLw1er1XlkHIhI0CnRgUmQqEKwKnRgIzBiBpBnUICpszryCCFiIyGM4TBRAH4A/AH4A/AH4AFA"));
// https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js
function project(latlong) {
var d = Math.PI / 180,
max = 85.0511287798,
R = 6378137, // earth radius in m
lat = Math.max(Math.min(max, latlong.lat), -max),
sin = Math.sin(lat * d);
return {x:R * latlong.lon * d,
y:R * Math.log((1 + sin) / (1 - sin)) / 2};
}
features.forEach(function(f) {
f.p = project(f);
});
var fix = {fix:false,satellites:0};
var nearest = undefined;
var nearestangle = 0;
var nearestdist = 0;
Bangle.on('GPS', function(f) {
fix = f;
fix.p = project(fix);
nearest = undefined;
nearestangle = 0;
nearestdist = 5000; // 5km
features.forEach(function(f) {
var dx = f.p.x - fix.p.x;
var dy = f.p.y - fix.p.y;
var d = Math.sqrt(dx*dx + dy*dy);
if (d<nearestdist) {
nearestdist = d;
nearest = f;
}
});
if (nearest) {
var dx = nearest.p.x - fix.p.x;
var dy = nearest.p.y - fix.p.y;
nearestangle = Math.atan2(dy,dx);
}
});
Bangle.on('mag', function(m) {
if (!Bangle.isLCDOn()) return;
var headingrad = m.heading*Math.PI/180; // in radians
if (!isFinite(headingrad)) headingrad=0;
if (nearest)
g.drawImage(img_fix,120,120,{
rotate: (Math.PI/2)+headingrad-nearestangle,
scale:3,
});
else
g.drawImage(img_nofix,120,120,{
rotate: headingrad,
scale:2,
});
g.clearRect(60,0,180,24);
g.setFontAlign(0,0);
g.setFont("6x8");
if (fix.fix) {
g.drawString(nearest ? nearest.name : "---",120,4);
g.setFont("6x8",2);
g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16);
} else {
g.drawString(fix.satellites+" satellites",120,4);
}
});
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
g.clear();`;
var icon = `require("heatshrink").decompress(atob("mEwxH+AH4A0PgYurg9kr0rGM4nBg9dsgADmUHGUYtHAAddGIJcgFpIxEMTsAlYtMAAZiaLh4AFmQwXLiSTaLiosBnMymUrGCYTBAAgvPCgjwaMh4raF/4v/F/4vUg4vulZgDgAAIF8EyEQUAh0cAAkVisHGA4+HF6gVBiwwFjkONo0HAAMOAAIvTnIhEiovMFQQADNgYvQroUDg4uGjj9EF448DF6a+HAAS+EF5CQCF6SMIeAQvFXYYwHF59eLpSOBF4hgKGAIvPskAFxKOFF80VM4KOFSBs5F6EWRY4xBF43+F5UyF6DuEizZBKwIuHSBQvXdIwvKMYsHlYvQW4IuPYAYRBMggvRgIvCFxwwCTYZlEg4vPlcHjkVFx6WJF6YuXMoTwCF58yVgIvXY4YvQrqqDGDAvvPYIvPsguaYQYv/F7owBF/4vfg4AGTIIAHF7gA/AH4AwA="))`;
sendCustomizedApp({
storage:[
{name:"osmpoi.app.js", content:app},
{name:"osmpoi.img", content:icon, evaluate:true},
]
});
});
</script>
</body>
</html>

2
apps/pong/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: 2 players local + improve ai

28
apps/pong/README.md Normal file
View File

@ -0,0 +1,28 @@
# Pong
A clone of the Atari game Pong
<img src="https://user-images.githubusercontent.com/702227/79855656-2a507a00-83c3-11ea-9162-65732729b992.png" height="384" width="384" />
## Features
- Play against a dumb AI
- Play local Multiplayer against your friends
## Controls
Player's controls:
- UP: BTN1
- DOWN: BTN2
long press to move faster
Restart a game:
- RESET: BTN3
Buttons for player 2:
- UP: BTN4
- DOWN: BTN5
## Creator
<https://twitter.com/fredericrous>

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIEBgOABQYFD8AUEApoXFDqIXV4BYGKZIANsIRE+IFE/IFEvCIFGrgXLDqIAOgc/9/2hv+g8///3AoUwvE3xuABYP4m3NzwFB7E2tu/CIMYm09wYFDjoFCj4pB/8HkEP+EBFII7EAosDJxYA="))

424
apps/pong/app.js Normal file
View File

@ -0,0 +1,424 @@
/**
* BangleJS Pong game
*
* Original Author: Frederic Rousseau https://github.com/fredericrous
* Created: April 2020
*
* Inspired by:
* - Let's make pong, One Man Army Studios, Youtube
* - Pong.js, KanoComputing, Github
* - Coding Challenge #67: Pong!, The Coding Train, Youtube
* - Pixl.js Multiplayer Pong, espruino website
*/
const SCREEN_WIDTH = 240;
const FPS = 16;
const MAX_SCORE = 11;
let scores = [0, 0];
let aiSpeedRandom = 0;
let winnerMessage = '';
const sound = {
ping: () => Bangle.beep(8, 466),
pong: () => Bangle.beep(8, 220),
fall: () => Bangle.beep(16*3, 494).then(_ => Bangle.beep(32*3, 3322))
};
function Vector(x, y) {
this.x = x;
this.y = y;
}
Vector.prototype.add = function (x) {
this.x += x.x || 0;
this.y += x.y || 0;
return this;
};
const constrain = (n, low, high) => Math.max(Math.min(n, high), low);
const random = (min, max) => Math.random() * (max - min) + min;
const intersects = (circ, rect, right) => {
var c = circ.pos;
var r = circ.r;
if (c.y - r < rect.pos.y + rect.height && c.y + r > rect.pos.y) {
if (right) {
return c.x + r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width
} else {
return c.x - r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width
}
}
return false;
}
///////////////////////////// Ball //////////////////////////////////////////
function Ball() {
this.r = 4;
this.prevPos = null;
this.originalSpeed = 4;
this.maxSpeed = 6;
this.reset();
}
Ball.prototype.reset = function() {
this.speed = this.originalSpeed;
var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
var bounceAngle = Math.PI/6;
this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
this.ballReturn = 0;
};
Ball.prototype.restart = function() {
this.reset();
ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
};
Ball.prototype.show = function (invert) {
if (this.prevPos != null) {
g.setColor(invert ? -1 : 0);
g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r);
}
g.setColor(invert ? 0 : -1);
g.fillCircle(this.pos.x, this.pos.y, this.r);
this.prevPos = {
x: this.pos.x,
y: this.pos.y,
r: this.r
};
};
function bounceAngle(playerY, ballY, playerHeight, maxHangle) {
let relativeIntersectY = (playerY + (playerHeight/2)) - ballY;
let normalizedRelativeIntersectionY = relativeIntersectY / (playerHeight/2);
let bounceAngle = normalizedRelativeIntersectionY * maxHangle;
return { x: Math.cos(bounceAngle), y: -Math.sin(bounceAngle) };
}
Ball.prototype.bouncePlayer = function (directionX, directionY, player) {
this.ballReturn++;
this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed);
var MAX_BOUNCE_ANGLE = 4 * Math.PI/12;
var angle = bounceAngle(player.pos.y, this.pos.y, player.height, MAX_BOUNCE_ANGLE)
this.velocity.x = this.speed * angle.x * directionX;
this.velocity.y = this.speed * angle.y * directionY;
this.ballReturn % 2 === 0 ? sound.ping() : sound.pong();
};
Ball.prototype.bounce = function (directionX, directionY, player) {
if (player)
return this.bouncePlayer(directionX, directionY, player);
if (directionX) {
this.velocity.x = Math.abs(this.velocity.x) * directionX;
}
if (directionY) {
this.velocity.y = Math.abs(this.velocity.y) * directionY;
}
};
Ball.prototype.fall = function (playerId) {
scores[playerId]++;
if (scores[playerId] >= MAX_SCORE) {
this.restart();
state = 3;
if (playerId === 1) {
winnerMessage = startOption === 0 ? "AI Wins!" : "Player 2 Wins!";
} else {
winnerMessage = startOption === 0 ? "You Win!" : "Player 1 Wins!";
}
} else {
sound.fall();
this.reset();
}
};
Ball.prototype.wallCollision = function () {
if (this.pos.y < 0) {
this.bounce(0, 1);
} else if (this.pos.y > SCREEN_WIDTH) {
this.bounce(0, -1);
} else if (this.pos.x < 0) {
this.fall(1);
} else if (this.pos.x > SCREEN_WIDTH) {
this.fall(0);
} else {
return false;
}
return true;
};
Ball.prototype.playerCollision = function (player) {
if (intersects(this, player)) {
if (this.pos.x < SCREEN_WIDTH/2) {
this.bounce(1, 1, player);
this.pos.add(new Vector(this.width, 0));
aiSpeedRandom = random(-1.6, 1.6);
} else {
this.bounce(-1, 1, player);
this.pos.add(new Vector(-(this.width / 2 + 1), 0));
}
return true;
}
return false;
};
Ball.prototype.collisions = function () {
return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai);
};
Ball.prototype.updatePosition = function () {
var elapsed = new Date().getTime() - this.lastUpdate;
var x = (elapsed / 50) * this.velocity.x;
var y = (elapsed / 50) * this.velocity.y;
this.pos.add(new Vector(x, y));
};
Ball.prototype.update = function () {
this.updatePosition();
this.lastUpdate = new Date().getTime();
this.collisions();
};
//////////////////////////// Player /////////////////////////////////////////
function Player(right) {
this.width = 4;
this.height = 30;
this.pos = new Vector(right ? SCREEN_WIDTH-this.width : this.width, SCREEN_WIDTH/2 - this.height/2);
this.acc = new Vector(0, 0);
this.speed = 15;
this.maxSpeed = 25;
this.prevPos = null;
this.right = right;
}
Player.prototype.show = function () {
if (this.prevPos != null) {
g.setColor(0);
g.fillRect(this.prevPos.x1, this.prevPos.y1, this.prevPos.x2, this.prevPos.y2);
}
g.setColor(-1);
g.fillRect(this.pos.x, this.pos.y, this.pos.x+this.width, this.pos.y+this.height);
this.prevPos = {
x1: this.pos.x,
y1: this.pos.y,
x2: this.pos.x+this.width,
y2: this.pos.y+this.height
};
};
Player.prototype.up = function () {
this.acc.y -= this.speed;
};
Player.prototype.down = function () {
this.acc.y += this.speed;
};
Player.prototype.stop = function () {
this.acc.y = 0;
};
Player.prototype.update = function () {
this.acc.y = constrain(this.acc.y, -this.maxSpeed, this.maxSpeed);
this.pos.add(this.acc);
this.pos.y = constrain(this.pos.y, 0, SCREEN_WIDTH-this.height);
};
////////////////////////////// AI ///////////////////////////////////////////
function AI() {
Player.call(this);
this.pos = new Vector(SCREEN_WIDTH-this.width*2, SCREEN_WIDTH/2 - this.height/2);
}
AI.prototype = Object.create(Player.prototype);
AI.prototype.constructor = Player;
AI.prototype.update = function () {
var y = ball.pos.y - this.height/2;
var randomizedY = ball.ballReturn < 3 ? y : y + (aiSpeedRandom * this.height/2);
var yConstrained = constrain(randomizedY, 0, SCREEN_WIDTH-this.height);
this.pos = new Vector(this.pos.x, yConstrained);
};
/////////////////////////////// Scenes ////////////////////////////////////////
function net() {
var dashSize = 5;
for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) {
g.setColor(-1);
let halfScreen = SCREEN_WIDTH/2;
g.fillRect(halfScreen-dashSize/2, y, halfScreen+dashSize/2, y+dashSize);
}
}
function drawScores() {
let x1 = SCREEN_WIDTH/4-5;
let x2 = SCREEN_WIDTH*3/4-5;
g.setColor(0);
g.setFont('Vector', 20);
g.drawString(prevScores[0], x1, 7);
g.drawString(prevScores[1], x2, 7);
g.setColor(-1);
g.setFont('Vector', 20);
g.drawString(scores[0], x1, 7);
g.drawString(scores[1], x2, 7);
prevScores = scores.slice();
}
function drawGameOver() {
g.setFont("Vector", 20);
g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10);
}
function showControls(hide) {
g.setColor(hide ? 0 : -1);
g.setFont("Vector", 8);
var topArrowString = `
########
##
## ##
### ##
### ##
###
##
`;
var arrows = [Graphics.createImage(topArrowString), Graphics.createImage(`
##
##
####################
##
##
`), Graphics.createImage(topArrowString.split('\n').reverse().join('\n'))
];
g.drawString('UP', 170, 50);
g.drawImage(arrows[0], 200, 40);
g.drawString('DOWN', 156, 120);
g.drawImage(arrows[1], 200, 120);
g.drawString('START', 152, 190);
g.drawImage(arrows[2], 200, 200);
}
function drawStartScreen(hide) {
g.setColor(hide ? 0 : -1);
g.setFont("Vector", 10);
g.drawString("1 PLAYER", 95, 80);
g.drawString("2 PLAYERS", 95, 110);
const ball1 = new Ball();
ball1.prevPos = null;
ball1.pos = new Vector(87, 86);
ball1.show(hide || !(startOption === 0));
const ball2 = new Ball();
ball2.prevPos = null;
ball2.pos = new Vector(87, 116);
ball2.show(hide || !(startOption === 1));
}
function drawStartTimer(count, callback) {
setTimeout(_ => {
player.show();
ai.show();
net();
g.setColor(0);
g.fillRect(117-7, 115-7, 117+14, 115+14);
if (count >= 0) {
g.setFont("Vector", 10);
g.drawString(count+1, 115, 115);
g.setColor(-1);
g.drawString(count === 0 ? 'Go!' : count, 115 - (count === 0 ? 4: 0), 115);
drawStartTimer(count - 1, callback);
} else {
g.setColor(0);
g.fillRect(117-7, 115-7, 117+14, 115+14);
callback();
}
}, 800);
}
//////////////////////////////// Main /////////////////////////////////////////
function onFrame() {
if (state === 1) {
ball.update();
player.update();
ai.update();
ball.show();
player.show();
ai.show();
net();
ball.show();
} else if (state === 3) {
g.clear();
g.setColor(0);
g.fillRect(0,0,240,240);
state++;
} else if (state === 4) {
drawGameOver();
} else {
player.show();
ai.show();
net();
}
drawScores();
}
function startThatGame() {
player.show();
ai.show();
net();
drawScores();
drawStartTimer(3, () => setInterval(onFrame, 1000 / FPS));
}
var player = new Player();
var ai;
var ball = new Ball();
var state = 0;
var prevScores = [0, 0];
var playerBle = null;
var startOption = 0;
g.clear();
g.setColor(0);
g.fillRect(0,0,240,240);
showControls();
setTimeout(() => {
showControls(true);
drawStartScreen();
}, 2000);
////////////////////////////// Controls ///////////////////////////////////////
setWatch(o => {
if (state === 0) {
if (o.state) {
startOption = startOption === 0 ? startOption : startOption - 1;
drawStartScreen();
}
} else o.state ? player.up() : player.stop();
}, BTN1, {repeat: true, edge: 'both'});
setWatch(o => {
if (state === 0) {
if (o.state) {
startOption = startOption === 1 ? startOption : startOption + 1;
drawStartScreen();
}
} else o.state ? player.down() : player.stop();
}, BTN2, {repeat: true, edge: 'both'});
setWatch(o => {
state++;
clearInterval();
if (state >= 2) {
g.setColor(0);
g.fillRect(0, 0, 240, 240);
ball.show(true);
scores = [0, 0];
playerBle = null;
ball = new Ball();
state = 1;
startThatGame();
} else {
drawStartScreen(true);
showControls(true);
if (startOption === 1) {
ai = new Player(true);
startThatGame();
} else {
ai = new AI();
startThatGame();
}
}
}, BTN3, {repeat: true});
setWatch(o => startOption === 1 && (o.state ? ai.up() : ai.stop()), BTN4, {repeat: true, edge: 'both'});
setWatch(o => startOption === 1 && (o.state ? ai.down() : ai.stop()), BTN5, {repeat: true, edge: 'both'});

BIN
apps/pong/pong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@ -1 +1,2 @@
0.01: First published version of app
0.02: Added support for locale and 12H clock

View File

@ -4,6 +4,8 @@
var hours;
var date;
var first = true;
var locale = require('locale');
var _12hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"] || false;
const screen = {
width: g.getWidth(),
@ -41,17 +43,7 @@
};
const dateStr = function (date) {
day = date.getDate();
month = date.getMonth();
year = date.getFullYear();
if (day < 10) {
day = "0" + day;
}
if (month < 10) {
month = "0" + month;
}
return year + "-" + month + "-" + day;
return locale.date(new Date(), 1);
};
const getArcXY = function (centerX, centerY, radius, angle) {
@ -72,7 +64,7 @@
//g.setPixel(r[0],r[1]);
g.drawLine(r1[0], r1[1], r2[0], r2[1]);
g.setColor('#333333');
g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width-4)
g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4)
};
const drawSecArc = function (sections, color) {
@ -84,7 +76,7 @@
//g.setPixel(r[0],r[1]);
g.drawLine(r1[0], r1[1], r2[0], r2[1]);
g.setColor('#333333');
g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width-4)
g.drawCircle(settings.circle.middle, settings.circle.center, rad - settings.circle.width - 4)
};
const drawClock = function () {
@ -104,7 +96,7 @@
}
first = false;
}
// Reset seconds
if (seconds == 59) {
g.setColor('#000000');
@ -128,14 +120,34 @@
//Update seconds when needed
if (seconds != currentTime.getSeconds()) {
seconds = currentTime.getSeconds();
drawSecArc(seconds, settings.circle.colorsec);
drawSecArc(seconds, settings.circle.colorsec);
}
//Write the time as configured in the settings
hours = currentTime.getHours();
if (_12hour && hours > 13) {
hours = hours - 12;
}
var meridian;
if (typeof locale.meridian === "function") {
meridian = locale.meridian(new Date());
} else {
meridian = "";
}
var timestr;
if (meridian.length > 0 && _12hour) {
timestr = hours + " " + meridian;
} else {
timestr = hours;
}
g.setColor(settings.time.color);
g.setFont(settings.time.font, settings.time.size);
g.drawString(hours, settings.time.center, settings.time.middle);
g.drawString(timestr, settings.time.center, settings.time.middle);
//Write the date as configured in the settings
g.setColor(settings.date.color);
@ -149,7 +161,7 @@
// clean app screen
g.clear();
g.setFontAlign( 0, 0, 0);
g.setFontAlign(0, 0, 0);
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

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

View File

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

View File

@ -6,3 +6,4 @@
0.04: Changed save file filename, add interface.html to allow laps to be loaded
0.05: Added widgets
0.06: Added total running time, moved lap time to smaller display, total run time now appends as first entry in array, saving now saves last lap as well
0.07: Ensure seconds counter is in sync with subseconds (fix #341)

View File

@ -94,7 +94,8 @@ setWatch(function() { // Start/stop
displayInterval = setInterval(function() {
var last = tCurrent;
if (started) tCurrent = Date.now();
if (Math.floor(last/1000)!=Math.floor(tCurrent/1000))
if (Math.floor((last-tStart)/1000)!=Math.floor((tCurrent-tStart)/1000) ||
Math.floor((last-tTotal)/1000)!=Math.floor((tCurrent-tTotal)/1000))
drawsecs();
else
drawms();

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)

View File

@ -1,18 +1,26 @@
(function() {
var clickTimes = [];
var CLICK_COUNT = 4; // number of taps
var CLICK_PERIOD = 1; // second
var clickPattern = "";
var TAPS = 4; // number of taps
var PERIOD = 1; // seconds
// we don't actually create/draw a widget here at all...
Bangle.on("lcdPower",function(on) {
// First click (that turns LCD on) isn't given to
// setWatch, so handle it here
if (on) clickTimes=[getTime()];
if (!on) return;
clickTimes=[getTime()];
clickPattern="x";
});
setWatch(function(e) {
while (clickTimes.length>=CLICK_COUNT) clickTimes.shift();
function tap(e,c) {
clickPattern = clickPattern.substr(-3)+c;
while (clickTimes.length>=TAPS) clickTimes.shift();
clickTimes.push(e.time);
var clickPeriod = e.time-clickTimes[0];
if (clickTimes.length==CLICK_COUNT && clickPeriod<CLICK_PERIOD)
if (clickPeriod<PERIOD && clickPattern.match(/.313/)) {
load("torch.app.js");
}, BTN3, {repeat:true, edge:"rising"});
}
}
setWatch(function(e) { tap(e,"1"); }, BTN1, {repeat:true, edge:"rising"});
setWatch(function(e) { tap(e,"3"); }, BTN3, {repeat:true, edge:"rising"});
})();

View File

@ -3,4 +3,4 @@
0.03: Close launcher when lcd turn off
0.04: Complete rewrite to add animation and loop ( issue #210 )
0.05: Improve perf
0.06: Only store relevant app data (saves RAM when many apps)
0.06: Complete rewrite in 80x80, better perf, add settings

View File

@ -1,160 +1,206 @@
Bangle.setLCDMode("120x120");
const Storage = require("Storage");
const filename = 'toucher.json';
let settings = Storage.readJSON(filename,1) || {
hightres: true,
animation : true,
frame : 3,
debug: false
};
if(!settings.highres) Bangle.setLCDMode("80x80");
else Bangle.setLCDMode();
g.clear();
g.flip();
const Storage = require("Storage");
function getApps(){
return Storage.list(/\.info$/).map(app=>{var a=Storage.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src,version:a.version}})
.filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type))
.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
});
}
let icons = {};
const HEIGHT = g.getHeight();
const WIDTH = g.getWidth();
const HALF = WIDTH/2;
const ANIMATION_FRAME = 4;
const ANIMATION_STEP = HALF / ANIMATION_FRAME;
const ORIGINAL_ICON_SIZE = 48;
const STATE = {
settings_open: false,
index: 0,
target: 240,
offset: 0
};
function getPosition(index){
return (index*HALF);
}
let current_app = 0;
let target = 0;
let slideOffset = 0;
function getApps(){
const exit_app = {
name: 'Exit',
special: true
};
const raw_apps = Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) })
.filter(app=>app.type=="app" || app.type=="clock" || !app.type)
.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
}).map(raw => ({
name: raw.name,
src: raw.src,
icon: raw.icon,
version: raw.version
}));
const back = {
name: 'BACK',
back: true
};
let icons = {};
const apps = [back].concat(getApps());
apps.push(back);
function noIcon(x, y, size){
const half = size/2;
g.setColor(1,1,1);
g.setFontAlign(-0,0);
const fontSize = Math.floor(size / 30 * 2);
g.setFont('6x8', fontSize);
if(fontSize) g.drawString('-?-', x+1.5, y);
g.drawRect(x-half, y-half, x+half, y+half);
const apps = [Object.assign({}, exit_app)].concat(raw_apps);
apps.push(exit_app);
return apps.map((app, i) => {
app.x = getPosition(i);
return app;
});
}
function drawIcons(offset){
apps.forEach((app, i) => {
const x = getPosition(i) + HALF - offset;
const y = HALF - (HALF*0.3);//-(HALF*0.7);
let diff = (x - HALF);
if(diff < 0) diff *=-1;
const APPS = getApps();
const dontRender = x+(HALF/2)<0 || x-(HALF/2)>120;
if(dontRender) {
delete icons[app.name];
function noIcon(x, y, scale){
if(scale < 0.2) return;
g.setColor(scale, scale, scale);
g.setFontAlign(0,0);
g.setFont('6x8',settings.highres ? 6:3);
g.drawString('x_x', x+1.5, y);
const h = (ORIGINAL_ICON_SIZE/3);
g.drawRect(x-h, y-h, x+h, y+h);
}
function render(){
const start = Date.now();
const ANIMATION_FRAME = settings.frame;
const ANIMATION_STEP = Math.floor(HALF / ANIMATION_FRAME);
const THRESHOLD = ANIMATION_STEP - 1;
g.clear();
const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF );
visibleApps.forEach(app => {
const x = app.x+HALF-STATE.offset;
const y = HALF - (HALF*0.3);
let dist = HALF - x;
if(dist < 0) dist *= -1;
const scale = 1 - (dist / HALF);
if(!scale) return;
if(app.special){
const font = settings.highres ? '6x8' : '4x6';
const fontSize = settings.highres ? 2 : 1;
g.setFont(font, fontSize);
g.setColor(scale,scale,scale);
g.setFontAlign(0,0);
g.drawString(app.name, HALF, HALF);
return;
}
let size = 30;
if((diff*0.5) < size) size -= (diff*0.5);
else size = 0;
const scale = size / 30;
if(size){
let c = size / 30 * 2;
c = c -1;
if(c < 0) c = 0;
//draw icon
const icon = app.icon ?
icons[app.name] ? icons[app.name] : Storage.read(app.icon)
: null;
if(app.back){
g.setFont('6x8', 1);
g.setFontAlign(0, -1);
g.setColor(c,c,c);
g.drawString('Back', HALF, HALF);
return;
if(icon){
icons[app.name] = icon;
try {
const rescale = settings.highres ? scale*ORIGINAL_ICON_SIZE : (scale*(ORIGINAL_ICON_SIZE/2));
const imageScale = settings.highres ? scale*2 : scale;
g.drawImage(icon, x-rescale, y-rescale, { scale: imageScale });
} catch(e){
noIcon(x, y, scale);
}
// icon
const icon = app.icon ?
icons[app.name] ? icons[app.name] : Storage.read(app.icon)
: null;
if(icon){
icons[app.name] = icon;
try {
g.drawImage(icon, x-(scale*24), y-(scale*24), { scale: scale });
} catch(e){
noIcon(x, y, size);
}
}else{
noIcon(x, y, size);
}
//text
g.setFont('6x8', 1);
g.setFontAlign(0, -1);
g.setColor(c,c,c);
g.drawString(app.name, HALF, HEIGHT - (HALF*0.7));
const type = app.type ? app.type : 'App';
const version = app.version ? app.version : '0.00';
const info = type+' v'+version;
g.setFontAlign(0,1);
g.setFont('4x6', 0.25);
g.setColor(c,c,c);
g.drawString(info, HALF, 110, { scale: scale });
}else{
noIcon(x, y, scale);
}
});
}
function draw(ignoreLoop){
g.setColor(0,0,0);
g.fillRect(0,0,WIDTH,HEIGHT);
drawIcons(slideOffset);
//draw text
g.setColor(scale,scale,scale);
if(scale > 0.1){
const font = settings.highres ? '6x8': '4x6';
const fontSize = settings.highres ? 2 : 1;
g.setFont(font, fontSize);
g.setFontAlign(0,0);
g.drawString(app.name, HALF, HEIGHT/4*3);
}
if(settings.highres){
const type = app.type ? app.type : 'App';
const version = app.version ? app.version : '0.00';
const info = type+' v'+version;
g.setFontAlign(0,1);
g.setFont('6x8', 1.5);
g.setColor(scale,scale,scale);
g.drawString(info, HALF, 215, { scale: scale });
}
});
const duration = Math.floor(Date.now()-start);
if(settings.debug){
g.setFontAlign(0,1);
g.setColor(0, 1, 0);
const fontSize = settings.highres ? 2 : 1;
g.setFont('4x6',fontSize);
g.drawString('Render: '+duration+'ms', HALF, HEIGHT);
}
g.flip();
if(slideOffset == target) return;
if(slideOffset < target) slideOffset+= ANIMATION_STEP;
else if(slideOffset > target) slideOffset -= ANIMATION_STEP;
if(!ignoreLoop) draw();
if(STATE.offset == STATE.target) return;
if(STATE.offset < STATE.target) STATE.offset += ANIMATION_STEP;
else if(STATE.offset > STATE.target) STATE.offset -= ANIMATION_STEP;
if(STATE.offset >= STATE.target-THRESHOLD && STATE.offset < STATE.target) STATE.offset = STATE.target;
if(STATE.offset <= STATE.target+THRESHOLD && STATE.offset > STATE.target) STATE.offset = STATE.target;
setTimeout(render, 0);
}
function animateTo(index){
target = getPosition(index);
draw();
}
function goTo(index){
current_app = index;
target = getPosition(index);
slideOffset = target;
draw(true);
STATE.index = index;
STATE.target = getPosition(index);
render();
}
goTo(1);
function jumpTo(index){
STATE.index = index;
STATE.target = getPosition(index);
STATE.offset = STATE.target;
render();
}
function prev(){
if(current_app == 0) goTo(apps.length-1);
current_app -= 1;
if(current_app < 0) current_app = 0;
animateTo(current_app);
if(STATE.settings_open) return;
if(STATE.index == 0) jumpTo(APPS.length-1);
setTimeout(() => {
if(!settings.animation) jumpTo(STATE.index-1);
else animateTo(STATE.index-1);
},1);
}
function next(){
if(current_app == apps.length-1) goTo(0);
current_app += 1;
if(current_app > apps.length-1) current_app = apps.length-1;
animateTo(current_app);
if(STATE.settings_open) return;
if(STATE.index == APPS.length-1) jumpTo(0);
setTimeout(() => {
if(!settings.animation) jumpTo(STATE.index+1);
else animateTo(STATE.index+1);
},1);
}
function run() {
const app = apps[current_app];
if(app.back) return load();
function run(){
const app = APPS[STATE.index];
if(app.name == 'Exit') return load();
if (Storage.read(app.src)===undefined) {
E.showMessage("App Source\nNot found");
setTimeout(draw, 2000);
setTimeout(render, 2000);
} else {
Bangle.setLCDMode();
g.clear();
@ -162,15 +208,12 @@ function run() {
E.showMessage("Loading...");
load(app.src);
}
}
setWatch(prev, BTN1, { repeat: true });
setWatch(next, BTN3, { repeat: true });
setWatch(run, BTN2, {repeat:true,edge:"falling"});
// Screen event
Bangle.on('touch', function(button){
if(STATE.settings_open) return;
switch(button){
case 1:
prev();
@ -185,6 +228,7 @@ Bangle.on('touch', function(button){
});
Bangle.on('swipe', dir => {
if(STATE.settings_open) return;
if(dir == 1) prev();
else next();
});
@ -193,3 +237,10 @@ Bangle.on('swipe', dir => {
Bangle.on('lcdPower', on => {
if(!on) return load();
});
setWatch(prev, BTN1, { repeat: true });
setWatch(next, BTN3, { repeat: true });
setWatch(run, BTN2, { repeat:true });
jumpTo(1);

59
apps/toucher/settings.js Normal file
View File

@ -0,0 +1,59 @@
(function(back) {
const Storage = require("Storage");
const filename = 'toucher.json';
let settings = Storage.readJSON(filename,1)|| null;
function getSettings(){
return {
highres: true,
animation : true,
frame : 3,
debug: true
};
}
function updateSettings() {
require("Storage").writeJSON(filename, settings);
Bangle.buzz();
}
if(!settings){
settings = getSettings();
updateSettings();
}
function saveChange(name){
return function(v){
settings[name] = v;
updateSettings();
}
}
E.showMenu({
'': { 'title': 'Toucher settings' },
"Resolution" : {
value : settings.highres,
format : v => v?"High":"Low",
onchange: v => {
saveChange('highres')(!settings.highres);
}
},
"Animation" : {
value : settings.animation,
format : v => v?"On":"Off",
onchange : saveChange('animation')
},
"Frame rate" : {
value : settings.frame,
min: 1, max: 10, step: 1,
onchange : saveChange('frame')
},
"Debug" : {
value : settings.debug,
format : v => v?"On":"Off",
onchange : saveChange('debug')
},
'< Back': back
});
});

54
apps/weather/app.js Normal file
View File

@ -0,0 +1,54 @@
(() => {
function draw(w) {
g.reset();
g.setColor(0).fillRect(0, 24, 239, 239);
require('weather').drawIcon(w.txt, 65, 90, 55);
const locale = require("locale");
g.setColor(-1);
const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/);
let width = g.setFont("Vector", 40).stringWidth(temp[1]);
width += g.setFont("Vector", 20).stringWidth(temp[2]);
g.setFont("Vector", 40).setFontAlign(-1, -1, 0);
g.drawString(temp[1], 180-width/2, 70);
g.setFont("Vector", 20).setFontAlign(1, -1, 0);
g.drawString(temp[2], 180+width/2, 70);
g.setFont("6x8", 1);
g.setFontAlign(-1, 0, 0);
g.drawString("Humidity", 135, 130);
g.drawString("Wind", 135, 142);
g.setFontAlign(1, 0, 0);
g.drawString(w.hum+"%", 225, 130);
g.drawString(locale.speed(w.wind), 225, 142);
g.setFont("6x8", 2).setFontAlign(0, 0, 0);
g.drawString(w.loc, 120, 170);
g.setFont("6x8", 1).setFontAlign(0, 0, 0);
g.drawString(w.txt.charAt(0).toUpperCase()+w.txt.slice(1), 120, 190);
g.flip();
}
const _GB = global.GB;
global.GB = (event) => {
if (event.t==="weather") draw(event);
if (_GB) setTimeout(_GB, 0, event);
};
Bangle.loadWidgets();
Bangle.drawWidgets();
const weather = require('weather').load();
if (weather) {
draw(weather);
} else {
E.showMessage('Weather unknown\n\nIs Gadgetbridge\nconnected?');
}
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'})
})()

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AE8N6AXV7vdFyoXBGCQUBAAoXp73u93tC6YWBAAIXSFwQwDRiAWDGASSBmYABIx5IDgYXCmC7KCoYRBnvUCwQwKC4gSEAAgwKC5gwKCwPjC6inBC6owBC6wVKPBXd6YXMDBAuNJJQXWfwZITC/6QIBw073ezR6m73anOJAwuHeBAYFIw4WJAAsL3YQOAAxeBCiWIC4e72AJChGACpMIxAXBIwIwEBIIXKBgouDEIYuLC4ghEC6ELLoYXPU4YXFLxQNBBgcLEQqqQFwYA/AH4AYA=="))

BIN
apps/weather/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

176
apps/weather/lib.js Normal file
View File

@ -0,0 +1,176 @@
exports = {
save: weather => {
let json = require('Storage').readJSON('weather.json')||{}
json.weather = Object.assign({}, weather) // don't mutate GB events
delete json.weather.t // don't save the event type (if present)
require('Storage').write('weather.json', json)
},
load: () => {
let json = require('Storage').readJSON('weather.json')||{}
return json.weather
},
drawIcon: (cond, x, y, r) => {
function drawSun(x, y, r) {
g.setColor("#FF7700");
g.fillCircle(x, y, r);
}
function drawCloud(x, y, r, c) {
const u = r/12;
if (c==null) c = "#EEEEEE";
g.setColor(c);
g.fillCircle(x-8*u, y+3*u, 4*u);
g.fillCircle(x-4*u, y-2*u, 5*u);
g.fillCircle(x+4*u, y+0*u, 4*u);
g.fillCircle(x+9*u, y+4*u, 3*u);
g.fillPoly([
x-8*u, y+7*u,
x-8*u, y+3*u,
x-4*u, y-2*u,
x+4*u, y+0*u,
x+9*u, y+4*u,
x+9*u, y+7*u,
]);
}
function drawBrokenClouds(x, y, r) {
drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777777");
drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
}
function drawFewClouds(x, y, r) {
drawSun(x+3/8*r, y-1/8*r, 5/8*r);
drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
}
function drawRainLines(x, y, r) {
g.setColor("#FFFFFF");
const y1 = y+1/2*r;
const y2 = y+1*r;
g.fillPoly([
x-6/12*r+1, y1,
x-8/12*r+1, y2,
x-7/12*r, y2,
x-5/12*r, y1,
]);
g.fillPoly([
x-2/12*r+1, y1,
x-4/12*r+1, y2,
x-3/12*r, y2,
x-1/12*r, y1,
]);
g.fillPoly([
x+2/12*r+1, y1,
x+0/12*r+1, y2,
x+1/12*r, y2,
x+3/12*r, y1,
]);
}
function drawShowerRain(x, y, r) {
drawFewClouds(x, y-1/3*r, r);
drawRainLines(x, y, r);
}
function drawRain(x, y, r) {
drawBrokenClouds(x, y-1/3*r, r);
drawRainLines(x, y, r);
}
function drawThunderstorm(x, y, r) {
function drawLightning(x, y, r) {
g.setColor("#FF7700");
g.fillPoly([
x-2/6*r, y-r,
x-4/6*r, y+1/6*r,
x-1/6*r, y+1/6*r,
x-3/6*r, y+1*r,
x+3/6*r, y-1/6*r,
x+0/6*r, y-1/6*r,
x+3/6*r, y-r,
]);
}
drawBrokenClouds(x, y-1/3*r, r);
drawLightning(x-1/12*r, y+1/2*r, 1/2*r);
}
function drawSnow(x, y, r) {
function rotatePoints(points, pivotX, pivotY, angle) {
for(let i = 0; i<points.length; i += 2) {
const x = points[i];
const y = points[i+1];
points[i] = Math.cos(angle)*(x-pivotX)-Math.sin(angle)*(y-pivotY)+
pivotX;
points[i+1] = Math.sin(angle)*(x-pivotX)+Math.cos(angle)*(y-pivotY)+
pivotY;
}
}
g.setColor("#FFFFFF");
const w = 1/12*r;
for(let i = 0; i<=6; ++i) {
const points = [
x+w, y,
x-w, y,
x-w, y+r,
x+w, y+r,
];
rotatePoints(points, x, y, i/3*Math.PI);
g.fillPoly(points);
for(let j = -1; j<=1; j += 2) {
const points = [
x+w, y+7/12*r,
x-w, y+7/12*r,
x-w, y+r,
x+w, y+r,
];
rotatePoints(points, x, y+7/12*r, j/3*Math.PI);
rotatePoints(points, x, y, i/3*Math.PI);
g.fillPoly(points);
}
}
}
function drawMist(x, y, r) {
const layers = [
[-0.4, 0.5],
[-0.8, 0.3],
[-0.2, 0.9],
[-0.9, 0.7],
[-0.2, 0.3],
];
g.setColor("#FFFFFF");
for(let i = 0; i<5; ++i) {
g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r,
y+(0.4*i-0.7)*r-1);
g.fillCircle(x+layers[i][0]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
g.fillCircle(x+layers[i][1]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
}
}
function chooseIcon(condition) {
if (!condition) return () => {};
condition = condition.toLowerCase();
if (condition.includes("thunderstorm")) return drawThunderstorm;
if (condition.includes("freezing")||condition.includes("snow")||
condition.includes("sleet")) {
return drawSnow;
}
if (condition.includes("drizzle")||
condition.includes("shower")) {
return drawRain;
}
if (condition.includes("rain")) return drawShowerRain;
if (condition.includes("clear")) return drawSun;
if (condition.includes("few clouds")) return drawFewClouds;
if (condition.includes("scattered clouds")) return drawCloud;
if (condition.includes("clouds")) return drawBrokenClouds;
return drawMist;
}
chooseIcon(cond)(x, y, r)
},
}

16
apps/weather/readme.md Normal file
View File

@ -0,0 +1,16 @@
# Weather
Shows Gadgetbridge weather reports.
This adds a widget with a weather pictogram and the temperature.
You can view the full report through the app:
![Screenshot](screenshot.png)
## Setup
See [this guide](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather)
to setup Gadgetbridge weather reporting.
## Controls
BTN2: opens the launcher

BIN
apps/weather/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

40
apps/weather/widget.js Normal file
View File

@ -0,0 +1,40 @@
(() => {
function draw() {
const w = require('weather').load()
if (!w) return;
g.reset();
g.setColor(0).fillRect(this.x, this.y, this.x+this.width, this.y+24)
if (w.txt) {
require('weather').drawIcon(w.txt, this.x+10, this.y+8, 8);
}
if (w.temp) {
let t = require('locale').temp(w.temp-273.15); // applies conversion
t = t.substr(0, t.length-2); // but we have no room for units
g.setFontAlign(0, 1); // center horizontally at bottom of widget
g.setFont('6x8', 1);
g.setColor(-1)
g.drawString(t, this.x+10, this.y+24)
}
}
function update(weather) {
require('weather').save(weather);
if (!WIDGETS["weather"].width) {
WIDGETS["weather"].width = 20
Bangle.drawWidgets()
} else if (Bangle.isLCDOn()) {
WIDGETS["weather"].draw()
}
}
const _GB = global.GB;
global.GB = (event) => {
if (event.t==="weather") update(event);
if (_GB) setTimeout(_GB, 0, event);
};
WIDGETS["weather"] = {area: "tl", width: 20, draw: draw};
if (!require('weather').load()) {
WIDGETS["weather"].width = 0
}
})();

View File

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

View File

@ -1,11 +1,11 @@
(function() {
let s = require('Storage').readJSON('welcome.settings.json', 1)
let s = require('Storage').readJSON('welcome.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')) {
setTimeout(() => {
s.welcomed = true
require('Storage').write('welcome.settings.json', {welcomed: "yes"})
require('Storage').write('welcome.json', {welcomed: "yes"})
load('welcome.app.js')
})
}

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
* @param {function} back Use back() to return to settings menu
*/
(function(back) {
const SETTINGS_FILE = 'widbatpc.settings.json'
const SETTINGS_FILE = 'widbatpc.json'
const COLORS = ['By Level', 'Green', 'Monochrome']
// initialize with default settings...
@ -11,21 +11,22 @@
'color': COLORS[0],
'percentage': true,
'charger': true,
'hideifmorethan': 100,
}
// ...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]
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["batpc"].reload()
s[key] = value;
storage.write(SETTINGS_FILE, s);
WIDGETS["batpc"].reload();
}
}
@ -51,8 +52,16 @@
const newIndex = (oldIndex + 1) % COLORS.length
s.color = COLORS[newIndex]
save('color')(s.color)
},
},
}
}
},
'Hide if >': {
value: s.hideifmorethan||100,
min: 10,
max : 100,
step: 10,
format: x => x+"%",
onchange: save('hideifmorethan'),
},
}
E.showMenu(menu)
})

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