Merge remote-tracking branch 'upstream/master' into ht_indication
|
@ -1,3 +1,4 @@
|
|||
.htaccess
|
||||
node_modules
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -18,7 +18,7 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
|
|||
* 10600 steps
|
||||

|
||||
|
||||
## Features
|
||||
## Features Widget
|
||||
|
||||
* Two line display
|
||||
* Can display distance (in km) or steps in each line
|
||||
|
@ -32,22 +32,23 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a
|
|||
* Steps are saved to a file and read-in at start (to not lose step progress)
|
||||
* Settings can be changed in Settings - App/widget settings - Active Pedometer
|
||||
|
||||
## Features App
|
||||
|
||||
* The app accesses the data stored for the current day
|
||||
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
|
||||
|
||||
## Data storage
|
||||
|
||||
* Data is stored to a file
|
||||
* Data is stored to a file named activepedomYYYYMMDD.data (activepedom20200427.data)
|
||||
* One file is created for each day
|
||||
* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime
|
||||
* now is UNIX timestamp in ms
|
||||
* You can chose the app to watch a steps graph
|
||||
* 'now' is UNIX timestamp in ms
|
||||
* You can use the app to watch a steps graph
|
||||
* You can import the file into Excel
|
||||
* The file does not include a header
|
||||
* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400)
|
||||
* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss
|
||||
|
||||
## App
|
||||
|
||||
* The app accesses the data stored for the current day
|
||||
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
|
||||
|
||||
## Settings
|
||||
|
||||
* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# Ball Maze
|
||||
|
||||
Navigate a ball through a maze by tilting your watch.
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Select a maze size to begin the game.
|
||||
Tilt your watch to steer the ball towards the target and advance to the next level.
|
||||
|
||||
## Creator
|
||||
|
||||
Richard de Boer <rigrig+banglejs@tubul.net>
|
|
@ -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();
|
||||
})();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4AU9wAOCw0OC5/gFyowHC+Hs5gACC7HhiMRjwXSCoIADC5wCB4MSkIXDGIoXKiUikQwJC5PhCwIXFGAgXJFwRHEGAnOC5HhC5IwC5gXJIw4XF4AXKFwwXEGAoXCiKlFMAzNCgDpDC4QAKcgZJBC6wADF6kAhgXP5xfEC58SC4iNCC4nhC5McC4S/DC6a9DC4IACC5MhC4XOC5HuLxPMC4PuC5IwHkUeC44ABA4IACFw5cBC5owEkUhjwXPGAyMCC5wxDLgIACC54ADC94AGC7sOCx/gC4owQCwwA/AH4AMA"))
|
After Width: | Height: | Size: 444 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
|
@ -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
|
|
@ -12,8 +12,13 @@
|
|||
date.setMonth(1, 3) // februari: months are zero-indexed
|
||||
const localized = locale.date(date, true)
|
||||
locale.dayFirst = /3.*2/.test(localized)
|
||||
|
||||
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(),
|
||||
height: g.getWidth(),
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
0.01: Basic calendar
|
|
@ -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
|
|
@ -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=="
|
||||
)
|
||||
)
|
|
@ -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" });
|
After Width: | Height: | Size: 540 B |
|
@ -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/)
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Core functionnality
|
||||
0.02: Offer to enable HID if disabled. Handle with/without media keys
|
|
@ -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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ADhvd6AWVAAIYTCwQABC9JGDJCYX/R+7XYgEE7tACycAgczmAX/C/4X/C6kBiMQCyoABDB0N7vdAgIWCAAIXjxAAQCwkIC6OAC/4X/C/4XbgAXRCwgA/AH4ANA"))
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
After Width: | Height: | Size: 655 B |
|
@ -0,0 +1,2 @@
|
|||
0.01: Core functionnality
|
||||
0.02: Offer to enable HID if disabled. Handle with/without media keys
|
|
@ -4,8 +4,9 @@ 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';
|
||||
if (settings.HID=="kbmedia") {
|
||||
sendHid = function (code, cb) {
|
||||
try {
|
||||
NRF.sendHIDReport([2,0,0,code,0,0,0,0,0], () => {
|
||||
|
@ -17,14 +18,32 @@ if (settings.HID) {
|
|||
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() {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Core functionnality
|
|
@ -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() {
|
||||
|
|
|
@ -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?
|
||||
|
|
@ -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"))
|
|
@ -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"});
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: Init
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ArmYAQCwkDC6MwFyowFC/4XKnGIAAIQFBAWDC5INCBwggEEIYXdxAODnAYCAYIgDDAQXECoIrDE4YrEBwYX/C/4X/C/4X8BwIKBAAM4DgQDBBAQDBBAIXFE4QOCA4QrCAAQHCC7wODCwYhEEAYXGACAX/C5cDCyMwC4YwSCwgA/AH4AlA="))
|
|
@ -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();
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"BTN1": "timer.app.js",
|
||||
"BTN3": "calendar.app.js"
|
||||
}
|
After Width: | Height: | Size: 513 B |
|
@ -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);
|
||||
});
|
|
@ -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"});
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Change img when no fix
|
||||
0.03: Add HTML class for Spectre.CSS
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: 2 players local + improve ai
|
||||
|
|
|
@ -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>
|
286
apps/pong/app.js
|
@ -8,6 +8,7 @@
|
|||
* - Let's make pong, One Man Army Studios, Youtube
|
||||
* - Pong.js, KanoComputing, Github
|
||||
* - Coding Challenge #67: Pong!, The Coding Train, Youtube
|
||||
* - Pixl.js Multiplayer Pong, espruino website
|
||||
*/
|
||||
|
||||
const SCREEN_WIDTH = 240;
|
||||
|
@ -15,6 +16,13 @@ const FPS = 16;
|
|||
const MAX_SCORE = 11;
|
||||
let scores = [0, 0];
|
||||
let aiSpeedRandom = 0;
|
||||
let winnerMessage = '';
|
||||
|
||||
const sound = {
|
||||
ping: () => Bangle.beep(8, 466),
|
||||
pong: () => Bangle.beep(8, 220),
|
||||
fall: () => Bangle.beep(16*3, 494).then(_ => Bangle.beep(32*3, 3322))
|
||||
};
|
||||
|
||||
function Vector(x, y) {
|
||||
this.x = x;
|
||||
|
@ -28,12 +36,18 @@ Vector.prototype.add = function (x) {
|
|||
|
||||
const constrain = (n, low, high) => Math.max(Math.min(n, high), low);
|
||||
const random = (min, max) => Math.random() * (max - min) + min;
|
||||
const intersects = (circ, rect) => {
|
||||
var c1 = circ.pos, c2 = {x: circ.pos.x+circ.r, y: circ.pos.y+circ.r};
|
||||
var r1 = rect.pos, r2 = {x: rect.pos.x+rect.width*2, y: rect.pos.y+rect.height};
|
||||
return !(c1.x > r2.x || c2.x < r1.x ||
|
||||
c1.y > r2.y || c2.y < r1.y);
|
||||
};
|
||||
const intersects = (circ, rect, right) => {
|
||||
var c = circ.pos;
|
||||
var r = circ.r;
|
||||
if (c.y - r < rect.pos.y + rect.height && c.y + r > rect.pos.y) {
|
||||
if (right) {
|
||||
return c.x + r > rect.pos.x - rect.width*2 && c.x < rect.pos.x + rect.width
|
||||
} else {
|
||||
return c.x - r < rect.pos.x + rect.width*2 && c.x > rect.pos.x - rect.width
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///////////////////////////// Ball //////////////////////////////////////////
|
||||
|
||||
|
@ -45,12 +59,26 @@ function Ball() {
|
|||
|
||||
this.reset();
|
||||
}
|
||||
Ball.prototype.show = function () {
|
||||
Ball.prototype.reset = function() {
|
||||
this.speed = this.originalSpeed;
|
||||
var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
|
||||
var bounceAngle = Math.PI/6;
|
||||
this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
|
||||
this.ballReturn = 0;
|
||||
};
|
||||
Ball.prototype.restart = function() {
|
||||
this.reset();
|
||||
ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
|
||||
player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
|
||||
};
|
||||
Ball.prototype.show = function (invert) {
|
||||
if (this.prevPos != null) {
|
||||
g.setColor(0);
|
||||
g.setColor(invert ? -1 : 0);
|
||||
g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r);
|
||||
}
|
||||
g.setColor(-1);
|
||||
g.setColor(invert ? 0 : -1);
|
||||
g.fillCircle(this.pos.x, this.pos.y, this.r);
|
||||
this.prevPos = {
|
||||
x: this.pos.x,
|
||||
|
@ -58,55 +86,62 @@ Ball.prototype.show = function () {
|
|||
r: this.r
|
||||
};
|
||||
};
|
||||
Ball.prototype.bouncePlayer = function (multiplyX, multiplyY, player) {
|
||||
function bounceAngle(playerY, ballY, playerHeight, maxHangle) {
|
||||
let relativeIntersectY = (playerY + (playerHeight/2)) - ballY;
|
||||
let normalizedRelativeIntersectionY = relativeIntersectY / (playerHeight/2);
|
||||
let bounceAngle = normalizedRelativeIntersectionY * maxHangle;
|
||||
return { x: Math.cos(bounceAngle), y: -Math.sin(bounceAngle) };
|
||||
}
|
||||
Ball.prototype.bouncePlayer = function (directionX, directionY, player) {
|
||||
this.ballReturn++;
|
||||
this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed);
|
||||
var relativeIntersectY = (player.pos.y+(player.height/2)) - this.pos.y;
|
||||
var normalizedRelativeIntersectionY = (relativeIntersectY/(player.height/2));
|
||||
var MAX_BOUNCE_ANGLE = 4 * Math.PI/12;
|
||||
var bounceAngle = normalizedRelativeIntersectionY * MAX_BOUNCE_ANGLE;
|
||||
this.velocity.x = this.speed * Math.cos(bounceAngle) * multiplyX;
|
||||
this.velocity.y = this.speed * -Math.sin(bounceAngle) * multiplyY;
|
||||
var angle = bounceAngle(player.pos.y, this.pos.y, player.height, MAX_BOUNCE_ANGLE)
|
||||
this.velocity.x = this.speed * angle.x * directionX;
|
||||
this.velocity.y = this.speed * angle.y * directionY;
|
||||
this.ballReturn % 2 === 0 ? sound.ping() : sound.pong();
|
||||
};
|
||||
Ball.prototype.bounce = function (multiplyX, multiplyY, player) {
|
||||
Ball.prototype.bounce = function (directionX, directionY, player) {
|
||||
if (player)
|
||||
return this.bouncePlayer(multiplyX, multiplyY, player);
|
||||
return this.bouncePlayer(directionX, directionY, player);
|
||||
|
||||
if (multiplyX) {
|
||||
this.velocity.x = Math.abs(this.velocity.x) * multiplyX;
|
||||
if (directionX) {
|
||||
this.velocity.x = Math.abs(this.velocity.x) * directionX;
|
||||
}
|
||||
if (multiplyY) {
|
||||
this.velocity.y = Math.abs(this.velocity.y) * multiplyY;
|
||||
if (directionY) {
|
||||
this.velocity.y = Math.abs(this.velocity.y) * directionY;
|
||||
}
|
||||
};
|
||||
Ball.prototype.checkWallsCollision = function () {
|
||||
Ball.prototype.fall = function (playerId) {
|
||||
scores[playerId]++;
|
||||
if (scores[playerId] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
if (playerId === 1) {
|
||||
winnerMessage = startOption === 0 ? "AI Wins!" : "Player 2 Wins!";
|
||||
} else {
|
||||
winnerMessage = startOption === 0 ? "You Win!" : "Player 1 Wins!";
|
||||
}
|
||||
} else {
|
||||
sound.fall();
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
Ball.prototype.wallCollision = function () {
|
||||
if (this.pos.y < 0) {
|
||||
this.bounce(0, 1);
|
||||
} else if (this.pos.y > SCREEN_WIDTH) {
|
||||
this.bounce(0, -1);
|
||||
} else if (this.pos.x < 0) {
|
||||
scores[1]++;
|
||||
if (scores[1] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
winnerMessage = "AI Wins!";
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
this.fall(1);
|
||||
} else if (this.pos.x > SCREEN_WIDTH) {
|
||||
scores[0]++;
|
||||
if (scores[0] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
winnerMessage = "You Win!";
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
this.fall(0);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
Ball.prototype.checkPlayerCollision = function (player) {
|
||||
Ball.prototype.playerCollision = function (player) {
|
||||
if (intersects(this, player)) {
|
||||
if (this.pos.x < SCREEN_WIDTH/2) {
|
||||
this.bounce(1, 1, player);
|
||||
|
@ -120,8 +155,8 @@ Ball.prototype.checkPlayerCollision = function (player) {
|
|||
}
|
||||
return false;
|
||||
};
|
||||
Ball.prototype.checkCollisions = function () {
|
||||
return this.checkWallsCollision() || this.checkPlayerCollision(player) || this.checkPlayerCollision(ai);
|
||||
Ball.prototype.collisions = function () {
|
||||
return this.wallCollision() || this.playerCollision(player) || this.playerCollision(ai);
|
||||
};
|
||||
Ball.prototype.updatePosition = function () {
|
||||
var elapsed = new Date().getTime() - this.lastUpdate;
|
||||
|
@ -132,31 +167,20 @@ Ball.prototype.updatePosition = function () {
|
|||
Ball.prototype.update = function () {
|
||||
this.updatePosition();
|
||||
this.lastUpdate = new Date().getTime();
|
||||
this.checkCollisions();
|
||||
};
|
||||
Ball.prototype.reset = function() {
|
||||
this.speed = this.originalSpeed;
|
||||
var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
|
||||
var bounceAngle = Math.PI/6;
|
||||
this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
|
||||
};
|
||||
Ball.prototype.restart = function() {
|
||||
ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
|
||||
player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
|
||||
this.collisions();
|
||||
};
|
||||
|
||||
//////////////////////////// Player /////////////////////////////////////////
|
||||
|
||||
function Player() {
|
||||
function Player(right) {
|
||||
this.width = 4;
|
||||
this.height = 30;
|
||||
this.pos = new Vector(this.width*2, SCREEN_WIDTH/2 - this.height/2);
|
||||
this.pos = new Vector(right ? SCREEN_WIDTH-this.width : this.width, SCREEN_WIDTH/2 - this.height/2);
|
||||
this.acc = new Vector(0, 0);
|
||||
this.speed = 15;
|
||||
this.maxSpeed = 25;
|
||||
this.prevPos = null;
|
||||
this.right = right;
|
||||
}
|
||||
Player.prototype.show = function () {
|
||||
if (this.prevPos != null) {
|
||||
|
@ -196,11 +220,14 @@ function AI() {
|
|||
AI.prototype = Object.create(Player.prototype);
|
||||
AI.prototype.constructor = Player;
|
||||
AI.prototype.update = function () {
|
||||
var y = ball.pos.y - (this.height/2 * aiSpeedRandom);
|
||||
var yConstrained = constrain(y, 0, SCREEN_WIDTH-this.height);
|
||||
var y = ball.pos.y - this.height/2;
|
||||
var randomizedY = ball.ballReturn < 3 ? y : y + (aiSpeedRandom * this.height/2);
|
||||
var yConstrained = constrain(randomizedY, 0, SCREEN_WIDTH-this.height);
|
||||
this.pos = new Vector(this.pos.x, yConstrained);
|
||||
};
|
||||
|
||||
/////////////////////////////// Scenes ////////////////////////////////////////
|
||||
|
||||
function net() {
|
||||
var dashSize = 5;
|
||||
for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) {
|
||||
|
@ -210,12 +237,6 @@ function net() {
|
|||
}
|
||||
}
|
||||
|
||||
var player = new Player();
|
||||
var ai = new AI();
|
||||
var ball = new Ball();
|
||||
var state = 0;
|
||||
var prevScores = [0, 0];
|
||||
|
||||
function drawScores() {
|
||||
let x1 = SCREEN_WIDTH/4-5;
|
||||
let x2 = SCREEN_WIDTH*3/4-5;
|
||||
|
@ -233,10 +254,80 @@ function drawScores() {
|
|||
|
||||
function drawGameOver() {
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString(winnerMessage, 75, SCREEN_WIDTH/2 - 10);
|
||||
g.drawString(winnerMessage, startOption === 0 ? 55 : 75, SCREEN_WIDTH/2 - 10);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
function showControls(hide) {
|
||||
g.setColor(hide ? 0 : -1);
|
||||
g.setFont("Vector", 8);
|
||||
var topArrowString = `
|
||||
########
|
||||
##
|
||||
## ##
|
||||
### ##
|
||||
### ##
|
||||
###
|
||||
##
|
||||
`;
|
||||
|
||||
var arrows = [Graphics.createImage(topArrowString), Graphics.createImage(`
|
||||
##
|
||||
##
|
||||
####################
|
||||
##
|
||||
##
|
||||
`), Graphics.createImage(topArrowString.split('\n').reverse().join('\n'))
|
||||
];
|
||||
|
||||
g.drawString('UP', 170, 50);
|
||||
g.drawImage(arrows[0], 200, 40);
|
||||
g.drawString('DOWN', 156, 120);
|
||||
g.drawImage(arrows[1], 200, 120);
|
||||
g.drawString('START', 152, 190);
|
||||
g.drawImage(arrows[2], 200, 200);
|
||||
}
|
||||
|
||||
function drawStartScreen(hide) {
|
||||
g.setColor(hide ? 0 : -1);
|
||||
g.setFont("Vector", 10);
|
||||
g.drawString("1 PLAYER", 95, 80);
|
||||
g.drawString("2 PLAYERS", 95, 110);
|
||||
|
||||
const ball1 = new Ball();
|
||||
ball1.prevPos = null;
|
||||
ball1.pos = new Vector(87, 86);
|
||||
ball1.show(hide || !(startOption === 0));
|
||||
|
||||
const ball2 = new Ball();
|
||||
ball2.prevPos = null;
|
||||
ball2.pos = new Vector(87, 116);
|
||||
ball2.show(hide || !(startOption === 1));
|
||||
}
|
||||
|
||||
function drawStartTimer(count, callback) {
|
||||
setTimeout(_ => {
|
||||
player.show();
|
||||
ai.show();
|
||||
net();
|
||||
g.setColor(0);
|
||||
g.fillRect(117-7, 115-7, 117+14, 115+14);
|
||||
if (count >= 0) {
|
||||
g.setFont("Vector", 10);
|
||||
g.drawString(count+1, 115, 115);
|
||||
g.setColor(-1);
|
||||
g.drawString(count === 0 ? 'Go!' : count, 115 - (count === 0 ? 4: 0), 115);
|
||||
drawStartTimer(count - 1, callback);
|
||||
} else {
|
||||
g.setColor(0);
|
||||
g.fillRect(117-7, 115-7, 117+14, 115+14);
|
||||
callback();
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
//////////////////////////////// Main /////////////////////////////////////////
|
||||
|
||||
function onFrame() {
|
||||
if (state === 1) {
|
||||
ball.update();
|
||||
player.update();
|
||||
|
@ -261,22 +352,73 @@ function draw() {
|
|||
drawScores();
|
||||
}
|
||||
|
||||
function startThatGame() {
|
||||
player.show();
|
||||
ai.show();
|
||||
net();
|
||||
drawScores();
|
||||
drawStartTimer(3, () => setInterval(onFrame, 1000 / FPS));
|
||||
}
|
||||
|
||||
var player = new Player();
|
||||
var ai;
|
||||
var ball = new Ball();
|
||||
var state = 0;
|
||||
var prevScores = [0, 0];
|
||||
var playerBle = null;
|
||||
var startOption = 0;
|
||||
|
||||
g.clear();
|
||||
g.setColor(0);
|
||||
g.fillRect(0,0,240,240);
|
||||
showControls();
|
||||
setTimeout(() => {
|
||||
showControls(true);
|
||||
drawStartScreen();
|
||||
}, 2000);
|
||||
|
||||
setInterval(draw, 1000 / FPS);
|
||||
////////////////////////////// Controls ///////////////////////////////////////
|
||||
|
||||
setWatch(o => o.state ? player.up() : player.stop(), BTN1, {repeat: true, edge: 'both'});
|
||||
setWatch(o => o.state ? player.down() : player.stop(), BTN3, {repeat: true, edge: 'both'});
|
||||
//setWatch(o => o.state ? player.down() : player.stop(), BTN5, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
if (state === 0) {
|
||||
if (o.state) {
|
||||
startOption = startOption === 0 ? startOption : startOption - 1;
|
||||
drawStartScreen();
|
||||
}
|
||||
} else o.state ? player.up() : player.stop();
|
||||
}, BTN1, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
if (state === 0) {
|
||||
if (o.state) {
|
||||
startOption = startOption === 1 ? startOption : startOption + 1;
|
||||
drawStartScreen();
|
||||
}
|
||||
} else o.state ? player.down() : player.stop();
|
||||
}, BTN2, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
state++;
|
||||
clearInterval();
|
||||
if (state >= 2) {
|
||||
ball.restart();
|
||||
g.setColor(0);
|
||||
g.fillRect(0, 0, 240, 240);
|
||||
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'});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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) => {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Initial version
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
require("heatshrink").decompress(
|
||||
atob(
|
||||
"mEwxH+AH4A/AEsxAAQso1eyrgvDrmrw4skAAQuDAAIHBrYABFsQvMGLYtGAAOAFweA2WrF4gwYFxAwEFwIvBwowFsIub64AB6wJF6wJB1mGMTFbrmsEYoADHAwAC1dhGCoTCmJhBEYoAM2RiFF6VbleBF6QABGAguSw2sgAwnCAdhXYIwBqwvT2WFDwYvP1YZCwMAlYwT1ZgORogZEqwwB1iRhBoYmGlcAYiZgOBgWFDIzCBAALESYIYvMw4ZHGCuHF5aOKeYgABYiCQMBYeyDZLzBAAQwO2QvPDhbzCeqAvbGAQQBlYvqeYIvteYMreJ7vaACbvQJxwAP1YvLGAeHF7uHFxYvDwovdwovPSDusRxgvEwwvbwwvNGAmrds4vGsOyFy+ysIvPSLqNPGDwuT/xyEwySS2QuEF6BgEYYL0Q1ZIEFyIwGMQIxM1ZcFFyYwHreFw+rSwmy1eHwoSGFygxJABwtXeo4upMSQtdGZorjAH4A/AF4A=="
|
||||
)
|
||||
)
|
|
@ -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();
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: New App! See the README.MD for details on how to use it.
|
|
@ -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)
|
||||
|
||||

|
||||
|
||||
* 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
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA"))
|
|
@ -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]);
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGeJAAwttGMotLGMQiD1uzAAWtGEgtE64ACF5IwbFwYtESUouGFpowaFywvXDIS7CFyIwXLwouSF6peF1ovrRqowWF4heEstlApIveDolfAAIEGF76OGFYQuMF6+zdo4uOF6+tF49lFwK9KF7AAJLxovUGBiOhF+IwLF5guWF+AwKF5YuYGBQvKFzQwJF5IucGBAvIFzwwHF44ugF+AwFF4wui/2CABQvrr1YAAIvjrwoDAAwvjFhFeR8onDX/4vcXxIvkYA73BR0gACYA4umMI4uoGAouqAH4AK"))
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AT5gAGFtoxlFpYxhFp4xeFyYwaFyowZF9wuXGC4vuFzIwVF9wdK53OApIwYDRHN6gAC5oFFF8QoC5wyIMRAvZ5wkERgJbCBQqPfEoKGGL4S/j5i3GFwS/jK5BnIF6owMW4S8KFygvKSIQDFF85bBF8QwKF54uUF+AwJF5wuWF+AwIF5ouYGBAvMFzQwHF5YucGAwvKFzwwFF5IugAAOCAA1erAABF0X+rwoDAAwvjFhFeMYIvkE4QAHF8a/vwS+JF8jAHe4KOkGAaQFroumAAUrAAQtpGAgusAH4A/AFI="))
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AI1gAEFlgAEz2WAAm6ABwuPxGCAAgJC0wwVGJQtIAAWIGIWXF6gxIEAItIMAgABMCowGFyKSGGCulRhQvHegovVLySRGF6QwBLyjyaF4IuQBAaQX3WmF5wIG0ovXXxaZJYDLuMF8SPHRRCPed4mIcwaNJd7YvBAA4uKH4OXF63+/wuHLxi+YF4JgHLxiOXFwJgHLxmmFwYvXGAqNQFzAwELxKMBdjQwJMAwtCRgovRFpDDIAAjqEFyItLGRQWQAH4A/AH4A/AH4A/AH4A/AH4AP"))
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA"))
|
|
@ -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
|
|
@ -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");
|
||||
let icons = {};
|
||||
|
||||
const HEIGHT = g.getHeight();
|
||||
const WIDTH = g.getWidth();
|
||||
const HALF = WIDTH/2;
|
||||
const ORIGINAL_ICON_SIZE = 48;
|
||||
|
||||
const STATE = {
|
||||
settings_open: false,
|
||||
index: 0,
|
||||
target: 240,
|
||||
offset: 0
|
||||
};
|
||||
|
||||
function getPosition(index){
|
||||
return (index*HALF);
|
||||
}
|
||||
|
||||
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))
|
||||
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 apps = [Object.assign({}, exit_app)].concat(raw_apps);
|
||||
apps.push(exit_app);
|
||||
return apps.map((app, i) => {
|
||||
app.x = getPosition(i);
|
||||
return app;
|
||||
});
|
||||
}
|
||||
|
||||
const HEIGHT = g.getHeight();
|
||||
const WIDTH = g.getWidth();
|
||||
const HALF = WIDTH/2;
|
||||
const ANIMATION_FRAME = 4;
|
||||
const ANIMATION_STEP = HALF / ANIMATION_FRAME;
|
||||
const APPS = getApps();
|
||||
|
||||
function getPosition(index){
|
||||
return (index*HALF);
|
||||
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);
|
||||
}
|
||||
|
||||
let current_app = 0;
|
||||
let target = 0;
|
||||
let slideOffset = 0;
|
||||
function render(){
|
||||
const start = Date.now();
|
||||
|
||||
const back = {
|
||||
name: 'BACK',
|
||||
back: true
|
||||
};
|
||||
const ANIMATION_FRAME = settings.frame;
|
||||
const ANIMATION_STEP = Math.floor(HALF / ANIMATION_FRAME);
|
||||
const THRESHOLD = ANIMATION_STEP - 1;
|
||||
|
||||
let icons = {};
|
||||
g.clear();
|
||||
const visibleApps = APPS.filter(app => app.x >= STATE.offset-HALF && app.x <= STATE.offset+WIDTH-HALF );
|
||||
|
||||
const apps = [back].concat(getApps());
|
||||
apps.push(back);
|
||||
visibleApps.forEach(app => {
|
||||
|
||||
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 x = app.x+HALF-STATE.offset;
|
||||
const y = HALF - (HALF*0.3);
|
||||
|
||||
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;
|
||||
let dist = HALF - x;
|
||||
if(dist < 0) dist *= -1;
|
||||
|
||||
const dontRender = x+(HALF/2)<0 || x-(HALF/2)>120;
|
||||
if(dontRender) {
|
||||
delete icons[app.name];
|
||||
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;
|
||||
|
||||
if(app.back){
|
||||
g.setFont('6x8', 1);
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(c,c,c);
|
||||
g.drawString('Back', HALF, HALF);
|
||||
return;
|
||||
}
|
||||
// icon
|
||||
|
||||
//draw 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 });
|
||||
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, size);
|
||||
noIcon(x, y, scale);
|
||||
}
|
||||
}else{
|
||||
noIcon(x, y, size);
|
||||
noIcon(x, y, scale);
|
||||
}
|
||||
//text
|
||||
g.setFont('6x8', 1);
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(c,c,c);
|
||||
g.drawString(app.name, HALF, HEIGHT - (HALF*0.7));
|
||||
|
||||
//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('4x6', 0.25);
|
||||
g.setColor(c,c,c);
|
||||
g.drawString(info, HALF, 110, { scale: scale });
|
||||
}
|
||||
});
|
||||
g.setFont('6x8', 1.5);
|
||||
g.setColor(scale,scale,scale);
|
||||
g.drawString(info, HALF, 215, { scale: scale });
|
||||
}
|
||||
|
||||
function draw(ignoreLoop){
|
||||
g.setColor(0,0,0);
|
||||
g.fillRect(0,0,WIDTH,HEIGHT);
|
||||
drawIcons(slideOffset);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
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);
|
|
@ -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
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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(() => {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
68
js/index.js
|
@ -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");
|
||||
|
|