diff --git a/.gitignore b/.gitignore
index b83632eaa..f1811806d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.htaccess
node_modules
package-lock.json
+.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95e973e0f..12b675e3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,3 +10,5 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* Add `Favourite` functionality
* Version number now clickable even when you're at the latest version (fix #291)
* Rewrite 'getInstalledApps' to minimize RAM usage
+* Added code to handle Settings
+* Added espruinotools.js for pretokenisation
diff --git a/apps.json b/apps.json
index cc8fc25e6..e9e38a1a1 100644
--- a/apps.json
+++ b/apps.json
@@ -2,7 +2,7 @@
{ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
- "version":"0.14",
+ "version":"0.15",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"tags": "tool,system",
"type":"bootloader",
@@ -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,
@@ -108,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",
@@ -122,9 +122,10 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
- "version":"0.18",
+ "version":"0.19",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
+ "readme": "README.md",
"storage": [
{"name":"setting.app.js","url":"settings.js"},
{"name":"setting.boot.js","url":"boot.js"},
@@ -163,10 +164,23 @@
{"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",
- "version": "0.11",
+ "version": "0.12",
"description": "An Analog Clock",
"tags": "clock",
"type":"clock",
@@ -303,6 +317,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},
@@ -348,6 +363,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",
@@ -434,7 +466,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",
@@ -449,7 +481,7 @@
"name": "Bluetooth Music Controls",
"shortName": "Music Control",
"icon": "hid-music.png",
- "version":"0.01",
+ "version":"0.02",
"description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!",
"tags": "bluetooth",
"storage": [
@@ -461,7 +493,7 @@
"name": "Bluetooth Keyboard",
"shortName": "Bluetooth Kbd",
"icon": "hid-keyboard.png",
- "version":"0.01",
+ "version":"0.02",
"description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps",
"tags": "bluetooth",
"storage": [
@@ -473,7 +505,7 @@
"name": "Binary Bluetooth Keyboard",
"shortName": "Binary BT Kbd",
"icon": "hid-binary-keyboard.png",
- "version":"0.01",
+ "version":"0.02",
"description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want",
"tags": "bluetooth",
"storage": [
@@ -903,8 +935,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"},
@@ -994,7 +1026,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",
@@ -1074,14 +1106,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
},
@@ -1126,7 +1162,7 @@
{ "id": "minionclk",
"name": "Minion clock",
"icon": "minionclk.png",
- "version": "0.01",
+ "version": "0.02",
"description": "Minion themed clock.",
"tags": "clock,minion",
"type": "clock",
@@ -1153,22 +1189,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",
@@ -1219,7 +1255,7 @@
"name": "Battery Chart",
"shortName":"Battery Chart",
"icon": "app.png",
- "version":"0.08",
+ "version":"0.09",
"readme": "README.md",
"description": "A widget and an app for recording and visualizing battery percentage over time.",
"tags": "app,widget,battery,time,record,chart,tool",
@@ -1321,6 +1357,28 @@
}
]
},
+ {
+ "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",
@@ -1347,6 +1405,7 @@
"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,
@@ -1379,9 +1438,10 @@
"name": "Camera shutter",
"shortName":"Cam shutter",
"icon": "app.png",
- "version":"0.01",
+ "version":"0.03",
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
- "tags": "tools",
+ "readme": "README.md",
+ "tags": "bluetooth,tool",
"storage": [
{"name":"hidcam.app.js","url":"app.js"},
{"name":"hidcam.img","url":"app-icon.js","evaluate":true}
@@ -1426,5 +1486,69 @@
{"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"}
+ ]
+ },
+ {
+ "id": "calendar",
+ "name": "Calendar",
+ "icon": "calendar.png",
+ "version": "0.01",
+ "description": "Simple calendar",
+ "tags": "calendar",
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "calendar.app.js",
+ "url": "calendar.js"
+ },
+ {
+ "name": "calendar.img",
+ "url": "calendar-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ { "id": "hidjoystick",
+ "name": "Bluetooth Joystick",
+ "shortName": "Joystick",
+ "icon": "app.png",
+ "version":"0.01",
+ "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.",
+ "tags": "bluetooth",
+ "storage": [
+ {"name":"hidjoystick.app.js","url":"app.js"},
+ {"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
+ ]
}
]
diff --git a/apps/_example_app/README.md b/apps/_example_app/README.md
new file mode 100644
index 000000000..dc139bc9a
--- /dev/null
+++ b/apps/_example_app/README.md
@@ -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 
+
+## 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
diff --git a/apps/_example_app/add_to_apps.json b/apps/_example_app/add_to_apps.json
index ca75a7bd8..bb0377b66 100644
--- a/apps/_example_app/add_to_apps.json
+++ b/apps/_example_app/add_to_apps.json
@@ -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}
diff --git a/apps/_example_widget/README.md b/apps/_example_widget/README.md
new file mode 100644
index 000000000..a909e9e7e
--- /dev/null
+++ b/apps/_example_widget/README.md
@@ -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 
+
+## 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
diff --git a/apps/_example_widget/add_to_apps.json b/apps/_example_widget/add_to_apps.json
index 5d0057960..527c698a0 100644
--- a/apps/_example_widget/add_to_apps.json
+++ b/apps/_example_widget/add_to_apps.json
@@ -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"}
]
diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog
index 2c81c0537..16aea0610 100644
--- a/apps/about/ChangeLog
+++ b/apps/about/ChangeLog
@@ -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
diff --git a/apps/about/app.js b/apps/about/app.js
index dc7b0cad8..57c85563d 100644
--- a/apps/about/app.js
+++ b/apps/about/app.js
@@ -29,5 +29,5 @@ g.drawString(NRF.getAddress(),120,232);
g.flip();
// Pixel chooser image
-g.drawImage(require("heatshrink").decompress(atob("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")),0,135);
+g.drawImage(require("heatshrink").decompress(atob("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")),0,135);
g.flip();
diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog
index 98e3da8e7..a289fba1f 100644
--- a/apps/aclock/ChangeLog
+++ b/apps/aclock/ChangeLog
@@ -6,3 +6,4 @@
0.09: center date, remove box around it, internal refactor to remove redundant code.
0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed
0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date
+0.12: Fix regression after 0.11
diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js
index 7b60a728f..8cd0af915 100644
--- a/apps/aclock/clock-analog.js
+++ b/apps/aclock/clock-analog.js
@@ -1,7 +1,3 @@
-// eliminate ide undefined errors
-let g;
-let Bangle;
-
// http://forum.espruino.com/conversations/345155/#comment15172813
const locale = require('locale');
const p = Math.PI / 2;
diff --git a/apps/activepedom/ChangeLog b/apps/activepedom/ChangeLog
index fb0bc78e5..ca26a648a 100644
--- a/apps/activepedom/ChangeLog
+++ b/apps/activepedom/ChangeLog
@@ -1,2 +1,4 @@
0.01: New Widget!
-0.02: Distance calculation and display
\ No newline at end of file
+0.02: Distance calculation and display
+0.03: Data logging and display
+0.04: Steps are set to 0 in log on new day
\ No newline at end of file
diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md
index 055a91f56..a2a351a12 100644
--- a/apps/activepedom/README.md
+++ b/apps/activepedom/README.md
@@ -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

@@ -16,7 +18,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c
* 10600 steps

