Merge remote-tracking branch 'upstream/master' into ht_indication

pull/378/head^2
Fredrik Lautrup 2020-05-04 18:50:50 +02:00
commit ad17efd39b
94 changed files with 9896 additions and 325 deletions

1
.gitignore vendored
View File

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

View File

@ -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

180
apps.json
View File

@ -2,7 +2,7 @@
{ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
"version":"0.14",
"version":"0.16",
"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.13",
"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": [
@ -960,8 +974,8 @@
"name": "Grocery",
"icon": "grocery.png",
"version":"0.01",
"description": "Simple grocery list - Display a list of product and track if you already put them in your cart.",
"tags": "tool,outdoors",
"description": "Simple grocery (shopping) list - Display a list of product and track if you already put them in your cart.",
"tags": "tool,outdoors,shopping,list",
"type": "app",
"custom":"grocery.html",
"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",
@ -1092,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
},
@ -1171,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",
@ -1237,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",
@ -1265,7 +1283,7 @@
"name": "Numerals Clock",
"shortName": "Numerals Clock",
"icon": "numerals.png",
"version":"0.04",
"version":"0.05",
"description": "A simple big numerals clock",
"tags": "numerals,clock",
"type":"clock",
@ -1387,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,
@ -1419,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}
@ -1448,6 +1468,7 @@
"version":"0.01",
"description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"storage": [
{"name":"hamloc.app.js","url":"app.js"},
{"name":"hamloc.img","url":"app-icon.js","evaluate":true}
@ -1456,9 +1477,10 @@
{ "id": "osmpoi",
"name": "POI Compass",
"icon": "app.png",
"version":"0.02",
"version":"0.03",
"description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.",
"tags": "tool,outdoors,gps",
"readme": "README.md",
"custom": "osmpoi.html",
"storage": [
{"name":"osmpoi.app.js"},
@ -1469,14 +1491,134 @@
"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}
]
},
{
"id": "largeclock",
"name": "Large Clock",
"icon": "largeclock.png",
"version": "0.01",
"description": "A readable and informational digital watch, with date, seconds and moon phase",
"readme": "README.md",
"tags": "clock",
"type": "clock",
"allow_emulator": true,
"storage": [
{
"name": "largeclock.app.js",
"url": "largeclock.js"
},
{
"name": "largeclock.img",
"url": "largeclock-icon.js",
"evaluate": true
},
{
"name": "largeclock.settings.js",
"url": "settings.js"
},
{
"name": "largeclock.json",
"url": "largeclock.json",
"evaluate": true
}
]
},
{ "id": "smtswch",
"name": "Smart Switch",
"shortName":"Smart Switch",
"icon": "app.png",
"version":"0.01",
"description": "Using EspruinoHub, control your smart devices on and off via Bluetooth Low Energy!",
"tags": "bluetooth,btle,smart,switch",
"type": "app",
"readme": "README.md",
"storage": [
{"name":"smtswch.app.js","url":"app.js"},
{"name":"smtswch.img","url":"app-icon.js","evaluate":true},
{"name":"light-on.img","url":"light-on.js","evaluate":true},
{"name":"light-off.img","url":"light-off.js","evaluate":true},
{"name":"switch-on.img","url":"switch-on.js","evaluate":true},
{"name":"switch-off.img","url":"switch-off.js","evaluate":true}
]
},
{
"id": "simpletimer",
"name": "Timer",
"icon": "app.png",
"version": "0.01",
"description": "Simple timer, useful when playing board games or cooking",
"tags": "timer",
"readme": "README.md",
"allow_emulator": true,
"storage": [
{
"name": "simpletimer.app.js",
"url": "app.js"
},
{
"name": "simpletimer.img",
"url": "app-icon.js",
"evaluate": true
}
]
}
]

View File

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

File diff suppressed because one or more lines are too long

View File

@ -6,3 +6,5 @@
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
0.13: Fix broken date padding (fix #376)

View File

@ -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;
@ -88,7 +84,7 @@ const drawDate = () => {
const dayString = locale.dow(currentDate, true);
// pad left date
const dateString = (currentDate.getDate() < 10) ? '0' : '' + currentDate.getDate().toString();
const dateString = ("0"+currentDate.getDate().toString()).substr(-2);
const dateDisplay = `${dayString}-${dateString}`;
// console.log(`${dayString}|${dateString}`);
// center date

View File

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

View File

@ -18,7 +18,7 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
* 10600 steps
![](10600.png)
## 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

View File

@ -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

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

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

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

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

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

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

BIN
apps/ballmaze/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

BIN
apps/ballmaze/maze.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

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

View File

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

View File

@ -6,3 +6,4 @@
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.
0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state

View File

@ -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) {

View File

@ -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();

View File

@ -13,3 +13,5 @@
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
0.16: Detect out of memory errors and draw them onto the bottom of the screen in red

View File

@ -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});
}
}
@ -37,6 +39,11 @@ Bangle.setLCDTimeout(s.timeout);
if (!s.timeout) Bangle.setLCDPower(1);
E.setTimeZone(s.timezone);
delete s;
// Draw out of memory errors onto the screen
E.on('errorFlag', function(errorFlags) { g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:",errorFlags);
E.getErrorFlags(); // clear flags so we get called next time
});
// stop users doing bad things!
global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }
// Load *.boot.js files

88
apps/boot/hid_info.txt Normal file
View File

