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 6d67d8b40..02567f994 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,
@@ -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",
@@ -467,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": [
@@ -479,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": [
@@ -491,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": [
@@ -1012,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",
@@ -1175,7 +1189,7 @@
"name": "Active Pedometer",
"shortName":"Active Pedometer",
"icon": "app.png",
- "version":"0.03",
+ "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",
"readme": "README.md",
@@ -1241,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",
@@ -1391,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,
@@ -1423,7 +1438,7 @@
"name": "Camera shutter",
"shortName":"Cam shutter",
"icon": "app.png",
- "version":"0.01",
+ "version":"0.02",
"description": "Enable HID, connect to your phone, start your camera and trigger the shot on your Bangle",
"tags": "tools",
"storage": [
@@ -1473,14 +1488,64 @@
"name": "Pong",
"shortName": "Pong",
"icon": "pong.png",
- "version": "0.01",
+ "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/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 c1b9ec011..ca26a648a 100644
--- a/apps/activepedom/ChangeLog
+++ b/apps/activepedom/ChangeLog
@@ -1,3 +1,4 @@
0.01: New Widget!
0.02: Distance calculation and display
-0.03: Data logging and display
\ No newline at end of file
+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 f45297e57..a2a351a12 100644
--- a/apps/activepedom/README.md
+++ b/apps/activepedom/README.md
@@ -18,7 +18,7 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
* 10600 steps

-## Features
+## Features Widget
* Two line display
* Can display distance (in km) or steps in each line
@@ -32,22 +32,23 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
* 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
+* 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 chose the app to watch a steps graph
+* '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
-## 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
-
## 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
index 0a9b3b93f..cc875f371 100644
--- a/apps/activepedom/app.js
+++ b/apps/activepedom/app.js
@@ -162,4 +162,4 @@ 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 c6bd410ce..2ae1b9b62 100644
--- a/apps/activepedom/widget.js
+++ b/apps/activepedom/widget.js
@@ -33,27 +33,28 @@
function storeData() {
now = new Date();
- month = now.getMonth() + 1;
- if (month < 10) month = "0" + month;
- filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data";
+ 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) {
+ if (dataFile) { //check if filen already exists
if (dataFile.getLength() == 0) {
- stepsToWrite = 0;
- }
- else {
- stepsToWrite = stepsCounted;
+ //new day, set steps to 0
+ stepsCounted = 0;
+ stepsTooShort = 0;
+ stepsTooLong = 0;
+ stepsOutsideTime = 0;
}
dataFile.write([
now.getTime(),
- stepsToWrite,
+ stepsCounted,
active,
stepsTooShort,
stepsTooLong,
stepsOutsideTime,
].join(",")+"\n");
}
- dataFile = undefined;
+ dataFile = undefined; //save memory
}
//return setting
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/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/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..2823e1f1d 100644
--- a/apps/hidcam/ChangeLog
+++ b/apps/hidcam/ChangeLog
@@ -1 +1,2 @@
0.01: Core functionnality
+0.02: Offer to enable HID if disabled
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/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/pong/ChangeLog b/apps/pong/ChangeLog
index 5560f00bc..6433ebce4 100644
--- a/apps/pong/ChangeLog
+++ b/apps/pong/ChangeLog
@@ -1 +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.js b/apps/pong/app.js
index 4531b3af8..ba34d60b5 100644
--- a/apps/pong/app.js
+++ b/apps/pong/app.js
@@ -8,6 +8,7 @@
* - 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;
@@ -15,6 +16,13 @@ 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;
@@ -28,12 +36,18 @@ Vector.prototype.add = function (x) {
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) => {
- var c1 = circ.pos, c2 = {x: circ.pos.x+circ.r, y: circ.pos.y+circ.r};
- var r1 = rect.pos, r2 = {x: rect.pos.x+rect.width*2, y: rect.pos.y+rect.height};
- return !(c1.x > r2.x || c2.x < r1.x ||
- c1.y > r2.y || c2.y < r1.y);
-};
+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 //////////////////////////////////////////
@@ -45,12 +59,26 @@ function Ball() {
this.reset();
}
-Ball.prototype.show = function () {
+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(0);
+ g.setColor(invert ? -1 : 0);
g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r);
}
- g.setColor(-1);
+ g.setColor(invert ? 0 : -1);
g.fillCircle(this.pos.x, this.pos.y, this.r);
this.prevPos = {
x: this.pos.x,
@@ -58,55 +86,62 @@ Ball.prototype.show = function () {
r: this.r
};
};
-Ball.prototype.bouncePlayer = function (multiplyX, multiplyY, player) {
+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 relativeIntersectY = (player.pos.y+(player.height/2)) - this.pos.y;
- var normalizedRelativeIntersectionY = (relativeIntersectY/(player.height/2));
var MAX_BOUNCE_ANGLE = 4 * Math.PI/12;
- var bounceAngle = normalizedRelativeIntersectionY * MAX_BOUNCE_ANGLE;
- this.velocity.x = this.speed * Math.cos(bounceAngle) * multiplyX;
- this.velocity.y = this.speed * -Math.sin(bounceAngle) * multiplyY;
+ 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 (multiplyX, multiplyY, player) {
+Ball.prototype.bounce = function (directionX, directionY, player) {
if (player)
- return this.bouncePlayer(multiplyX, multiplyY, player);
+ return this.bouncePlayer(directionX, directionY, player);
- if (multiplyX) {
- this.velocity.x = Math.abs(this.velocity.x) * multiplyX;
+ if (directionX) {
+ this.velocity.x = Math.abs(this.velocity.x) * directionX;
}
- if (multiplyY) {
- this.velocity.y = Math.abs(this.velocity.y) * multiplyY;
+ if (directionY) {
+ this.velocity.y = Math.abs(this.velocity.y) * directionY;
}
};
-Ball.prototype.checkWallsCollision = function () {
+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) {
- scores[1]++;
- if (scores[1] >= MAX_SCORE) {
- this.restart();
- state = 3;
- winnerMessage = "AI Wins!";
- } else {
- this.reset();
- }
+ this.fall(1);
} else if (this.pos.x > SCREEN_WIDTH) {
- scores[0]++;
- if (scores[0] >= MAX_SCORE) {
- this.restart();
- state = 3;
- winnerMessage = "You Win!";
- } else {
- this.reset();
- }
+ this.fall(0);
} else {
return false;
}
return true;
};
-Ball.prototype.checkPlayerCollision = function (player) {
+Ball.prototype.playerCollision = function (player) {
if (intersects(this, player)) {
if (this.pos.x < SCREEN_WIDTH/2) {
this.bounce(1, 1, player);
@@ -120,8 +155,8 @@ Ball.prototype.checkPlayerCollision = function (player) {
}
return false;
};
-Ball.prototype.checkCollisions = function () {
- return this.checkWallsCollision() || this.checkPlayerCollision(player) || this.checkPlayerCollision(ai);
+Ball.prototype.collisions = function () {
+ return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai);
};
Ball.prototype.updatePosition = function () {
var elapsed = new Date().getTime() - this.lastUpdate;
@@ -132,31 +167,20 @@ Ball.prototype.updatePosition = function () {
Ball.prototype.update = function () {
this.updatePosition();
this.lastUpdate = new Date().getTime();
- this.checkCollisions();
-};
-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));
-};
-Ball.prototype.restart = function() {
- 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);
+ this.collisions();
};
//////////////////////////// Player /////////////////////////////////////////
-function Player() {
+function Player(right) {
this.width = 4;
this.height = 30;
- this.pos = new Vector(this.width*2, SCREEN_WIDTH/2 - this.height/2);
+ 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) {
@@ -196,11 +220,14 @@ function AI() {
AI.prototype = Object.create(Player.prototype);
AI.prototype.constructor = Player;
AI.prototype.update = function () {
- var y = ball.pos.y - (this.height/2 * aiSpeedRandom);
- var yConstrained = constrain(y, 0, SCREEN_WIDTH-this.height);
+ 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) {
@@ -210,12 +237,6 @@ function net() {
}
}
-var player = new Player();
-var ai = new AI();
-var ball = new Ball();
-var state = 0;
-var prevScores = [0, 0];
-
function drawScores() {
let x1 = SCREEN_WIDTH/4-5;
let x2 = SCREEN_WIDTH*3/4-5;
@@ -233,10 +254,80 @@ function drawScores() {
function drawGameOver() {
g.setFont("Vector", 20);
- g.drawString(winnerMessage, 75, SCREEN_WIDTH/2 - 10);
+ g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10);
}
-function draw() {
+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();
@@ -261,22 +352,73 @@ function draw() {
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);
-setInterval(draw, 1000 / FPS);
+////////////////////////////// Controls ///////////////////////////////////////
-setWatch(o => o.state ? player.up() : player.stop(), BTN1, {repeat: true, edge: 'both'});
-setWatch(o => o.state ? player.down() : player.stop(), BTN3, {repeat: true, edge: 'both'});
-//setWatch(o => o.state ? player.down() : player.stop(), BTN5, {repeat: true, edge: 'both'});
+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) {
- ball.restart();
g.setColor(0);
- g.fillRect(0,0,240,240);
+ 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();
+ }
}
-}, BTN2, {repeat: true});
+}, 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/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..55048a9d4 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();
}
},
diff --git a/index.html b/index.html
index 3c8b440e4..3815c12eb 100644
--- a/index.html
+++ b/index.html
@@ -138,6 +138,14 @@
+ Settings
+
+
+
+
@@ -156,6 +164,7 @@
+