-## 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
diff --git a/apps/activepedom/app.js b/apps/activepedom/app.js
new file mode 100644
index 000000000..cc875f371
--- /dev/null
+++ b/apps/activepedom/app.js
@@ -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();
+
+})();
\ No newline at end of file
diff --git a/apps/activepedom/widget.js b/apps/activepedom/widget.js
index d569716ec..2ae1b9b62 100644
--- a/apps/activepedom/widget.js
+++ b/apps/activepedom/widget.js
@@ -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};
-
})();
\ No newline at end of file
diff --git a/apps/ballmaze/README.md b/apps/ballmaze/README.md
new file mode 100644
index 000000000..22a295686
--- /dev/null
+++ b/apps/ballmaze/README.md
@@ -0,0 +1,15 @@
+# Ball Maze
+
+Navigate a ball through a maze by tilting your watch.
+
+
+
+
+## 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
diff --git a/apps/ballmaze/app.js b/apps/ballmaze/app.js
new file mode 100644
index 000000000..3e26277b7
--- /dev/null
+++ b/apps/ballmaze/app.js
@@ -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 ((col0) && !linked(row-1, col)) sides.push(TOP);
+ if ((row0 && !(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 (cold+1) {
+ dist[i+1] = d+1;
+ todo.push(i+1);
+ }
+ if (rowd+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 (col0 && !(walls&TOP) && dist[i-cols]0 && !(walls&LEFT) && dist[i-1] {
+ 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 {
+ // remember chosen size
+ settings.size = size;
+ require("Storage").write("ballmaze.json", settings);
+ playMaze(size);
+ };
+ });
+ menu["< Exit"] = () => load();
+ showMenu(menu);
+ }
+
+ startGame();
+})();
diff --git a/apps/ballmaze/icon.js b/apps/ballmaze/icon.js
new file mode 100644
index 000000000..10b5a502e
--- /dev/null
+++ b/apps/ballmaze/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA"))
diff --git a/apps/ballmaze/icon.png b/apps/ballmaze/icon.png
new file mode 100644
index 000000000..44697db4b
Binary files /dev/null and b/apps/ballmaze/icon.png differ
diff --git a/apps/ballmaze/maze.png b/apps/ballmaze/maze.png
new file mode 100644
index 000000000..7bda56d9b
Binary files /dev/null and b/apps/ballmaze/maze.png differ
diff --git a/apps/ballmaze/size_select.png b/apps/ballmaze/size_select.png
new file mode 100644
index 000000000..cac278820
Binary files /dev/null and b/apps/ballmaze/size_select.png differ
diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog
index 2e0fd088c..616ee66e9 100644
--- a/apps/barclock/ChangeLog
+++ b/apps/barclock/ChangeLog
@@ -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
\ No newline at end of file
diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js
index da436daee..0f2609298 100644
--- a/apps/barclock/clock-bar.js
+++ b/apps/barclock/clock-bar.js
@@ -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(),
diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog
index 439d877be..66b40fbbf 100644
--- a/apps/batchart/ChangeLog
+++ b/apps/batchart/ChangeLog
@@ -5,4 +5,5 @@
0.05: Display temperature and LCD state in chart
0.06: Fixes widget events and charting of component states
0.07: Improve logging and charting of component states and add widget icon
-0.08: Fix for Home button in the app and README added.
\ No newline at end of file
+0.08: Fix for Home button in the app and README added.
+0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state
\ No newline at end of file
diff --git a/apps/batchart/app.js b/apps/batchart/app.js
index 2d0d8e585..472fb3a8a 100644
--- a/apps/batchart/app.js
+++ b/apps/batchart/app.js
@@ -8,7 +8,7 @@ const GraphXMax = GraphXZero + MaxValueCount;
const GraphLcdY = GraphYZero + 10;
const GraphCompassY = GraphYZero + 16;
-// const GraphBluetoothY = GraphYZero + 22;
+const GraphBluetoothY = GraphYZero + 22;
const GraphGpsY = GraphYZero + 28;
const GraphHrmY = GraphYZero + 34;
@@ -175,13 +175,13 @@ function renderData(dataArray) {
g.drawLine(GraphXZero + i, GraphCompassY, GraphXZero + i, GraphCompassY + 1);
}
- // // Bluetooth state
- // if (switchables & switchableConsumers.lcd == switchableConsumers.lcd) {
- // g.setColor(0, 0, 1);
- // g.setFontAlign(1, -1, 0);
- // g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true);
- // g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1);
- // }
+ // Bluetooth state
+ if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.bluetooth) {
+ g.setColor(0, 0, 1);
+ g.setFontAlign(1, -1, 0);
+ g.drawString("BLE", GraphXZero - GraphMarkerOffset, GraphBluetoothY - 2, true);
+ g.drawLine(GraphXZero + i, GraphBluetoothY, GraphXZero + i, GraphBluetoothY + 1);
+ }
// Gps state
if (parseInt(dataInfo[switchabelsIndex]) & switchableConsumers.gps) {
diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js
index 1b8ce79ba..96f8b4b25 100644
--- a/apps/batchart/widget.js
+++ b/apps/batchart/widget.js
@@ -71,8 +71,10 @@
enabledConsumers = enabledConsumers | switchableConsumers.gps;
if (hrmEventReceived)
enabledConsumers = enabledConsumers | switchableConsumers.hrm;
- //if (Bangle.isBluetoothOn())
- // enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
+
+ // Very coarse first approach to check if the BLE device is on.
+ if (NRF.getSecurityStatus().connected)
+ enabledConsumers = enabledConsumers | switchableConsumers.bluetooth;
// Reset the event registration vars
compassEventReceived = false;
@@ -110,19 +112,14 @@
}
function reload() {
- WIDGETS.batchart.width = 24;
+ WIDGETS["batchart"].width = 24;
recordingInterval = setInterval(logBatteryData, recordingInterval10Min);
-
- logBatteryData();
}
// add the widget
- WIDGETS.batchart = {
- area: "tl", width: 24, draw: draw, reload: function () {
- reload();
- Bangle.drawWidgets();
- }
+ WIDGETS["batchart"] = {
+ area: "tl", width: 24, draw: draw, reload: reload
};
reload();
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index 7ab79a5a5..cf5c243f8 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -13,3 +13,4 @@
0.13: Now automatically load *.boot.js at startup
Move alarm code into alarm.boot.js
0.14: Move welcome loaders to *.boot.js
+0.15: Added BLE HID option for Joystick and bare Keyboard
diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js
index dd3b3a9ba..84b3460e1 100644
--- a/apps/boot/boot0.js
+++ b/apps/boot/boot0.js
@@ -4,7 +4,9 @@ E.setFlags({pretokenise:1});
var s = require('Storage').readJSON('setting.json',1)||{};
if (s.ble!==false) {
if (s.HID) { // Human interface device
- Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
+ if (s.HID=="joy") Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));
+ else if (s.HID=="kb") Bangle.HID = E.toUint8Array(atob("BQEJBqEBBQcZ4CnnFQAlAXUBlQiBApUBdQiBAZUFdQEFCBkBKQWRApUBdQORAZUGdQgVACVzBQcZAClzgQAJBRUAJv8AdQiVArECwA=="));
+ else /*kbmedia*/Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
NRF.setServices({}, {uart:true, hid:Bangle.HID});
}
}
diff --git a/apps/boot/hid_info.txt b/apps/boot/hid_info.txt
new file mode 100644
index 000000000..873b50f63
--- /dev/null
+++ b/apps/boot/hid_info.txt
@@ -0,0 +1,88 @@
+
+## Joystick:
+
+https://github.com/espruino/BangleApps/issues/349#issuecomment-620231524
+
+```
+0x05, 0x01, // Usage Page (Generic Desktop)
+0x09, 0x04, // Usage (Joystick)
+0xA1, 0x01, // Collection (Application)
+ 0x09, 0x01, // Usage (Pointer)
+ 0xA1, 0x00, // Collection (Physical)
+ // Buttons
+ 0x05, 0x09, // Usage Page (Buttons)
+ 0x19, 0x01, // Usage Minimum (1)
+ 0x29, 0x05, // Usage Maximum (5)
+ 0x15, 0x00, // Logical Minimum (0)
+ 0x25, 0x01, // Logical Maximum (1)
+ 0x95, 0x05, // Report Count (5)
+ 0x75, 0x01, // Report Size (1)
+ 0x81, 0x02, // Input (Data, Variable, Absolute)
+
+ // padding bits
+ 0x95, 0x03, // Report Count (3)
+ 0x75, 0x01, // Report Size (1)
+ 0x81, 0x03, // Input (Constant)
+
+ // Stick
+ 0x05, 0x01, // Usage Page (Generic Desktop)
+ 0x09, 0x30, // Usage (X)
+ 0x09, 0x31, // Usage (Y)
+ 0x15, 0x81, // Logical Minimum (-127)
+ 0x25, 0x7f, // Logical Maximum (127)
+ 0x75, 0x08, // Report Size (8)
+ 0x95, 0x02, // Report Count (2)
+ 0x81, 0x02, // Input (Data, Variable, Absolute)
+ 0xC0, // End Collection (Physical)
+0xC0 // End Collection (Application)
+```
+
+## Keyboard
+
+http://www.espruino.com/BLE+Keyboard
+
+```
+0x05, 0x01, // Usage Page (Generic Desktop)
+0x09, 0x06, // Usage (Keyboard)
+0xA1, 0x01, // Collection (Application)
+0x05, 0x07, // Usage Page (Key Codes)
+0x19, 0xe0, // Usage Minimum (224)
+0x29, 0xe7, // Usage Maximum (231)
+0x15, 0x00, // Logical Minimum (0)
+0x25, 0x01, // Logical Maximum (1)
+0x75, 0x01, // Report Size (1)
+0x95, 0x08, // Report Count (8)
+0x81, 0x02, // Input (Data, Variable, Absolute)
+
+0x95, 0x01, // Report Count (1)
+0x75, 0x08, // Report Size (8)
+0x81, 0x01, // Input (Constant) reserved byte(1)
+
+0x95, 0x05, // Report Count (5)
+0x75, 0x01, // Report Size (1)
+0x05, 0x08, // Usage Page (Page# for LEDs)
+0x19, 0x01, // Usage Minimum (1)
+0x29, 0x05, // Usage Maximum (5)
+0x91, 0x02, // Output (Data, Variable, Absolute), Led report
+0x95, 0x01, // Report Count (1)
+0x75, 0x03, // Report Size (3)
+0x91, 0x01, // Output (Data, Variable, Absolute), Led report padding
+
+0x95, 0x06, // Report Count (6)
+0x75, 0x08, // Report Size (8)
+0x15, 0x00, // Logical Minimum (0)
+0x25, 0x73, // Logical Maximum (115 - include F13, etc)
+0x05, 0x07, // Usage Page (Key codes)
+0x19, 0x00, // Usage Minimum (0)
+0x29, 0x73, // Usage Maximum (115 - include F13, etc)
+0x81, 0x00, // Input (Data, Array) Key array(6 bytes)
+
+0x09, 0x05, // Usage (Vendor Defined)
+0x15, 0x00, // Logical Minimum (0)
+0x26, 0xFF, 0x00, // Logical Maximum (255)
+0x75, 0x08, // Report Count (2)
+0x95, 0x02, // Report Size (8 bit)
+0xB1, 0x02, // Feature (Data, Variable, Absolute)
+
+0xC0 // End Collection (Application)
+```
diff --git a/apps/buffgym/.eslintrc.json b/apps/buffgym/.eslintrc.json
new file mode 100644
index 000000000..c91a72544
--- /dev/null
+++ b/apps/buffgym/.eslintrc.json
@@ -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"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/apps/buffgym/ChangeLog b/apps/buffgym/ChangeLog
new file mode 100644
index 000000000..6efdd865a
--- /dev/null
+++ b/apps/buffgym/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Create BuffGym app
+0.02: Add web interface for personalising workout
diff --git a/apps/buffgym/README.md b/apps/buffgym/README.md
new file mode 100644
index 000000000..bcadf22c4
--- /dev/null
+++ b/apps/buffgym/README.md
@@ -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
+
+
+
+### 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.
+
+
+
+### Recording your training
+
+You will now begin moving through the exercises in the workout. You will see the exercise information on the display.
+
+
+
+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.
+
+
+
+
+### Workout completed
+
+Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are.
+
+
+
+## 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.
diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js
new file mode 100644
index 000000000..ea20aa132
--- /dev/null
+++ b/apps/buffgym/buffgym-exercise.js
@@ -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");
+ }
+}
\ No newline at end of file
diff --git a/apps/buffgym/buffgym-icon.js b/apps/buffgym/buffgym-icon.js
new file mode 100644
index 000000000..523ed35b6
--- /dev/null
+++ b/apps/buffgym/buffgym-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA="))
diff --git a/apps/buffgym/buffgym-scrn1.png b/apps/buffgym/buffgym-scrn1.png
new file mode 100755
index 000000000..07b79386f
Binary files /dev/null and b/apps/buffgym/buffgym-scrn1.png differ
diff --git a/apps/buffgym/buffgym-scrn2.png b/apps/buffgym/buffgym-scrn2.png
new file mode 100755
index 000000000..ec70fb791
Binary files /dev/null and b/apps/buffgym/buffgym-scrn2.png differ
diff --git a/apps/buffgym/buffgym-scrn3.png b/apps/buffgym/buffgym-scrn3.png
new file mode 100755
index 000000000..0888fc507
Binary files /dev/null and b/apps/buffgym/buffgym-scrn3.png differ
diff --git a/apps/buffgym/buffgym-scrn4.png b/apps/buffgym/buffgym-scrn4.png
new file mode 100755
index 000000000..3078d25dc
Binary files /dev/null and b/apps/buffgym/buffgym-scrn4.png differ
diff --git a/apps/buffgym/buffgym-scrn5.png b/apps/buffgym/buffgym-scrn5.png
new file mode 100755
index 000000000..b34a5b124
Binary files /dev/null and b/apps/buffgym/buffgym-scrn5.png differ
diff --git a/apps/buffgym/buffgym-scrn6.png b/apps/buffgym/buffgym-scrn6.png
new file mode 100755
index 000000000..563426ad3
Binary files /dev/null and b/apps/buffgym/buffgym-scrn6.png differ
diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js
new file mode 100644
index 000000000..dc0c05671
--- /dev/null
+++ b/apps/buffgym/buffgym-set.js
@@ -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--;
+ }
+}
\ No newline at end of file
diff --git a/apps/buffgym/buffgym-workout-a.json b/apps/buffgym/buffgym-workout-a.json
new file mode 100644
index 000000000..8eb8611d6
--- /dev/null
+++ b/apps/buffgym/buffgym-workout-a.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/buffgym/buffgym-workout-b.json b/apps/buffgym/buffgym-workout-b.json
new file mode 100644
index 000000000..43845a98b
--- /dev/null
+++ b/apps/buffgym/buffgym-workout-b.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/buffgym/buffgym-workout-index.json b/apps/buffgym/buffgym-workout-index.json
new file mode 100644
index 000000000..af74d5e3b
--- /dev/null
+++ b/apps/buffgym/buffgym-workout-index.json
@@ -0,0 +1,10 @@
+[
+ {
+ "title": "Workout A",
+ "file": "buffgym-workout-a.json"
+ },
+ {
+ "title": "Workout B",
+ "file": "buffgym-workout-b.json"
+ }
+]
\ No newline at end of file
diff --git a/apps/buffgym/buffgym-workout.js b/apps/buffgym/buffgym-workout.js
new file mode 100644
index 000000000..124c27f4b
--- /dev/null
+++ b/apps/buffgym/buffgym-workout.js
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js
new file mode 100755
index 000000000..fc2be83f9
--- /dev/null
+++ b/apps/buffgym/buffgym.app.js
@@ -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();
\ No newline at end of file
diff --git a/apps/buffgym/buffgym.html b/apps/buffgym/buffgym.html
new file mode 100644
index 000000000..3c18932e9
--- /dev/null
+++ b/apps/buffgym/buffgym.html
@@ -0,0 +1,250 @@
+
+
+
+
+
+
BuffGym
+
+ 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.
+
+
+ For more information on how to train this program refer the Stronglifts website
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/buffgym/buffgym.png b/apps/buffgym/buffgym.png
new file mode 100755
index 000000000..9bde64cc4
Binary files /dev/null and b/apps/buffgym/buffgym.png differ
diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog
new file mode 100644
index 000000000..3cf79ffe8
--- /dev/null
+++ b/apps/calendar/ChangeLog
@@ -0,0 +1 @@
+0.01: Basic calendar
diff --git a/apps/calendar/README.md b/apps/calendar/README.md
new file mode 100644
index 000000000..19a60afc0
--- /dev/null
+++ b/apps/calendar/README.md
@@ -0,0 +1,8 @@
+# Calendar
+
+Basic calendar
+
+## Usage
+
+- Use `BTN4` (left screen tap) to go to the previous month
+- Use `BTN5` (right screen tap) to go to the next month
diff --git a/apps/calendar/calendar-icon.js b/apps/calendar/calendar-icon.js
new file mode 100644
index 000000000..ed1bf3667
--- /dev/null
+++ b/apps/calendar/calendar-icon.js
@@ -0,0 +1,5 @@
+require("heatshrink").decompress(
+ atob(
+ "mEwxH+AH4A/ADuIUCARRDhgePCKIv13YAEDoYJFAA4RJFyQvcGBYRGy4dDy4uLCJgv/DoOBDgOBF5oRLF6IeBDgIvNCJYvQDwQuNCJovRADov/F9OsAEgv/F/4vhwIACAqYv/F/4vnd94vvX/4v/F/7vvF96//F/4v/d94v/F/4wsFxQwjFxgA/AH4A/AH4AZA=="
+ )
+)
diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js
new file mode 100644
index 000000000..720986162
--- /dev/null
+++ b/apps/calendar/calendar.js
@@ -0,0 +1,160 @@
+const maxX = 240;
+const maxY = 240;
+const rowN = 7;
+const colN = 7;
+const headerH = maxY / 7;
+const rowH = (maxY - headerH) / rowN;
+const colW = maxX / colN;
+const color1 = "#035AA6";
+const color2 = "#4192D9";
+const color3 = "#026873";
+const color4 = "#038C8C";
+const color5 = "#03A696";
+const black = "#000000";
+const white = "#ffffff";
+const gray1 = "#444444";
+const gray2 = "#888888";
+const gray3 = "#bbbbbb";
+const red = "#d41706";
+
+function drawCalendar(date) {
+ g.setBgColor(color4);
+ g.clearRect(0, 0, maxX, maxY);
+ g.setBgColor(color1);
+ g.clearRect(0, 0, maxX, headerH);
+ g.setBgColor(color2);
+ g.clearRect(0, headerH, maxX, headerH + rowH);
+ g.setBgColor(color3);
+ g.clearRect(colW * 5, headerH + rowH, maxX, maxY);
+ for (let y = headerH; y < maxY; y += rowH) {
+ g.drawLine(0, y, maxX, y);
+ }
+ for (let x = 0; x < maxX; x += colW) {
+ g.drawLine(x, headerH, x, maxY);
+ }
+
+ const month = date.getMonth();
+ const year = date.getFullYear();
+ const monthMap = {
+ 0: "January",
+ 1: "February",
+ 2: "March",
+ 3: "April",
+ 4: "May",
+ 5: "June",
+ 6: "July",
+ 7: "August",
+ 8: "September",
+ 9: "October",
+ 10: "November",
+ 11: "December"
+ };
+ g.setFontAlign(0, 0);
+ g.setFont("6x8", 2);
+ g.setColor(white);
+ g.drawString(`${monthMap[month]} ${year}`, maxX / 2, headerH / 2);
+ g.drawPoly([10, headerH / 2, 20, 10, 20, headerH - 10], true);
+ g.drawPoly(
+ [maxX - 10, headerH / 2, maxX - 20, 10, maxX - 20, headerH - 10],
+ true
+ );
+
+ g.setFont("6x8", 2);
+ const dowLbls = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
+ dowLbls.forEach((lbl, i) => {
+ g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
+ });
+
+ date.setDate(1);
+ const dow = date.getDay();
+ const dowNorm = dow === 0 ? 7 : dow;
+
+ const monthMaxDayMap = {
+ 0: 31,
+ 1: (2020 - year) % 4 === 0 ? 29 : 28,
+ 2: 31,
+ 3: 30,
+ 4: 31,
+ 5: 30,
+ 6: 31,
+ 7: 31,
+ 8: 30,
+ 9: 31,
+ 10: 30,
+ 11: 31
+ };
+
+ let days = [];
+ let nextMonthDay = 1;
+ let thisMonthDay = 51;
+ let prevMonthDay = monthMaxDayMap[month > 0 ? month - 1 : 11] - dowNorm;
+ for (let i = 0; i < colN * (rowN - 1) + 1; i++) {
+ if (i < dowNorm) {
+ days.push(prevMonthDay);
+ prevMonthDay++;
+ } else if (thisMonthDay <= monthMaxDayMap[month] + 50) {
+ days.push(thisMonthDay);
+ thisMonthDay++;
+ } else {
+ days.push(nextMonthDay);
+ nextMonthDay++;
+ }
+ }
+
+ let i = 0;
+ for (y = 0; y < rowN - 1; y++) {
+ for (x = 0; x < colN; x++) {
+ i++;
+ const day = days[i];
+ const isToday =
+ today.year === year && today.month === month && today.day === day - 50;
+ if (isToday) {
+ g.setColor(red);
+ g.drawRect(
+ x * colW,
+ y * rowH + headerH + rowH,
+ x * colW + colW - 1,
+ y * rowH + headerH + rowH + rowH
+ );
+ }
+ g.setColor(day < 50 ? gray3 : white);
+ g.drawString(
+ (day > 50 ? day - 50 : day).toString(),
+ x * colW + colW / 2,
+ headerH + rowH + y * rowH + rowH / 2
+ );
+ }
+ }
+}
+
+const date = new Date();
+const today = {
+ day: date.getDate(),
+ month: date.getMonth(),
+ year: date.getFullYear()
+};
+drawCalendar(date);
+clearWatch();
+setWatch(
+ () => {
+ const month = date.getMonth();
+ const prevMonth = month > 0 ? month - 1 : 11;
+ if (prevMonth === 11) date.setFullYear(date.getFullYear() - 1);
+ date.setMonth(prevMonth);
+ drawCalendar(date);
+ },
+ BTN4,
+ { repeat: true }
+);
+setWatch(
+ () => {
+ const month = date.getMonth();
+ const prevMonth = month < 11 ? month + 1 : 0;
+ if (prevMonth === 0) date.setFullYear(date.getFullYear() + 1);
+ date.setMonth(month + 1);
+ drawCalendar(date);
+ },
+ BTN5,
+ { repeat: true }
+);
+setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
diff --git a/apps/calendar/calendar.png b/apps/calendar/calendar.png
new file mode 100644
index 000000000..056cab3b7
Binary files /dev/null and b/apps/calendar/calendar.png differ
diff --git a/apps/chronowid/ChangeLog b/apps/chronowid/ChangeLog
index 263145407..e173467a1 100644
--- a/apps/chronowid/ChangeLog
+++ b/apps/chronowid/ChangeLog
@@ -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)
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/apps/chronowid/widget.js b/apps/chronowid/widget.js
index 557104d92..0c9366b86 100644
--- a/apps/chronowid/widget.js
+++ b/apps/chronowid/widget.js
@@ -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();
})();
\ No newline at end of file
diff --git a/apps/hidbkbd/ChangeLog b/apps/hidbkbd/ChangeLog
new file mode 100644
index 000000000..459bf40b9
--- /dev/null
+++ b/apps/hidbkbd/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Core functionnality
+0.02: Offer to enable HID if disabled. Handle with/without media keys
diff --git a/apps/hidbkbd/hid-binary-keyboard.js b/apps/hidbkbd/hid-binary-keyboard.js
index fa1017714..81838b42d 100644
--- a/apps/hidbkbd/hid-binary-keyboard.js
+++ b/apps/hidbkbd/hid-binary-keyboard.js
@@ -45,13 +45,7 @@ const KEY = {
0 : 39
};
-function sendHID(code) {
- return new Promise(resolve=>{
- NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
- NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], resolve);
- });
- });
-};
+var sendHID;
function showChars(x,chars) {
var lines = Math.round(Math.sqrt(chars.length)*2);
@@ -103,10 +97,24 @@ function startKeyboardHID() {
}).then(startKeyboardHID);
};
-if (!settings.HID) {
- E.showMessage('HID disabled');
- setTimeout(load, 1000);
-} else {
+if (settings.HID=="kb" || settings.HID=="kbmedia") {
+ if (settings.HID=="kbmedia") {
+ sendHID = function(code) {
+ return new Promise(resolve=>{
+ NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
+ NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], resolve);
+ });
+ });
+ };
+ } else {
+ sendHID = function(code) {
+ return new Promise(resolve=>{
+ NRF.sendHIDReport([0,0,code,0,0,0,0,0], () => {
+ NRF.sendHIDReport([0,0,0,0,0,0,0,0], resolve);
+ });
+ });
+ };
+ }
startKeyboardHID();
setWatch(() => {
sendHID(44); // space
@@ -114,4 +122,12 @@ if (!settings.HID) {
setWatch(() => {
sendHID(40); // enter
}, BTN3, {repeat:true});
+} else {
+ E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
+ if (enable) {
+ settings.HID = "kb";
+ require("Storage").write('setting.json', settings);
+ setTimeout(load, 1000, "hidbkbd.app.js");
+ } else setTimeout(load, 1000);
+ });
}
diff --git a/apps/hidcam/ChangeLog b/apps/hidcam/ChangeLog
index 73b3268b7..480d7d448 100644
--- a/apps/hidcam/ChangeLog
+++ b/apps/hidcam/ChangeLog
@@ -1 +1,3 @@
0.01: Core functionnality
+0.02: Offer to enable HID if disabled
+0.03: Adds Readme and tags to be used by App Loader
diff --git a/apps/hidcam/README.md b/apps/hidcam/README.md
new file mode 100644
index 000000000..5e8d40817
--- /dev/null
+++ b/apps/hidcam/README.md
@@ -0,0 +1,18 @@
+# Camera shutter
+
+Control the camera shutter from your phone using your watch
+
+## Usage
+
+1. In settings, enable HID for "Keyboard & Media".
+2. Pair your watch to your phone.
+3. Load your camera app on your phone.
+4. There you go, launch the app on your watch and press button 2 to trigger the shutter !
+
+## How does it work ?
+
+The app uses HID to send the key "Vol +", which is a shortcut for camera trigger on Android and iOS.
+
+## Creator
+
+Paul Charlet, using code from HID music app.
\ No newline at end of file
diff --git a/apps/hidcam/app.js b/apps/hidcam/app.js
index 89b8ac4a1..adb1a4b29 100644
--- a/apps/hidcam/app.js
+++ b/apps/hidcam/app.js
@@ -4,7 +4,7 @@ const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, camShot, profile;
-if (settings.HID) {
+if (settings.HID=="kbmedia") {
profile = 'camShutter';
sendHid = function (code, cb) {
try {
@@ -19,8 +19,13 @@ if (settings.HID) {
};
camShot = function (cb) { sendHid(0x80, cb); };
} else {
- E.showMessage('HID disabled');
- setTimeout(load, 1000);
+ E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
+ if (enable) {
+ settings.HID = "kbmedia";
+ require("Storage").write('setting.json', settings);
+ setTimeout(load, 1000, "hidcam.app.js");
+ } else setTimeout(load, 1000);
+ });
}
function drawApp() {
g.clear();
diff --git a/apps/hidjoystick/app-icon.js b/apps/hidjoystick/app-icon.js
new file mode 100644
index 000000000..21d10dd00
--- /dev/null
+++ b/apps/hidjoystick/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AH4ADhvd6AWVAAIYTCwQABC9JGDJCYX/R+7XYgEE7tACycAgczmAX/C/4X/C6kBiMQCyoABDB0N7vdAgIWCAAIXjxAAQCwkIC6OAC/4X/C/4XbgAXRCwgA/AH4ANA"))
diff --git a/apps/hidjoystick/app.js b/apps/hidjoystick/app.js
new file mode 100644
index 000000000..0b3187a53
--- /dev/null
+++ b/apps/hidjoystick/app.js
@@ -0,0 +1,74 @@
+var storage = require('Storage');
+const settings = storage.readJSON('setting.json',1) || { HID: false };
+
+var sendInProgress = false; // Only send one message at a time, do not flood
+
+const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
+ try {
+ const buttons = (btn5<<4) | (btn4<<3) | (btn3<<2) | (btn2<<1) | (btn1<<0);
+ if (!sendInProgress) {
+ sendInProgress = true;
+ NRF.sendHIDReport([buttons, x, y], () => {
+ sendInProgress = false;
+ if (cb) cb();
+ });
+ }
+ } catch(e) {
+ print(e);
+ }
+};
+
+function drawApp() {
+ g.clear();
+ g.setFont("6x8",2);
+ g.setFontAlign(0,0);
+ g.drawString("Joystick", 120, 120);
+ const d = g.getWidth() - 18;
+
+ function c(a) {
+ return {
+ width: 8,
+ height: a.length,
+ bpp: 1,
+ buffer: (new Uint8Array(a)).buffer
+ };
+ }
+
+ g.drawImage(c([16,56,124,254,16,16,16,16]),d,40);
+ g.drawImage(c([16,16,16,16,254,124,56,16]),d,194);
+ g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
+}
+
+function update() {
+ const btn1 = BTN1.read();
+ const btn2 = BTN2.read();
+ const btn3 = BTN3.read();
+ const btn4 = BTN4.read();
+ const btn5 = BTN5.read();
+ const acc = Bangle.getAccel();
+ var x = acc.x*-127;
+ var y = acc.y*-127;
+
+ // check limits
+ if (x > 127) x = 127;
+ else if (x < -127) x = -127;
+ if (y > 127) y = 127;
+ else if (y < -127) y = -127;
+
+ sendHid(x & 0xff, y & 0xff, btn1, btn2, btn3, btn4, btn5);
+}
+
+if (settings.HID === "joy") {
+ drawApp();
+ setInterval(update, 100); // 10 Hz
+} else {
+ E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
+ if (enable) {
+ settings.HID = "joy";
+ storage.write('setting.json', settings);
+ setTimeout(load, 1000, "hidjoystick.app.js");
+ } else {
+ setTimeout(load, 1000);
+ }
+ });
+}
diff --git a/apps/hidjoystick/app.png b/apps/hidjoystick/app.png
new file mode 100644
index 000000000..aca42a818
Binary files /dev/null and b/apps/hidjoystick/app.png differ
diff --git a/apps/hidkbd/ChangeLog b/apps/hidkbd/ChangeLog
new file mode 100644
index 000000000..459bf40b9
--- /dev/null
+++ b/apps/hidkbd/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Core functionnality
+0.02: Offer to enable HID if disabled. Handle with/without media keys
diff --git a/apps/hidkbd/hid-keyboard.js b/apps/hidkbd/hid-keyboard.js
index ed406e093..0d489bc0d 100644
--- a/apps/hidkbd/hid-keyboard.js
+++ b/apps/hidkbd/hid-keyboard.js
@@ -4,27 +4,46 @@ const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, next, prev, toggle, up, down, profile;
-if (settings.HID) {
+if (settings.HID=="kb" || settings.HID=="kbmedia") {
profile = 'Keyboard';
- sendHid = function (code, cb) {
- try {
- NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
- NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], () => {
- if (cb) cb();
+ if (settings.HID=="kbmedia") {
+ sendHid = function (code, cb) {
+ try {
+ NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
+ NRF.sendHIDReport([2,0,0,0,0,0,0,0,0], () => {
+ if (cb) cb();
+ });
});
- });
- } catch(e) {
- print(e);
- }
- };
+ } catch(e) {
+ print(e);
+ }
+ };
+ } else {
+ sendHid = function (code, cb) {
+ try {
+ NRF.sendHIDReport([0,0,code,0,0,0,0,0], () => {
+ NRF.sendHIDReport([0,0,0,0,0,0,0,0], () => {
+ if (cb) cb();
+ });
+ });
+ } catch(e) {
+ print(e);
+ }
+ };
+ }
next = function (cb) { sendHid(0x4f, cb); };
prev = function (cb) { sendHid(0x50, cb); };
toggle = function (cb) { sendHid(0x2c, cb); };
up = function (cb) {sendHid(0x52, cb); };
down = function (cb) { sendHid(0x51, cb); };
} else {
- E.showMessage('HID disabled');
- setTimeout(load, 1000);
+ E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
+ if (enable) {
+ settings.HID = "kb";
+ require("Storage").write('setting.json', settings);
+ setTimeout(load, 1000, "hidkbd.app.js");
+ } else setTimeout(load, 1000);
+ });
}
function drawApp() {
diff --git a/apps/hidmsic/ChangeLog b/apps/hidmsic/ChangeLog
new file mode 100644
index 000000000..73b3268b7
--- /dev/null
+++ b/apps/hidmsic/ChangeLog
@@ -0,0 +1 @@
+0.01: Core functionnality
diff --git a/apps/hidmsic/hid-music.js b/apps/hidmsic/hid-music.js
index 034bbd231..db81744f3 100644
--- a/apps/hidmsic/hid-music.js
+++ b/apps/hidmsic/hid-music.js
@@ -4,7 +4,7 @@ const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, next, prev, toggle, up, down, profile;
-if (settings.HID) {
+if (settings.HID=="kbmedia") {
profile = 'Music';
sendHid = function (code, cb) {
try {
@@ -23,8 +23,13 @@ if (settings.HID) {
up = function (cb) {sendHid(0x40, cb); };
down = function (cb) { sendHid(0x80, cb); };
} else {
- E.showMessage('HID disabled');
- setTimeout(load, 1000);
+ E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) {
+ if (enable) {
+ settings.HID = "kbmedia";
+ require("Storage").write('setting.json', settings);
+ setTimeout(load, 1000, "hidmsc.app.js");
+ } else setTimeout(load, 1000);
+ });
}
function drawApp() {
diff --git a/apps/impwclock/README.md b/apps/impwclock/README.md
new file mode 100644
index 000000000..30e42c95e
--- /dev/null
+++ b/apps/impwclock/README.md
@@ -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?
+
diff --git a/apps/impwclock/clock-impword-icon.js b/apps/impwclock/clock-impword-icon.js
new file mode 100644
index 000000000..f5ed47f1f
--- /dev/null
+++ b/apps/impwclock/clock-impword-icon.js
@@ -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"))
\ No newline at end of file
diff --git a/apps/impwclock/clock-impword.js b/apps/impwclock/clock-impword.js
new file mode 100644
index 000000000..c54fa7976
--- /dev/null
+++ b/apps/impwclock/clock-impword.js
@@ -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"});
diff --git a/apps/impwclock/clock-impword.png b/apps/impwclock/clock-impword.png
new file mode 100644
index 000000000..e7ed0e828
Binary files /dev/null and b/apps/impwclock/clock-impword.png differ
diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog
index e6a689e9e..4bc1ea352 100644
--- a/apps/mclock/ChangeLog
+++ b/apps/mclock/ChangeLog
@@ -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)
diff --git a/apps/mclock/clock-morphing-faster.js b/apps/mclock/clock-morphing-faster.js
new file mode 100644
index 000000000..69cd72707
--- /dev/null
+++ b/apps/mclock/clock-morphing-faster.js
@@ -0,0 +1,206 @@
+var locale = require("locale");
+var CHARW = 34;
+var CHARP = 2;
+var Y = 50;
+// Offscreen buffer
+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)
+ maxFive is true is this digit only counts 0..5 */
+const DIGITS = {
+" ":(g,s,p,n)=>{},
+"0":(g,s,p,n)=>{
+g.fillRect(1+s*n,1-p, 1+s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1+s*n,1+2*s-p, 1+s,1+2*s+p);
+g.fillRect(1+s*n,1+s-p, 1+s*n,1+2*s+p);
+g.fillRect(1+s*n-p,1, 1+s*n+p,1+s)},
+"1":(g,s,p,n)=>{
+g.fillRect(1+(1-n)*s,1-p, 1+s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1+(1-n)*s,1+s-p, 1+s,1+s+p);
+g.fillRect(1-p+(1-n)*s,1+s, 1+p+(1-n)*s,1+2*s);
+g.fillRect(1+(1-n)*s,1-p+2*s, 1+s,1+p+2*s)},
+"2":(g,s,p,n)=>{
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1,1+s-p, 1+s,1+s+p);
+g.fillRect(1-p,1+(1+n)*s, 1+p,1+2*s);
+g.fillRect(1+s-p,1+(2-n)*s, 1+s+p,1+2*s);
+g.fillRect(1,1+2*s-p, 1+s,1+2*s+p)},
+"3":(g,s,p,n)=>{
+g.fillRect(1,1-p, 1+(1-n)*s,1+p);
+g.fillRect(1-p,1, 1+p,n);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1+s*n,1+2*s-p, 1+s,1+2*s+p)},
+"4":(g,s,p,n)=>{
+g.fillRect(1-p,1, 1+p,1+s);
+g.fillRect(1+s,1-p, 1+(1-n)*s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+(1-n)*s);
+g.fillRect(1,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1+(1-n)*s,1+2*s-p, 1+s,1+2*s+p)},
+"5to0": (g,s,p,n)=>{ // 5 -> 0
+g.fillRect(1-p,1, 1+p,1+s);
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1+s*n,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1,1+2*s*p, 1+s,1+2*s+p);
+g.fillRect(1,1+2*s-p, 1,1+2*s+p);
+g.fillRect(1+s-p,1+(1-n)*s, 1+s+p,1+s);
+g.fillRect(1-p,1+s, 1+p,1+(1+n)*s)},
+"5to6": (g,s,p,n)=>{ // 5 -> 6
+g.fillRect(1-p,1, 1+p,1+s);
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1,1+2*s-p, 1+s,1+2*s+p);
+g.fillRect(1-p,2-n, 1+p,1+2*s)},
+"6":(g,s,p,n)=>{
+g.fillRect(1-p,1, 1+p,1+(1-n)*s);
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1+s*n,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+(1-n)*s, 1+s+p,1+s);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1+s*n,1+2*s-p, 1+s,1+2*s+p);
+g.fillRect(1-p,1+(1-n)*s, 1+p,1+s*(2-2*n))},
+"7":(g,s,p,n)=>{
+g.fillRect(1-p,1, 1+p,n);
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1+(1-n)*s,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1+(1-n)*s,1+2*s-p, 1+s,1+2*s+p);
+g.fillRect(1+(1-n)*s-p,1+s, 1+(1-n)*s+p,1+2*s)},
+"8":(g,s,p,n)=>{
+g.fillRect(1-p,1, 1+p,1+s);
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1,1+s-p, 1+s,1+s+p);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1,1+2*s-p, 1+s,1+2*s+p);
+g.fillRect(1-p,1+s, 1+p,1+s*(2-n))},
+"9":(g,s,p,n)=>{
+g.fillRect(1-p,1, 1+p,1+s);
+g.fillRect(1,1-p, 1+s,1+p);
+g.fillRect(1+s-p,1, 1+s+p,1+s);
+g.fillRect(1,1+s-p, 1+(1-n)*s,1+s+p);
+g.fillRect(1-p,1+s, 1+p,1+(1+n)*s);
+g.fillRect(1+s-p,1+s, 1+s+p,1+2*s);
+g.fillRect(1,1+2*s-p, 1+s,1+2*s+p)},
+":":(g,s,p,n)=>{
+g.fillRect(1+s*0.4,1+s*0.4-p, 1+s*0.6,1+s*0.4+p);
+g.fillRect(1+s*0.6-p,1+s*0.4, 1+s*0.6+p,1+s*0.6);
+g.fillRect(1+s*0.6,1+s*0.6-p, 1+s*0.4,1+s*0.6+p);
+g.fillRect(1+s*0.4-p,1+s*0.4, 1+s*0.4+p,1+s*0.6);
+g.fillRect(1+s*0.4,1+s*1.4-p, 1+s*0.6,1+s*1.4+p);
+g.fillRect(1+s*0.6-p,1+s*1.4, 1+s*0.6+p,1+s*1.6);
+g.fillRect(1+s*0.6,1+s*1.6-p, 1+s*0.4,1+s*1.6+p);
+g.fillRect(1+s*0.4-p,1+s*1.4, 1+s*0.4+p,1+s*1.6)
+}};
+
+/* Draw a transition between lastText and thisText.
+ 'n' is the amount - 0..1 */
+function drawDigits(lastText,thisText,n) {
+ const p = CHARP; // padding around digits
+ const s = CHARW; // character size
+ var x = p; // x offset
+ var y = Y+p; // y offset
+ g.reset();
+ for (var i=0;i=1) {
+ n=1;
+ clearInterval(animInterval);
+ animInterval = undefined;
+ }
+ drawDigits(l,t,n);
+ }, 20);
+ lastTime = t;
+}
+
+Bangle.on('lcdPower',function(on) {
+ if (animInterval) {
+ clearInterval(animInterval);
+ animInterval = undefined;
+ }
+ if (timeInterval) {
+ clearInterval(timeInterval);
+ timeInterval = undefined;
+ }
+ if (on) {
+ showTime();
+ timeInterval = setInterval(showTime, 1000);
+ }
+});
+
+g.clear();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+// Update time once a second
+timeInterval = setInterval(showTime, 1000);
+showTime();
+
+// Show launcher when middle button pressed
+setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js
index ce30ad033..e9365db52 100644
--- a/apps/mclock/clock-morphing.js
+++ b/apps/mclock/clock-morphing.js
@@ -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{
+ 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
diff --git a/apps/metronome/ChangeLog b/apps/metronome/ChangeLog
new file mode 100644
index 000000000..a65efbaaf
--- /dev/null
+++ b/apps/metronome/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New App!
+0.02: Watch vibrates with every beat
+0.03: Uses mean of three time intervalls to calculate bmp
diff --git a/apps/metronome/README.md b/apps/metronome/README.md
index 19d489327..1bb9a893c 100644
--- a/apps/metronome/README.md
+++ b/apps/metronome/README.md
@@ -8,3 +8,7 @@ This metronome makes your watch blink and vibrate with a given rate.
* Use `BTN1` to increase the bmp value by one.
* Use `BTN3` to decrease the bmp value by one.
* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`.
+
+## Attributions
+
+Icon made by Roundicons from www.flaticon.com
diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog
index 7b83706bf..dbe920a80 100755
--- a/apps/minionclk/ChangeLog
+++ b/apps/minionclk/ChangeLog
@@ -1 +1,2 @@
0.01: First release
+0.02: Improved date readability, fixed drawing of widgets
diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js
index 88fe446ae..3453f49e1 100755
--- a/apps/minionclk/app.js
+++ b/apps/minionclk/app.js
@@ -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' });
diff --git a/apps/pong/ChangeLog b/apps/pong/ChangeLog
new file mode 100644
index 000000000..6433ebce4
--- /dev/null
+++ b/apps/pong/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: 2 players local + improve ai
diff --git a/apps/pong/README.md b/apps/pong/README.md
new file mode 100644
index 000000000..ea4939539
--- /dev/null
+++ b/apps/pong/README.md
@@ -0,0 +1,28 @@
+# Pong
+
+A clone of the Atari game Pong
+
+
+
+## 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
+
+
diff --git a/apps/pong/app-icon.js b/apps/pong/app-icon.js
new file mode 100644
index 000000000..881e60ba9
--- /dev/null
+++ b/apps/pong/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgIEBgOABQYFD8AUEApoXFDqIXV4BYGKZIANsIRE+IFE/IFEvCIFGrgXLDqIAOgc/9/2hv+g8///3AoUwvE3xuABYP4m3NzwFB7E2tu/CIMYm09wYFDjoFCj4pB/8HkEP+EBFII7EAosDJxYA="))
diff --git a/apps/pong/app.js b/apps/pong/app.js
new file mode 100644
index 000000000..ba34d60b5
--- /dev/null
+++ b/apps/pong/app.js
@@ -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'});
diff --git a/apps/pong/pong.png b/apps/pong/pong.png
new file mode 100644
index 000000000..cc97f58f7
Binary files /dev/null and b/apps/pong/pong.png differ
diff --git a/apps/rclock/ChangeLog b/apps/rclock/ChangeLog
index a8f708a0a..23b1a6e87 100644
--- a/apps/rclock/ChangeLog
+++ b/apps/rclock/ChangeLog
@@ -1 +1,2 @@
0.01: First published version of app
+0.02: Added support for locale and 12H clock
\ No newline at end of file
diff --git a/apps/rclock/rclock.app.js b/apps/rclock/rclock.app.js
index bd8395116..4e63fe36a 100644
--- a/apps/rclock/rclock.app.js
+++ b/apps/rclock/rclock.app.js
@@ -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();
diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog
index 9263b3b13..f168a1fe5 100644
--- a/apps/setting/ChangeLog
+++ b/apps/setting/ChangeLog
@@ -20,3 +20,4 @@
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
+0.19: Allow BLE HID settings, add README.md
diff --git a/apps/setting/README.md b/apps/setting/README.md
new file mode 100644
index 000000000..4052da0ff
--- /dev/null
+++ b/apps/setting/README.md
@@ -0,0 +1,18 @@
+# Settings
+
+This is Bangle.js's settings menu
+
+* **Make Connectable** regardless of the current Bluetooth settings, makes Bangle.js so you can connect to it (while the window is up)
+* **App/Widget Settings** settings specific to installed applications
+* **BLE** is Bluetooth LE enabled and the watch connectable?
+* **Programmable** if BLE is on, can the watch be connected to in order to program/upload apps?
+* **Debug Info** should debug info be shown on the watch's screen or not?
+* **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected
+* **Vibration** enable/disable the vibration motor
+* **Locale** set time zone/whether the clock is 12/24 hour (for supported clocks)
+* **Select Clock** if you have more than one clock face, select the default one
+* **HID** When Bluetooth is enabled, Bangle.js can appear as a Bluetooth Keyboard/Joystick/etc to send keypresses to a connected device. **Note:** on some platforms enabling HID can cause you problems when trying to connect to Bangle.js to upload apps.
+* **Set Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader
+* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on.
+* **Reset Settings** Reset the settings to defaults
+* **Turn Off** Turn Bangle.js off
diff --git a/apps/setting/settings.js b/apps/setting/settings.js
index 97ce464ad..8ed6f58c5 100644
--- a/apps/setting/settings.js
+++ b/apps/setting/settings.js
@@ -61,6 +61,8 @@ const boolFormat = v => v ? "On" : "Off";
function showMainMenu() {
var beepV = [false, true, "vib"];
var beepN = ["Off", "Piezo", "Vibrate"];
+ var hidV = [false, "kbmedia", "kb", "joy"];
+ var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"];
const mainmenu = {
'': { 'title': 'Settings' },
'Make Connectable': ()=>makeConnectable(),
@@ -115,10 +117,11 @@ function showMainMenu() {
'Locale': ()=>showLocaleMenu(),
'Select Clock': ()=>showClockMenu(),
'HID': {
- value: settings.HID,
- format: boolFormat,
- onchange: () => {
- settings.HID = !settings.HID;
+ value: 0 | hidV.indexOf(settings.HID),
+ min: 0, max: 3,
+ format: v => hidN[v],
+ onchange: v => {
+ settings.HID = hidV[v];
updateSettings();
}
},
@@ -419,7 +422,7 @@ function showAppSettingsMenu() {
const apps = storage.list(/\.settings\.js$/)
.map(s => s.substr(0, s.length-12))
.map(id => {
- const a=storage.readJSON(id+'.info',1);
+ const a=storage.readJSON(id+'.info',1) || {name: id};
return {id:id,name:a.name,sortorder:a.sortorder};
})
.sort((a, b) => {
diff --git a/apps/swatch/ChangeLog b/apps/swatch/ChangeLog
index 9a037fa41..caa74a1ba 100644
--- a/apps/swatch/ChangeLog
+++ b/apps/swatch/ChangeLog
@@ -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)
diff --git a/apps/swatch/stopwatch.js b/apps/swatch/stopwatch.js
index 659f0606d..478de2712 100644
--- a/apps/swatch/stopwatch.js
+++ b/apps/swatch/stopwatch.js
@@ -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();
diff --git a/apps/torch/ChangeLog b/apps/torch/ChangeLog
index 5560f00bc..8e76b717a 100644
--- a/apps/torch/ChangeLog
+++ b/apps/torch/ChangeLog
@@ -1 +1,2 @@
0.01: New App!
+0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
diff --git a/apps/torch/widget.js b/apps/torch/widget.js
index e3d1ea22f..a5002ea71 100644
--- a/apps/torch/widget.js
+++ b/apps/torch/widget.js
@@ -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{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.nameb.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.nameb.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);
\ No newline at end of file
diff --git a/apps/toucher/settings.js b/apps/toucher/settings.js
new file mode 100644
index 000000000..6f7320513
--- /dev/null
+++ b/apps/toucher/settings.js
@@ -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
+ });
+});
\ No newline at end of file
diff --git a/apps/weather/app.js b/apps/weather/app.js
new file mode 100644
index 000000000..8493144f7
--- /dev/null
+++ b/apps/weather/app.js
@@ -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'})
+})()
diff --git a/apps/weather/icon.js b/apps/weather/icon.js
new file mode 100644
index 000000000..18ca2b0c9
--- /dev/null
+++ b/apps/weather/icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AE8N6AXV7vdFyoXBGCQUBAAoXp73u93tC6YWBAAIXSFwQwDRiAWDGASSBmYABIx5IDgYXCmC7KCoYRBnvUCwQwKC4gSEAAgwKC5gwKCwPjC6inBC6owBC6wVKPBXd6YXMDBAuNJJQXWfwZITC/6QIBw073ezR6m73anOJAwuHeBAYFIw4WJAAsL3YQOAAxeBCiWIC4e72AJChGACpMIxAXBIwIwEBIIXKBgouDEIYuLC4ghEC6ELLoYXPU4YXFLxQNBBgcLEQqqQFwYA/AH4AYA=="))
diff --git a/apps/weather/icon.png b/apps/weather/icon.png
new file mode 100644
index 000000000..bbfc0ace0
Binary files /dev/null and b/apps/weather/icon.png differ
diff --git a/apps/weather/lib.js b/apps/weather/lib.js
new file mode 100644
index 000000000..f87984fe5
--- /dev/null
+++ b/apps/weather/lib.js
@@ -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 {};
+ 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)
+ },
+}
diff --git a/apps/weather/readme.md b/apps/weather/readme.md
new file mode 100644
index 000000000..a326b67dd
--- /dev/null
+++ b/apps/weather/readme.md
@@ -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:
+
+
+## Setup
+
+See [this guide](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather)
+to setup Gadgetbridge weather reporting.
+
+## Controls
+
+BTN2: opens the launcher
diff --git a/apps/weather/screenshot.png b/apps/weather/screenshot.png
new file mode 100644
index 000000000..ce79b3b64
Binary files /dev/null and b/apps/weather/screenshot.png differ
diff --git a/apps/weather/widget.js b/apps/weather/widget.js
new file mode 100644
index 000000000..e02591543
--- /dev/null
+++ b/apps/weather/widget.js
@@ -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
+ }
+})();
diff --git a/index.html b/index.html
index 3c8b440e4..3815c12eb 100644
--- a/index.html
+++ b/index.html
@@ -138,6 +138,14 @@