@ -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)
```

1
apps/calendar/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Basic calendar

8
apps/calendar/README.md Normal file
View File

@ -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

View File

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

160
apps/calendar/calendar.js Normal file
View File

@ -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" });

BIN
apps/calendar/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

View File

@ -12,3 +12,7 @@ The chosen coding uses alternating pairs of letters and digits, like so:
##
* support Paul Brewer KI6CQ HamGridSquare.js
* support Chris Veness 2002-2012 LatLon library
## Requests
If you have any bug or feature request, please contact [Renaudgweb](https://github.com/renaudgweb/)

2
apps/hidbkbd/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Core functionnality
0.02: Offer to enable HID if disabled. Handle with/without media keys

View File

@ -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);
});
}

View File

@ -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

18
apps/hidcam/README.md Normal file
View File

@ -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.

View File

@ -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();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4ADhvd6AWVAAIYTCwQABC9JGDJCYX/R+7XYgEE7tACycAgczmAX/C/4X/C6kBiMQCyoABDB0N7vdAgIWCAAIXjxAAQCwkIC6OAC/4X/C/4XbgAXRCwgA/AH4ANA"))

74
apps/hidjoystick/app.js Normal file
View File

@ -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);
}
});
}

BIN
apps/hidjoystick/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

2
apps/hidkbd/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Core functionnality
0.02: Offer to enable HID if disabled. Handle with/without media keys

View File

@ -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() {

1
apps/hidmsic/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Core functionnality

View File

@ -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() {

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

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1 @@
0.01: Init

19
apps/largeclock/README.md Normal file
View File

@ -0,0 +1,19 @@
# Large clock
A readable and informational digital watch, with date, seconds and moon phase and with programmable BTN1 & BTN3
## Features
- Readable
- Informative: hours, minutes, secondsa, date, year and moon phase
- Pairs nicely with any other apps: in setting > large clock any installed app can be assigned to BTN1 and BTN3 in order to open it easily directly from the watch, without the hassle of passing trough the launcher. For example BTN1 can be assigned to alarm and BTN3 to chronometer.
## How to use it
- The clock can be used as any other one, if you like it just set it as the default clock app in settings > select clock
- In setting > large clock you can select which app is to be open by BTN1 and BTN3
## Credits
- The clock face is heavily inspired by Big Clock byJeffmer https://jeffmer.github.io/JeffsBangleAppsDev/
- The moon phase is basically the one from the widget https://github.com/espruino/BangleApps/tree/master/apps/widmp

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4ArmYAQCwkDC6MwFyowFC/4XKnGIAAIQFBAWDC5INCBwggEEIYXdxAODnAYCAYIgDDAQXECoIrDE4YrEBwYX/C/4X/C/4X8BwIKBAAM4DgQDBBAQDBBAIXFE4QOCA4QrCAAQHCC7wODCwYhEEAYXGACAX/C5cDCyMwC4YwSCwgA/AH4AlA="))

View File

@ -0,0 +1,198 @@
const REFRESH_RATE = 1000;
let interval;
let lastMoonPhase;
let lastMinutes;
const moonR = 12;
const moonX = 215;
const moonY = 50;
const settings = require("Storage").readJSON("largeclock.json", 1);
const BTN1app = settings.BTN1;
const BTN3app = settings.BTN3;
console.log("BTN1app", BTN1app);
console.log("BTN3app", BTN3app);
function drawMoon(d) {
const BLACK = 0,
MOON = 0x41f,
MC = 29.5305882,
NM = 694039.09;
var moon = {
// reset
0: () => {
g.setColor(BLACK).fillRect(
moonX - moonR,
moonY - moonR,
moonX + moonR,
moonY + moonR
);
},
// new moon
1: () => {
moon[0]();
g.setColor(MOON).drawCircle(moonX, moonY, moonR);
},
// 1/4 ascending
2: () => {
moon[3]();
g.setColor(BLACK).fillEllipse(
moonX - moonR / 2,
moonY - moonR,
moonX + moonR / 2,
moonY + moonR
);
},
// 1/2 ascending
3: () => {
moon[0]();
g.setColor(MOON)
.fillCircle(moonX, moonY, moonR)
.setColor(BLACK)
.fillRect(moonX, moonY - moonR, moonX + moonR + moonR, moonY + moonR);
},
// 3/4 ascending
4: () => {
moon[7]();
g.setColor(MOON).fillEllipse(
moonX - moonR / 2,
moonY - moonR,
moonX + moonR / 2,
moonY + moonR
);
},
// Full moon
5: () => {
moon[0]();
g.setColor(MOON).fillCircle(moonX, moonY, moonR);
},
// 3/4 descending
6: () => {
moon[3]();
g.setColor(MOON).fillEllipse(
moonX - moonR / 2,
moonY - moonR,
moonX + moonR / 2,
moonY + moonR
);
},
// 1/2 descending
7: () => {
moon[0]();
g.setColor(MOON)
.fillCircle(moonX, moonY, moonR)
.setColor(BLACK)
.fillRect(moonX - moonR, moonY - moonR, moonX, moonY + moonR);
},
// 1/4 descending
8: () => {
moon[7]();
g.setColor(BLACK).fillEllipse(
moonX - moonR / 2,
moonY - moonR,
moonX + moonR / 2,
moonY + moonR
);
}
};
function moonPhase(d) {
var tmp,
month = d.getMonth(),
year = d.getFullYear(),
day = d.getDate();
if (month < 3) {
year--;
month += 12;
}
tmp = (365.25 * year + 30.6 * ++month + day - NM) / MC;
return Math.round((tmp - (tmp | 0)) * 7 + 1);
}
const currentMoonPhase = moonPhase(d);
if (currentMoonPhase != lastMoonPhase) {
moon[currentMoonPhase]();
lastMoonPhase = currentMoonPhase;
}
}
function drawTime(d) {
const da = d.toString().split(" ");
const time = da[4].substr(0, 5).split(":");
const dow = da[0];
const month = da[1];
const day = da[2];
const year = da[3];
const hours = time[0];
const minutes = time[1];
const seconds = d.getSeconds();
if (minutes != lastMinutes) {
g.clearRect(0, 24, moonX - moonR - 10, 239);
g.setColor(1, 1, 1);
g.setFontAlign(-1, -1);
g.setFont("Vector", 100);
g.drawString(hours, 50, 24, true);
g.setColor(1, 50, 1);
g.drawString(minutes, 50, 135, true);
g.setFont("Vector", 20);
g.setRotation(3);
g.drawString(`${dow} ${day} ${month}`, 50, 15, true);
g.drawString(year, 75, 205, true);
lastMinutes = minutes;
}
g.setRotation(0);
g.setFont("Vector", 20);
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.clearRect(200, 210, 240, 240);
g.drawString(seconds, 215, 215);
}
function drawClockFace() {
const d = new Date();
drawTime(d);
drawMoon(d);
}
Bangle.on("lcdPower", function(on) {
if (on) {
g.clear();
Bangle.drawWidgets();
drawClockFace();
interval = setInterval(drawClockFace, REFRESH_RATE);
} else {
clearInterval(interval);
lastMinutes = undefined;
lastMoonPhase = undefined;
}
});
Bangle.setLCDMode();
// Show launcher when middle button pressed
clearWatch();
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
setWatch(
function() {
load(BTN1app);
},
BTN1,
{ repeat: false, edge: "rising" }
);
setWatch(
function() {
load(BTN3app);
},
BTN3,
{ repeat: false, edge: "rising" }
);
g.clear();
clearInterval();
drawClockFace();
interval = setInterval(drawClockFace, REFRESH_RATE);
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -0,0 +1,4 @@
{
"BTN1": "timer.app.js",
"BTN3": "calendar.app.js"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

View File

@ -0,0 +1,72 @@
(function(back) {
const s = require("Storage");
const apps = s
.list(/\.info$/)
.map(app => {
var a = s.readJSON(app, 1);
return (
a && {
n: a.name,
t: a.type,
src: a.src
}
);
})
.filter(app => app && (app.t == "app" || app.t == "clock" || !app.t))
.map(a => {
return { n: a.n, src: a.src };
});
apps.sort((a, b) => {
if (a.n < b.n) return -1;
if (a.n > b.n) return 1;
return 0;
});
const settings = s.readJSON("largeclock.json", 1) || {
BTN1: "",
BTN3: ""
};
function showApps(btn) {
function format(v) {
return v === settings[btn] ? "*" : "";
}
function onchange(v) {
settings[btn] = v;
s.write("largeclock.json", settings);
}
const btnMenu = {
"": {
title: `Apps for ${btn}`
},
"< Back": () => E.showMenu(mainMenu)
};
if (apps.length > 0) {
for (let i = 0; i < apps.length; i++) {
btnMenu[apps[i].n] = {
value: apps[i].src,
format: format,
onchange: onchange
};
}
} else {
btnMenu["...No Apps..."] = {
value: undefined,
format: () => "",
onchange: () => {}
};
}
return E.showMenu(btnMenu);
}
const mainMenu = {
"": { title: "Large Clock Settings" },
"< Back": back,
"BTN1 app": () => showApps("BTN1"),
"BTN3 app": () => showApps("BTN3")
};
E.showMenu(mainMenu);
});

View File

@ -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<lastText.length;i++) {
var lastCh = lastText[i];
var thisCh = thisText[i];
if (thisCh==":") x-=4;
if (lastCh!=thisCh) {
var ch, chn = n;
if ((thisCh-1==lastCh ||
(thisCh==0 && lastCh==5) ||
(thisCh==0 && lastCh==9)))
ch = lastCh;
else {
ch = thisCh;
chn = 0;
}
if (ch=="5") ch = (lastCh==5 && thisCh==0)?"5to0":"5to6";
buf.clear();
DIGITS[ch](buf,s,p,chn);
g.drawImage(bufimg,x-1,y-1);
}
if (thisCh==":") x-=4;
x+=s+p+7;
}
}
function drawSeconds() {
var x = CHARW*6 + CHARP*2 - 4;
var y = Y + 2*CHARW + CHARP;
var d = new Date();
g.reset();
g.setFont("6x8");
g.setFontAlign(-1,-1);
g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true);
// date
g.setFontAlign(0,-1);
var date = locale.date(d,false);
g.drawString(date, g.getWidth()/2, y+8, true);
}
/* Show the current time, and animate if needed */
function showTime() {
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 || l==" ") {
drawDigits(t,l,0);
drawSeconds();
return;
}
var n = 0;
animInterval = setInterval(function() {
n += 1/10;
if (n>=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"});

3
apps/metronome/ChangeLog Normal file
View File

@ -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

View File

@ -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

View File

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

View File

@ -12,6 +12,7 @@
}
let numeralsSettings = storage.readJSON('numerals.json',1);
if (!numeralsSettings) resetSettings();
if (numeralsSettings.menuButton===undefined) numeralsSettings.menuButton=22;
let dm = ["fill","frame"];
let col = ["rnd","r/g","y/w","o/c","b/y"];
let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]];
@ -30,7 +31,7 @@
onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();}
},
"Menu button": {
value: 1|btn[numeralsSettings.menuButton],
value: btn.findIndex(e=>e[0]==numeralsSettings.menuButton),
min:0,max:4,
format: v=>btn[v][1],
onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();}

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Change img when no fix
0.03: Add HTML class for Spectre.CSS

View File

@ -3,3 +3,7 @@
## Description
Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.
## Requests
If you have any bug or feature request, please contact [Renaudgweb](https://github.com/renaudgweb/)

View File

@ -28,8 +28,8 @@
<div id="map">
</div>
<div id="controls">
<select id="amenity">
<div id="controls" class="form-group">
<select id="amenity" class="form-select select-lg">
<option value="drinking_water">Drinking Water</option>
<option value="bicycle_parking">Bicycle Park</option>
<option value="post_box">Post Box</option>
@ -37,7 +37,7 @@
<option value="bbq">Barbecue</option>
<option value="cafe">Café</option>
<option value="fast_food">Fast food</option>
<option value="restaurant">restaurant</option>
<option value="restaurant">Restaurant</option>
<option value="bicycle_repair_station">Bicycle Repair</option>
<option value="bicycle_rental">Bicycle rental</option>
<option value="bus_station">Bus station</option>

View File

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

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

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

View File

@ -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'});

View File

@ -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

18
apps/setting/README.md Normal file
View File

@ -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

View File

@ -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) => {

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -0,0 +1,16 @@
# Timer
Simple timer, useful when playing board games or cooking
## Features
- When the time is up the timer can be reset to starting time, this is useful e.g. for playing board games
- When the countdown is running the timer cannot be adjusted, this prevents accidental time variations
- When the time is up the starting time is shown, as a reminder of the time elapsed
## How to use it
- Tap on minutes to increase them one by one
- Tap on seconds to increase them one by one
- Press BTN3 to reset time to 0
- Press BTN1 to start the timer or reset to the original time

View File

@ -0,0 +1,5 @@
require("heatshrink").decompress(
atob(
"mEwxH+AH4A/AEsxAAQso1eyrgvDrmrw4skAAQuDAAIHBrYABFsQvMGLYtGAAOAFweA2WrF4gwYFxAwEFwIvBwowFsIub64AB6wJF6wJB1mGMTFbrmsEYoADHAwAC1dhGCoTCmJhBEYoAM2RiFF6VbleBF6QABGAguSw2sgAwnCAdhXYIwBqwvT2WFDwYvP1YZCwMAlYwT1ZgORogZEqwwB1iRhBoYmGlcAYiZgOBgWFDIzCBAALESYIYvMw4ZHGCuHF5aOKeYgABYiCQMBYeyDZLzBAAQwO2QvPDhbzCeqAvbGAQQBlYvqeYIvteYMreJ7vaACbvQJxwAP1YvLGAeHF7uHFxYvDwovdwovPSDusRxgvEwwvbwwvNGAmrds4vGsOyFy+ysIvPSLqNPGDwuT/xyEwySS2QuEF6BgEYYL0Q1ZIEFyIwGMQIxM1ZcFFyYwHreFw+rSwmy1eHwoSGFygxJABwtXeo4upMSQtdGZorjAH4A/AF4A=="
)
)

151
apps/simpletimer/app.js Normal file
View File

@ -0,0 +1,151 @@
let counter = 0;
let setValue = 0;
let counterInterval;
let state;
const DEBOUNCE = 50;
function buzzAndBeep() {
return Bangle.buzz(1000, 1)
.then(() => Bangle.beep(200, 3000))
.then(() => setTimeout(buzzAndBeep, 5000));
}
function outOfTime() {
g.clearRect(0, 0, 220, 70);
g.setFontAlign(0, 0);
g.setFont("6x8", 3);
g.drawString("Time UP!", 120, 50);
counter = setValue;
buzzAndBeep();
setInterval(() => {
g.clearRect(0, 70, 220, 160);
setTimeout(draw, 200);
}, 400);
state = "stopped";
}
function draw() {
const minutes = Math.floor(counter / 60);
const seconds = Math.floor(counter % 60);
const seconds2Digits = seconds < 10 ? `0${seconds}` : seconds.toString();
g.clearRect(0, 70, 220, 160);
g.setFontAlign(0, 0);
g.setFont("6x8", 7);
g.drawString(
`${minutes < 10 ? "0" : ""}${minutes}:${seconds2Digits}`,
120,
120
);
}
function countDown() {
if (counter <= 0) {
if (counterInterval) {
clearInterval(counterInterval);
counterInterval = undefined;
}
outOfTime();
return;
}
counter--;
draw();
}
function clearIntervals() {
clearInterval();
counterInterval = undefined;
}
function set(delta) {
if (state === "started") return;
counter += delta;
if (state === "unset") {
state = "set";
}
draw();
g.flip();
}
function startTimer() {
setValue = counter;
countDown();
counterInterval = setInterval(countDown, 1000);
}
// unset -> set -> started -> -> stopped -> set
const stateMap = {
set: () => {
state = "started";
startTimer();
},
started: () => {
reset(setValue);
},
stopped: () => {
reset(setValue);
}
};
function changeState() {
if (stateMap[state]) stateMap[state]();
}
function drawLabels() {
g.clear();
g.setFontAlign(-1, 0);
g.setFont("6x8", 7);
g.drawString(`+ +`, 35, 180);
g.setFontAlign(0, 0, 3);
g.setFont("6x8", 1);
g.drawString(`reset (re)start`, 230, 120);
}
function reset(value) {
clearIntervals();
counter = value;
setValue = value;
drawLabels();
draw();
state = value === 0 ? "unset" : "set";
}
function addWatch() {
clearWatch();
setWatch(changeState, BTN1, {
debounce: DEBOUNCE,
repeat: true,
edge: "falling"
});
setWatch(
() => {
reset(0);
},
BTN3,
{
debounce: DEBOUNCE,
repeat: true,
edge: "falling"
}
);
setWatch(
() => {
set(60);
},
BTN4,
{
debounce: DEBOUNCE,
repeat: true,
edge: "falling"
}
);
setWatch(() => set(1), BTN5, {
debounce: DEBOUNCE,
repeat: true,
edge: "falling"
});
}
reset(0);
addWatch();

BIN
apps/simpletimer/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

1
apps/smtswch/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App! See the README.MD for details on how to use it.

72
apps/smtswch/README.md Normal file
View File

@ -0,0 +1,72 @@
# Smart Switch app for BangleJS
This app allows you to remotely control devices (or anything else you like!) with:
* [Bangle.js](https://www.espruino.com/Bangle.js) (Hackable JavaScript Smartwatch)
* [EspruinoHub](https://github.com/espruino/EspruinoHub) (Bluetooth Low Energy -> MQTT bridge)
* [Node-RED](https://nodered.org) (Flow-based programming tool)
![Demo of Smart Switch app in action](https://raw.githubusercontent.com/wdmtech/BangleApps/add-video/apps/smtswch/demo.gif)
* Swipe right to turn a device ON
* Swipe left to turn a device OFF
* BTN1 (top-right) - Previous device (page)
* BTN3 (bottom-right) - Next device (page)
> Currently, devices can only be added/removed/changed by editing them in the app's source code.
# How to use
First, you'll need a device that supports BLE.
Install EspruinoHub following the directions at [https://github.com/espruino/EspruinoHub](https://github.com/espruino/EspruinoHub)
Install [Node-RED](https://nodered.org/docs/getting-started)
## Example Node-RED flow
Import the following JSON into Node-RED and configure the MQTT IN node to use your EspruinoHub's MQTT instance (default port is 1883):
```JSON
[{"id":"87c6f73e.f22038","type":"mqtt in","z":"a256522.ca0b0b","name":"⌚BangleJS data","topic":"/ble/advertise/ec:5a:c1:a7:fc:91/data","qos":"2","datatype":"auto","broker":"b961407a.91beb","x":860,"y":100,"wires":[["c37809de.3fc538"]]},{"id":"c37809de.3fc538","type":"function","z":"a256522.ca0b0b","name":"Set topic, remove quotes","func":"msg.topic = \"any_topic_here\";\nmsg.payload = msg.payload.replace(/['\"]+/g, \"\")\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":100,"wires":[["9019be89.5b6d5"]]},{"id":"9019be89.5b6d5","type":"debug","z":"a256522.ca0b0b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1250,"y":100,"wires":[]},{"id":"b961407a.91beb","type":"mqtt-broker","z":"","name":"","broker":"192.168.1.22","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"hello_there","birthQos":"0","birthPayload":"","closeTopic":"bye_now","closeQos":"0","closePayload":"true","willTopic":"bye_now","willQos":"0","willPayload":"true"}]
```
Replace the topic of the MQTT IN node to use the ID of your Bangle.js device, e.g:
`/ble/advertise/ec:5a:c1:a7:fc:91/data`
Once you see the MQTT IN node is configured correctly (it says `connected` below the node itself), try swiping in the Smart Switch app, and
you should see some data in the Debug node.
The possibilities for switching things on and off via Bangle.js are now endless. Have fun!
# How it works
This is the code that does the actual [BLE advertising](https://www.espruino.com/BLE%20Advertising) on the watch itself:
```JS
NRF.setAdvertising({
0xFFFF: [currentPage, page.state]
});
```
# Not working?
If you can't see any data in Node-RED after swiping, check to see if your device is advertising by visiting port 1888 of your EspruinoHub instance:
You should see something like the following:
```
ec:5a:c1:a7:fc:91 - Bangle.js fc91 (RSSI -83)
ffff => {"data":"1,1"}
```
# Any comments?
[Tweet me!](https://twitter.com/BillyWhizzkid)
# Future
PRs welcome!
[ ] Add an HTML GUI for configuring devices inside the Bangle.js App Loader
[ ] Allow enable/disable of buzz/beep on change of device state

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA"))

79
apps/smtswch/app.js Normal file
View File

@ -0,0 +1,79 @@
// Learn more!
// https://www.espruino.com/Reference#l_NRF_setAdvertising
// https://www.espruino.com/Bangle.js#buttons
// Initial graphics setup
g.clear();
g.setFontAlign(0, 0); // center font
// g.setFont("6x8", 8); // bitmap font, 8x magnified
g.setFont("Vector", 40); // vector font, 80px
// Let the app begin!
const storage = require("Storage");
let currentPage = 0;
let pages = [
{
name: "Downstairs",
icon: "light",
state: false
},
{
name: "Upstairs",
icon: "switch",
state: false
}];
function loadPage(page) {
const icon = page.state ? page.icon + "-on" : page.icon + "-off";
Bangle.beep();
g.clear();
g.setFont("Vector", 10);
g.drawString("prev", g.getWidth() - 25, 20);
g.drawString("next", g.getWidth() - 25, 220);
g.setFont("Vector", 15);
g.drawString(page.name, g.getWidth() / 2, 200);
g.setFont("Vector", 40);
g.drawString(page.state ? "On" : "Off", g.getWidth() / 2, g.getHeight() / 2);
g.drawImage(storage.read(`${icon}.img`), g.getWidth() / 2 - 24, g.getHeight() / 2 - 24 - 50);
}
function prevPage() {
if (currentPage > 0) {
currentPage--;
loadPage(pages[currentPage]);
}
}
function nextPage() {
if (currentPage < pages.length - 1) {
currentPage++;
loadPage(pages[currentPage]);
}
}
function swipe(dir) {
const page = pages[currentPage];
page.state = dir == 1;
NRF.setAdvertising({
0xFFFF: [currentPage, page.state]
});
loadPage(page);
// optional - this keeps the watch LCD lit up
g.flip();
Bangle.buzz();
}
Bangle.on('swipe', swipe);
setWatch(prevPage, BTN, {edge: "rising", debounce: 50, repeat: true});
setWatch(nextPage, BTN3, {edge: "rising", debounce: 50, repeat: true});
loadPage(pages[currentPage]);

BIN
apps/smtswch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGeJAAwttGMotLGMQiD1uzAAWtGEgtE64ACF5IwbFwYtESUouGFpowaFywvXDIS7CFyIwXLwouSF6peF1ovrRqowWF4heEstlApIveDolfAAIEGF76OGFYQuMF6+zdo4uOF6+tF49lFwK9KF7AAJLxovUGBiOhF+IwLF5guWF+AwKF5YuYGBQvKFzQwJF5IucGBAvIFzwwHF44ugF+AwFF4wui/2CABQvrr1YAAIvjrwoDAAwvjFhFeR8onDX/4vcXxIvkYA73BR0gACYA4umMI4uoGAouqAH4AK"))

1
apps/smtswch/light-on.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AT5gAGFtoxlFpYxhFp4xeFyYwaFyowZF9wuXGC4vuFzIwVF9wdK53OApIwYDRHN6gAC5oFFF8QoC5wyIMRAvZ5wkERgJbCBQqPfEoKGGL4S/j5i3GFwS/jK5BnIF6owMW4S8KFygvKSIQDFF85bBF8QwKF54uUF+AwJF5wuWF+AwIF5ouYGBAvMFzQwHF5YucGAwvKFzwwFF5IugAAOCAA1erAABF0X+rwoDAAwvjFhFeMYIvkE4QAHF8a/vwS+JF8jAHe4KOkGAaQFroumAAUrAAQtpGAgusAH4A/AFI="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AI1gAEFlgAEz2WAAm6ABwuPxGCAAgJC0wwVGJQtIAAWIGIWXF6gxIEAItIMAgABMCowGFyKSGGCulRhQvHegovVLySRGF6QwBLyjyaF4IuQBAaQX3WmF5wIG0ovXXxaZJYDLuMF8SPHRRCPed4mIcwaNJd7YvBAA4uKH4OXF63+/wuHLxi+YF4JgHLxiOXFwJgHLxmmFwYvXGAqNQFzAwELxKMBdjQwJMAwtCRgovRFpDDIAAjqEFyItLGRQWQAH4A/AH4A/AH4A/AH4A/AH4AP"))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA"))

View File

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

View File

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

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

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

219
bin/apploader.js Normal file
View File

@ -0,0 +1,219 @@
#!/bin/node
/* Simple Command-line app loader for Node.js
===============================================
NOTE: This needs the '@abandonware/noble' library to be installed.
However we don't want this in package.json (at least
as a normal dependency) because we want `sanitycheck.js`
to be able to run *quickly* in travis for every commit,
and we don't want NPM pulling in (and compiling native modules)
for Noble.
*/
var SETTINGS = {
pretokenise : true
};
var Utils = require("../js/utils.js");
var AppInfo = require("../js/appinfo.js");
var apps;
function ERROR(msg) {
console.error(msg);
process.exit(1);
}
try {
apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json"));
} catch(e) {
ERROR("'apps.json' could not be loaded");
}
var args = process.argv;
if (args.length==3 && args[2]=="list") cmdListApps();
else if (args.length==4 && args[2]=="install") cmdInstallApp(args[3]);
else {
console.log(`apploader.js
-------------
USAGE:
apploader.js list
apploader.js install appname
`);
process.exit(0);
}
function cmdListApps() {
console.log(apps.map(a=>a.id).join("\n"));
}
function cmdInstallApp(appId) {
var app = apps.find(a=>a.id==appId);
if (!app) ERROR(`App ${JSON.stringify(appId)} not found`);
if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`);
return AppInfo.getFiles(app, {
fileGetter:function(url) {
return Promise.resolve(require("fs").readFileSync(url).toString());
}, settings : SETTINGS}).then(files => {
//console.log(files);
var command = files.map(f=>f.cmd).join("\n")+"\n";
bangleSend(command).then(() => process.exit(0));
});
}
function bangleSend(command) {
var noble = require('noble');
var log = function() {
var args = [].slice.call(arguments);
console.log("UART: "+args.join(" "));
}
var RESET = true;
var DEVICEADDRESS = "";
var complete = false;
var foundDevices = [];
var flowControlPaused = false;
var btDevice;
var txCharacteristic;
var rxCharacteristic;
return new Promise((resolve,reject) => {
function foundDevice(dev) {
if (btDevice!==undefined) return;
log("Connecting to "+dev.address);
noble.stopScanning();
connect(dev, function() {
// Connected!
function writeCode() {
log("Writing code...");
write(command, function() {
complete = true;
btDevice.disconnect();
});
}
if (RESET) {
setTimeout(function() {
log("Resetting...");
write("\x03\x10reset()\n", function() {
setTimeout(writeCode, 1000);
});
}, 500);
} else
setTimeout(writeCode, 1000);
});
}
function connect(dev, callback) {
btDevice = dev;
log("BT> Connecting");
btDevice.on('disconnect', function() {
log("Disconnected");
setTimeout(function() {
if (complete) resolve();
else reject("Disconnected but not complete");
}, 500);
});
btDevice.connect(function (error) {
if (error) {
log("BT> ERROR Connecting",error);
btDevice = undefined;
return;
}
log("BT> Connected");
btDevice.discoverAllServicesAndCharacteristics(function(error, services, characteristics) {
function findByUUID(list, uuid) {
for (var i=0;i<list.length;i++)
if (list[i].uuid==uuid) return list[i];
return undefined;
}
var btUARTService = findByUUID(services, "6e400001b5a3f393e0a9e50e24dcca9e");
txCharacteristic = findByUUID(characteristics, "6e400002b5a3f393e0a9e50e24dcca9e");
rxCharacteristic = findByUUID(characteristics, "6e400003b5a3f393e0a9e50e24dcca9e");
if (error || !btUARTService || !txCharacteristic || !rxCharacteristic) {
log("BT> ERROR getting services/characteristics");
log("Service "+btUARTService);
log("TX "+txCharacteristic);
log("RX "+rxCharacteristic);
btDevice.disconnect();
txCharacteristic = undefined;
rxCharacteristic = undefined;
btDevice = undefined;
return openCallback();
}
rxCharacteristic.on('data', function (data) {
var s = "";
for (var i=0;i<data.length;i++) {
var ch = data[i];
if (ch==19) {
log("XOFF");
flowControlPaused = 200;
} else if (ch==17) {
log("XON");
flowControlPaused = false;
} else
s+=String.fromCharCode(ch);
}
log("Received", JSON.stringify(s));
});
rxCharacteristic.subscribe(function() {
callback();
});
});
});
};
function write(data, callback) {
var amt = 0;
var total = data.length;
var progress=0;
function writeAgain() {
if (flowControlPaused) {
flowControlPaused--;
if (flowControlPaused==0)
log("TIMEOUT - sending");
setTimeout(writeAgain,50);
return;
}
if (!data.length) return callback();
var d = data.substr(0,20);
data = data.substr(20);
var buf = new Buffer(d.length);
progress++;
if (progress>=10) {
log("Writing "+amt+"/"+total);
progress=0;
}
//log("Writing ",JSON.stringify(d));
amt += d.length;
for (var i = 0; i < buf.length; i++)
buf.writeUInt8(d.charCodeAt(i), i);
txCharacteristic.write(buf, false, writeAgain);
}
writeAgain();
}
function disconnect() {
btDevice.disconnect();
}
log("Discovering...");
noble.on('discover', function(dev) {
if (!dev.advertisement) return;
if (!dev.advertisement.localName) return;
var a = dev.address.toString();
if (foundDevices.indexOf(a)>=0) return;
foundDevices.push(a);
log("Found device: ",a,dev.advertisement.localName);
if (a == DEVICEADDRESS)
return foundDevice(dev);
else if (DEVICEADDRESS=="" && dev.advertisement.localName.indexOf("Bangle.js")==0) {
return foundDevice(dev);
}
});
noble.startScanning([], true);
});
}

View File

@ -3,6 +3,9 @@
Mashes together a bunch of different apps to make
a single firmware JS file which can be uploaded.
*/
var SETTINGS = {
pretokenise : true
};
var path = require('path');
var ROOTDIR = path.join(__dirname, '..');
@ -16,7 +19,7 @@ var APPS = [ // IDs of apps to install
var MINIFY = true;
var fs = require("fs");
var AppInfo = require(ROOTDIR+"/appinfo.js");
var AppInfo = require(ROOTDIR+"/js/appinfo.js");
var appjson = JSON.parse(fs.readFileSync(APPJSON).toString());
var appfiles = [];
@ -49,7 +52,10 @@ function fileGetter(url) {
Promise.all(APPS.map(appid => {
var app = appjson.find(app=>app.id==appid);
if (app===undefined) throw new Error(`App ${appid} not found`);
return AppInfo.getFiles(app, fileGetter).then(files => {
return AppInfo.getFiles(app, {
fileGetter : fileGetter,
settings : SETTINGS
}).then(files => {
appfiles = appfiles.concat(files);
});
})).then(() => {

View File

@ -27,14 +27,26 @@ function WARN(s) {
var appsFile, apps;
try {
appsFile = fs.readFileSync(BASEDIR+"apps.json");
appsFile = fs.readFileSync(BASEDIR+"apps.json").toString();
} catch (e) {
ERROR("apps.json not found");
}
try{
apps = JSON.parse(appsFile);
} catch (e) {
console.log(e);
var m = e.toString().match(/in JSON at position (\d+)/);
if (m) {
var char = parseInt(m[1]);
console.log("===============================================");
console.log("LINE "+appsFile.substr(0,char).split("\n").length);
console.log("===============================================");
console.log(appsFile.substr(char-10, 20));
console.log("===============================================");
}
console.log(m);
ERROR("apps.json not valid JSON");
}
const APP_KEYS = [
@ -44,6 +56,7 @@ const APP_KEYS = [
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate'];
const DATA_KEYS = ['name', 'wildcard', 'storageFile'];
const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info
const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ];
function globToRegex(pattern) {
const ESCAPE = '.*+-?^${}()|[]\\';
@ -194,6 +207,8 @@ apps.forEach((app,appIdx) => {
// Do not allow files from different apps to collide
let fileA
while(fileA=allFiles.pop()) {
if (VALID_DUPLICATES.includes(fileA.file))
return;
const nameA = (fileA.file||fileA.data),
globA = globToRegex(nameA),
typeA = fileA.file?'storage':'data'

View File

@ -138,6 +138,14 @@
<button class="btn" id="removeall">Remove all Apps</button>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite">Install favourite apps</button></p>
<h3>Settings</h3>
<div class="form-group">
<label class="form-switch">
<input type="checkbox" id="settings-pretokenise">
<i class="form-icon"></i> Pretokenise apps before upload (smaller, faster apps)
</label>
<button class="btn" id="defaultsettings">Default settings</button>
</div>
</div>
</div>
@ -156,6 +164,7 @@
<script src="js/comms.js"></script>
<script src="js/appinfo.js"></script>
<script src="js/index.js"></script>
<script src="js/espruinotools.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="js/pwa.js" defer></script>
</body>

View File

@ -2,15 +2,34 @@ function toJS(txt) {
return JSON.stringify(txt);
}
if ("undefined"!=typeof module)
Espruino = require("./espruinotools.js");
var AppInfo = {
getFiles : (app,fileGetter) => {
/* Get files needed for app.
options = {
fileGetter : callback for getting URL,
settings : global settings object
}
*/
getFiles : (app,options) => {
return new Promise((resolve,reject) => {
// Load all files
Promise.all(app.storage.map(storageFile => {
if (storageFile.content)
return Promise.resolve(storageFile);
else if (storageFile.url)
return fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => {
return options.fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => {
if (storageFile.url.endsWith(".js") && !storageFile.url.endsWith(".min.js")) { // if original file ends in '.js'...
return Espruino.transform(content, {
SET_TIME_ON_WRITE : false,
PRETOKENISE : options.settings.pretokenise,
//MINIFICATION_LEVEL : "ESPRIMA", // disable due to https://github.com/espruino/BangleApps/pull/355#issuecomment-620124162
builtinModules : "Flash,Storage,heatshrink,tensorflow,locale"
});
} else
return content;
}).then(content => {
return {
name : storageFile.name,
content : content,

View File

@ -10,7 +10,10 @@ reset : (opt) => new Promise((resolve,reject) => {
}),
uploadApp : (app,skipReset) => { // expects an apps.json structure (i.e. with `storage`)
Progress.show({title:`Uploading ${app.name}`,sticky:true});
return AppInfo.getFiles(app, httpGet).then(fileContents => {
return AppInfo.getFiles(app, {
fileGetter : httpGet,
settings : SETTINGS
}).then(fileContents => {
return new Promise((resolve,reject) => {
console.log("uploadApp",fileContents.map(f=>f.name).join(", "));
var maxBytes = fileContents.reduce((b,f)=>b+f.content.length, 0)||1;

6806
js/espruinotools.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,11 @@
var appJSON = []; // List of apps and info from apps.json
var appsInstalled = []; // list of app JSON
var files = []; // list of files on Bangle
const FAVOURITE = "favouriteapps.json";
var DEFAULTSETTINGS = {
pretokenise : true,
favourites : ["boot","launch","setting"]
};
var SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone
httpGet("apps.json").then(apps=>{
try {
@ -143,23 +147,18 @@ function handleAppInterface(app) {
});
}
function getAppFavourites() {
var f = localStorage.getItem(FAVOURITE);
return (f === null) ? ["boot","launch","setting"] : JSON.parse(f);
}
function changeAppFavourite(favourite, app) {
var favourites = getAppFavourites();
var favourites = SETTINGS.favourites;
if (favourite) {
favourites = favourites.concat([app.id]);
SETTINGS.favourites = SETTINGS.favourites.concat([app.id]);
} else {
if ([ "boot","setting"].includes(app.id)) {
showToast(app.name + ' is required, can\'t remove it' , 'warning');
}else {
favourites = favourites.filter(e => e != app.id);
SETTINGS.favourites = SETTINGS.favourites.filter(e => e != app.id);
}
}
localStorage.setItem(FAVOURITE, JSON.stringify(favourites));
saveSettings();
refreshLibrary();
}
@ -192,7 +191,7 @@ function refreshFilter(){
function refreshLibrary() {
var panelbody = document.querySelector("#librarycontainer .panel-body");
var visibleApps = appJSON;
var favourites = getAppFavourites();
var favourites = SETTINGS.favourites;
if (activeFilter) {
if ( activeFilter == "favourites" ) {
@ -590,6 +589,49 @@ if (window.location.host=="banglejs.com") {
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.';
}
// Settings
var SETTINGS_HOOKS = {}; // stuff to get called when a setting is loaded
/// Load settings and update controls
function loadSettings() {
var j = localStorage.getItem("settings");
if (typeof j != "string") return;
try {
var s = JSON.parse(j);
Object.keys(s).forEach( k => {
SETTINGS[k]=s[k];
if (SETTINGS_HOOKS[k]) SETTINGS_HOOKS[k]();
} );
} catch (e) {
console.error("Invalid settings");
}
}
/// Save settings
function saveSettings() {
localStorage.setItem("settings", JSON.stringify(SETTINGS));
console.log("Changed settings", SETTINGS);
}
// Link in settings DOM elements
function settingsCheckbox(id, name) {
var setting = document.getElementById(id);
function update() {
setting.checked = SETTINGS[name];
}
SETTINGS_HOOKS[name] = update;
setting.addEventListener('click', function() {
SETTINGS[name] = setting.checked;
saveSettings();
});
}
settingsCheckbox("settings-pretokenise", "pretokenise");
loadSettings();
document.getElementById("defaultsettings").addEventListener("click",event=>{
SETTINGS = JSON.parse(JSON.stringify(DEFAULTSETTINGS)); // clone
saveSettings();
loadSettings(); // update all settings
refreshLibrary(); // favourites were in settings
});
document.getElementById("settime").addEventListener("click",event=>{
Comms.setTime().then(()=>{
showToast("Time set successfully","success");
@ -620,9 +662,9 @@ document.getElementById("installdefault").addEventListener("click",event=>{
});
});
// Install all favoutrie apps in one go
// Install all favourite apps in one go
document.getElementById("installfavourite").addEventListener("click",event=>{
var favApps = getAppFavourites();
var favApps = SETTINGS.favourites;
installMultipleApps(favApps, "favourite").catch(err=>{
Progress.hide({sticky:true});
showToast("App Install failed, "+err,"error");