Merge remote-tracking branch 'upstream/master'
12
README.md
|
@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer.
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { url:"screenshot.png" } ],`
|
||||||
|
|
||||||
|
To get a screenshot you can:
|
||||||
|
|
||||||
|
* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then
|
||||||
|
right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is
|
||||||
|
unable to read data back from the LCD controller).
|
||||||
|
* Run your code in the emulator and use the screenshot button in the bottom right of the window.
|
||||||
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### Online
|
### Online
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A"))
|
require("heatshrink").decompress(atob("mEkgIRO4AFJgPgAocDAoswAocHAokGjAFDhgFFhgFDjEOAoc4gxSE44FDuPjAod//+AAoXfn4FCgPMjJUCmIJBAoU7AoJUCv4CBsACBtwCBuACB4w3CEQIaCKgMBFgQFBgYFCLQMDMIfAg55D4BcDg/gNAcD+B0DSIMcOgiGEjCYEjgFEhhVCUgQ"))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))
|
require("heatshrink").decompress(atob("kkkwIEBgf8AYMB//4AgN///ggEf4E/wED+EACQN8C4Pgh4TBh8BCYMAvEcEoWD4AEBnk4gFggPHwAXBj1wgIwB88An/Ah3gg/+gF+gH/+EH8Ef/+ABAPvuAIBgnyCIQjBBAMAJAIIEuAICFgIIBh14BAMB8eAg0Ajk8KAXBKAU4jwDBg+ADoIXBg4NBnxPBEgPAgP8gZaBg//KoKLBKAIEBMQMAA"))
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY="))
|
require("heatshrink").decompress(atob("kUw4MA///xP5gEH/AMBh//4AHBwF4gEDwEHgEB4fw8EAsf/jEAjPh80AhngjnAgcwAIMB5kA50A+cAmfAtnAhnYmc//8zhln/+c4YjBg0w440Bxk38EB/cP/0B//Dwf/+FxwEf8EGIAJGB2BkCnhiB4EPgF//EDFQIpB+HGgOMnkxwFjh8MsEY4YQHn/x//j//8n/wHYItBCAKFBhgKBKAIQBBgIQC4AQCmAQChkD/v8gcA/wCBBoMA7+39kAPwP/WIMP4aYBCAYhCCAkHAYOAA="))
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Fixed issue with wrong device informations
|
0.02: Fixed issue with wrong device informations
|
||||||
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
0.03: Ensure manufacturer:undefined doesn't overflow screen
|
||||||
|
0.04: Set Bangle.js 2 compatible, show widgets
|
||||||
|
|
|
@ -5,6 +5,7 @@ let menu = {
|
||||||
|
|
||||||
function showMainMenu() {
|
function showMainMenu() {
|
||||||
menu["< Back"] = () => load();
|
menu["< Back"] = () => load();
|
||||||
|
Bangle.drawWidgets();
|
||||||
return E.showMenu(menu);
|
return E.showMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,5 +56,6 @@ function waitMessage() {
|
||||||
E.showMessage("scanning");
|
E.showMessage("scanning");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
scan();
|
scan();
|
||||||
waitMessage();
|
waitMessage();
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "bledetect",
|
"id": "bledetect",
|
||||||
"name": "BLE Detector",
|
"name": "BLE Detector",
|
||||||
"shortName": "BLE Detector",
|
"shortName": "BLE Detector",
|
||||||
"version": "0.03",
|
"version": "0.04",
|
||||||
"description": "Detect BLE devices and show some informations.",
|
"description": "Detect BLE devices and show some informations.",
|
||||||
"icon": "bledetect.png",
|
"icon": "bledetect.png",
|
||||||
"tags": "app,bluetooth,tool",
|
"tags": "app,bluetooth,tool",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"bledetect.app.js","url":"bledetect.js"},
|
{"name":"bledetect.app.js","url":"bledetect.js"},
|
||||||
|
|
|
@ -48,3 +48,4 @@
|
||||||
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.boot.js
|
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname.<priority>.boot.js
|
||||||
0.43: Fix Gadgetbridge handling with Programmable:off
|
0.43: Fix Gadgetbridge handling with Programmable:off
|
||||||
0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)
|
0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)
|
||||||
|
0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
|
||||||
|
|
|
@ -213,7 +213,7 @@ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
|
||||||
var fileSize = boot.length + bootPost.length;
|
var fileSize = boot.length + bootPost.length;
|
||||||
bootFiles.forEach(bootFile=>{
|
bootFiles.forEach(bootFile=>{
|
||||||
// match the size of data we're adding below in bootFiles.forEach
|
// match the size of data we're adding below in bootFiles.forEach
|
||||||
fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+1;
|
fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2;
|
||||||
});
|
});
|
||||||
// write file in chunks (so as not to use up all RAM)
|
// write file in chunks (so as not to use up all RAM)
|
||||||
require('Storage').write('.boot0',boot,0,fileSize);
|
require('Storage').write('.boot0',boot,0,fileSize);
|
||||||
|
@ -231,8 +231,8 @@ bootFiles.forEach(bootFile=>{
|
||||||
var bf = require('Storage').read(bootFile);
|
var bf = require('Storage').read(bootFile);
|
||||||
require('Storage').write('.boot0',bf,fileOffset);
|
require('Storage').write('.boot0',bf,fileOffset);
|
||||||
fileOffset+=bf.length;
|
fileOffset+=bf.length;
|
||||||
require('Storage').write('.boot0',"\n",fileOffset);
|
require('Storage').write('.boot0',";\n",fileOffset);
|
||||||
fileOffset+=1;
|
fileOffset+=2;
|
||||||
});
|
});
|
||||||
require('Storage').write('.boot0',bootPost,fileOffset);
|
require('Storage').write('.boot0',bootPost,fileOffset);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "boot",
|
"id": "boot",
|
||||||
"name": "Bootloader",
|
"name": "Bootloader",
|
||||||
"version": "0.44",
|
"version": "0.45",
|
||||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||||
"icon": "bootloader.png",
|
"icon": "bootloader.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
|
0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up".
|
||||||
|
0.03: Fix the clock for dark mode.
|
||||||
|
|
|
@ -76,7 +76,7 @@ function draw_clock(){
|
||||||
// g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y);
|
// g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y);
|
||||||
// g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius);
|
// g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius);
|
||||||
|
|
||||||
g.setColor(g.theme.fg);
|
g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg);
|
||||||
let ticks = [0, 90, 180, 270];
|
let ticks = [0, 90, 180, 270];
|
||||||
ticks.forEach((item)=>{
|
ticks.forEach((item)=>{
|
||||||
let agl = item+180;
|
let agl = item+180;
|
||||||
|
@ -92,13 +92,13 @@ function draw_clock(){
|
||||||
let minute_agl = minute_angle(date);
|
let minute_agl = minute_angle(date);
|
||||||
g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); //
|
g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); //
|
||||||
g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); //
|
g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); //
|
||||||
g.setColor(g.theme.fg);
|
g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg);
|
||||||
g.fillCircle(clock_center.x, clock_center.y, 6);
|
g.fillCircle(clock_center.x, clock_center.y, 6);
|
||||||
g.setColor(g.theme.bg);
|
g.setColor(g.theme.dark ? g.theme.fg : g.theme.bg);
|
||||||
g.fillCircle(clock_center.x, clock_center.y, 3);
|
g.fillCircle(clock_center.x, clock_center.y, 3);
|
||||||
|
|
||||||
// draw minute ticks. Takes long time to draw!
|
// draw minute ticks. Takes long time to draw!
|
||||||
g.setColor(g.theme.fg);
|
g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg);
|
||||||
for (var i=0; i<60; i++){
|
for (var i=0; i<60; i++){
|
||||||
let agl = i*6+180;
|
let agl = i*6+180;
|
||||||
g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180});
|
g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "crowclk",
|
"id": "crowclk",
|
||||||
"name": "Crow Clock",
|
"name": "Crow Clock",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
|
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
|
||||||
"icon": "crow_clock.png",
|
"icon": "crow_clock.png",
|
||||||
"screenshots": [{"url":"screenshot_crow.png"}],
|
"screenshots": [{"url":"screenshot_crow.png"}],
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Initial version
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Cycling
|
||||||
|
> Displays data from a BLE Cycling Speed and Cadence sensor.
|
||||||
|
|
||||||
|
*This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.*
|
||||||
|
|
||||||
|
The following data are displayed:
|
||||||
|
- curent speed
|
||||||
|
- moving time
|
||||||
|
- average speed
|
||||||
|
- maximum speed
|
||||||
|
- trip distance
|
||||||
|
- total distance
|
||||||
|
|
||||||
|
Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over.
|
||||||
|
|
||||||
|
**Cadence / Crank features are currently not implemented**
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Open the app and connect to a CSC sensor.
|
||||||
|
|
||||||
|
Upon first connection, close the app afain and enter the settings app to configure the wheel circumference. The total circumference is (cm + mm) - it is split up into two values for ease of configuration. Check the status screen inside the Cycling app while connected to see the address of the currently connected sensor (if you need to differentiate between multiple sensors).
|
||||||
|
|
||||||
|
Inside the Cycling app, use button / tap screen to:
|
||||||
|
- cycle through screens (if connected)
|
||||||
|
- reconnect (if connection aborted)
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
* Sensor battery status
|
||||||
|
* Implement crank events / show cadence
|
||||||
|
* Bangle.js 1 compatibility
|
||||||
|
* Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage.
|
|
@ -0,0 +1,111 @@
|
||||||
|
// UUID of the Bluetooth CSC Service
|
||||||
|
const SERVICE_UUID = "1816";
|
||||||
|
// UUID of the CSC measurement characteristic
|
||||||
|
const MEASUREMENT_UUID = "2a5b";
|
||||||
|
|
||||||
|
// Wheel revolution present bit mask
|
||||||
|
const FLAGS_WREV_BM = 0x01;
|
||||||
|
// Crank revolution present bit mask
|
||||||
|
const FLAGS_CREV_BM = 0x02;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake BLECSC implementation for the emulator, where it's hard to test
|
||||||
|
* with actual hardware. Generates "random" wheel events (no crank).
|
||||||
|
*
|
||||||
|
* To upload as a module, paste the entire file in the console using this
|
||||||
|
* command: require("Storage").write("blecsc-emu",`<FILE CONTENT HERE>`);
|
||||||
|
*/
|
||||||
|
class BLECSCEmulator {
|
||||||
|
constructor() {
|
||||||
|
this.timeout = undefined;
|
||||||
|
this.interval = 500;
|
||||||
|
this.ccr = 0;
|
||||||
|
this.lwt = 0;
|
||||||
|
this.handlers = {
|
||||||
|
// value
|
||||||
|
// disconnect
|
||||||
|
// wheelEvent
|
||||||
|
// crankEvent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceAddress() {
|
||||||
|
return 'fa:ke:00:de:vi:ce';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the GATT characteristicvaluechanged event.
|
||||||
|
* Consumers must not call this method!
|
||||||
|
*/
|
||||||
|
onValue(event) {
|
||||||
|
// Not interested in non-CSC characteristics
|
||||||
|
if (event.target.uuid != "0x" + MEASUREMENT_UUID) return;
|
||||||
|
|
||||||
|
// Notify the generic 'value' handler
|
||||||
|
if (this.handlers.value) this.handlers.value(event);
|
||||||
|
|
||||||
|
const flags = event.target.value.getUint8(0, true);
|
||||||
|
// Notify the 'wheelEvent' handler
|
||||||
|
if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({
|
||||||
|
cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions
|
||||||
|
lwet: event.target.value.getUint16(5, true), // last wheel event time
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify the 'crankEvent' handler
|
||||||
|
if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({
|
||||||
|
ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions
|
||||||
|
lcet: event.target.value.getUint16(9, true), // last crank event time
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event handler.
|
||||||
|
*
|
||||||
|
* @param {string} event value|disconnect
|
||||||
|
* @param {function} handler handler function that receives the event as its first argument
|
||||||
|
*/
|
||||||
|
on(event, handler) {
|
||||||
|
this.handlers[event] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeEvent() {
|
||||||
|
this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20));
|
||||||
|
this.lwt = (this.lwt + this.interval) % 0x10000;
|
||||||
|
this.ccr++;
|
||||||
|
|
||||||
|
var buffer = new ArrayBuffer(8);
|
||||||
|
var view = new DataView(buffer);
|
||||||
|
view.setUint8(0, 0x01); // Wheel revolution data present bit
|
||||||
|
view.setUint32(1, this.ccr, true); // Cumulative crank revolutions
|
||||||
|
view.setUint16(5, this.lwt, true); // Last wheel event time
|
||||||
|
|
||||||
|
this.onValue({
|
||||||
|
target: {
|
||||||
|
uuid: "0x2a5b",
|
||||||
|
value: view,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and connect to a device which exposes the CSC service.
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the device.
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
if (!this.timeout) return;
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports = BLECSCEmulator;
|
|
@ -0,0 +1,150 @@
|
||||||
|
const SERVICE_UUID = "1816";
|
||||||
|
// UUID of the CSC measurement characteristic
|
||||||
|
const MEASUREMENT_UUID = "2a5b";
|
||||||
|
|
||||||
|
// Wheel revolution present bit mask
|
||||||
|
const FLAGS_WREV_BM = 0x01;
|
||||||
|
// Crank revolution present bit mask
|
||||||
|
const FLAGS_CREV_BM = 0x02;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class communicates with a Bluetooth CSC peripherial using the Espruino NRF library.
|
||||||
|
*
|
||||||
|
* ## Usage:
|
||||||
|
* 1. Register event handlers using the \`on(eventName, handlerFunction)\` method
|
||||||
|
* You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can
|
||||||
|
* have raw characteristic values passed through using the \`value\` event.
|
||||||
|
* 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method
|
||||||
|
* 3. To tear down the connection, call the \`disconnect()\` method
|
||||||
|
*
|
||||||
|
* ## Events
|
||||||
|
* - \`wheelEvent\` - the peripharial sends a notification containing wheel event data
|
||||||
|
* - \`crankEvent\` - the peripharial sends a notification containing crank event data
|
||||||
|
* - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event)
|
||||||
|
* - \`disconnect\` - the peripherial ends the connection or the connection is lost
|
||||||
|
*
|
||||||
|
* Each event can only have one handler. Any call to \`on()\` will
|
||||||
|
* replace a previously registered handler for the same event.
|
||||||
|
*/
|
||||||
|
class BLECSC {
|
||||||
|
constructor() {
|
||||||
|
this.device = undefined;
|
||||||
|
this.ccInterval = undefined;
|
||||||
|
this.gatt = undefined;
|
||||||
|
this.handlers = {
|
||||||
|
// wheelEvent
|
||||||
|
// crankEvent
|
||||||
|
// value
|
||||||
|
// disconnect
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceAddress() {
|
||||||
|
if (!this.device || !this.device.id)
|
||||||
|
return '00:00:00:00:00:00';
|
||||||
|
return this.device.id.split(" ")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
checkConnection() {
|
||||||
|
if (!this.device)
|
||||||
|
console.log("no device");
|
||||||
|
// else
|
||||||
|
// console.log("rssi: " + this.device.rssi);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the GATT characteristicvaluechanged event.
|
||||||
|
* Consumers must not call this method!
|
||||||
|
*/
|
||||||
|
onValue(event) {
|
||||||
|
// Not interested in non-CSC characteristics
|
||||||
|
if (event.target.uuid != "0x" + MEASUREMENT_UUID) return;
|
||||||
|
|
||||||
|
// Notify the generic 'value' handler
|
||||||
|
if (this.handlers.value) this.handlers.value(event);
|
||||||
|
|
||||||
|
const flags = event.target.value.getUint8(0, true);
|
||||||
|
// Notify the 'wheelEvent' handler
|
||||||
|
if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({
|
||||||
|
cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions
|
||||||
|
lwet: event.target.value.getUint16(5, true), // last wheel event time
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify the 'crankEvent' handler
|
||||||
|
if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({
|
||||||
|
ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions
|
||||||
|
lcet: event.target.value.getUint16(9, true), // last crank event time
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the NRF disconnect event.
|
||||||
|
* Consumers must not call this method!
|
||||||
|
*/
|
||||||
|
onDisconnect(event) {
|
||||||
|
console.log("disconnected");
|
||||||
|
if (this.ccInterval)
|
||||||
|
clearInterval(this.ccInterval);
|
||||||
|
|
||||||
|
if (!this.handlers.disconnect) return;
|
||||||
|
this.handlers.disconnect(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event handler.
|
||||||
|
*
|
||||||
|
* @param {string} event wheelEvent|crankEvent|value|disconnect
|
||||||
|
* @param {function} handler function that will receive the event as its first argument
|
||||||
|
*/
|
||||||
|
on(event, handler) {
|
||||||
|
this.handlers[event] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find and connect to a device which exposes the CSC service.
|
||||||
|
*
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
// Register handler for the disconnect event to be passed throug
|
||||||
|
NRF.on('disconnect', this.onDisconnect.bind(this));
|
||||||
|
|
||||||
|
// Find a device, then get the CSC Service and subscribe to
|
||||||
|
// notifications on the CSC Measurement characteristic.
|
||||||
|
// NRF.setLowPowerConnection(true);
|
||||||
|
return NRF.requestDevice({
|
||||||
|
timeout: 5000,
|
||||||
|
filters: [{ services: [SERVICE_UUID] }],
|
||||||
|
}).then(device => {
|
||||||
|
this.device = device;
|
||||||
|
this.device.on('gattserverdisconnected', this.onDisconnect.bind(this));
|
||||||
|
this.ccInterval = setInterval(this.checkConnection.bind(this), 2000);
|
||||||
|
return device.gatt.connect();
|
||||||
|
}).then(gatt => {
|
||||||
|
this.gatt = gatt;
|
||||||
|
return gatt.getPrimaryService(SERVICE_UUID);
|
||||||
|
}).then(service => {
|
||||||
|
return service.getCharacteristic(MEASUREMENT_UUID);
|
||||||
|
}).then(characteristic => {
|
||||||
|
characteristic.on('characteristicvaluechanged', this.onValue.bind(this));
|
||||||
|
return characteristic.startNotifications();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect the device.
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
if (this.ccInterval)
|
||||||
|
clearInterval(this.ccInterval);
|
||||||
|
|
||||||
|
if (!this.gatt) return;
|
||||||
|
try {
|
||||||
|
this.gatt.disconnect();
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports = BLECSC;
|
|
@ -0,0 +1,453 @@
|
||||||
|
const Layout = require('Layout');
|
||||||
|
const storage = require('Storage');
|
||||||
|
|
||||||
|
const SETTINGS_FILE = 'cycling.json';
|
||||||
|
const SETTINGS_DEFAULT = {
|
||||||
|
sensors: {},
|
||||||
|
metric: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const RECONNECT_TIMEOUT = 4000;
|
||||||
|
const MAX_CONN_ATTEMPTS = 2;
|
||||||
|
|
||||||
|
class CSCSensor {
|
||||||
|
constructor(blecsc, display) {
|
||||||
|
// Dependency injection
|
||||||
|
this.blecsc = blecsc;
|
||||||
|
this.display = display;
|
||||||
|
|
||||||
|
// Load settings
|
||||||
|
this.settings = storage.readJSON(SETTINGS_FILE, true) || SETTINGS_DEFAULT;
|
||||||
|
this.wheelCirc = undefined;
|
||||||
|
|
||||||
|
// CSC runtime variables
|
||||||
|
this.movingTime = 0; // unit: s
|
||||||
|
this.lastBangleTime = Date.now(); // unit: ms
|
||||||
|
this.lwet = 0; // last wheel event time (unit: s/1024)
|
||||||
|
this.cwr = -1; // cumulative wheel revolutions
|
||||||
|
this.cwrTrip = 0; // wheel revolutions since trip start
|
||||||
|
this.speed = 0; // unit: m/s
|
||||||
|
this.maxSpeed = 0; // unit: m/s
|
||||||
|
this.speedFailed = 0;
|
||||||
|
|
||||||
|
// Other runtime variables
|
||||||
|
this.connected = false;
|
||||||
|
this.failedAttempts = 0;
|
||||||
|
this.failed = false;
|
||||||
|
|
||||||
|
// Layout configuration
|
||||||
|
this.layout = 0;
|
||||||
|
this.display.useMetricUnits(true);
|
||||||
|
this.deviceAddress = undefined;
|
||||||
|
this.display.useMetricUnits((this.settings.metric));
|
||||||
|
}
|
||||||
|
|
||||||
|
onDisconnect(event) {
|
||||||
|
console.log("disconnected ", event);
|
||||||
|
|
||||||
|
this.connected = false;
|
||||||
|
this.wheelCirc = undefined;
|
||||||
|
|
||||||
|
this.setLayout(0);
|
||||||
|
this.display.setDeviceAddress("unknown");
|
||||||
|
|
||||||
|
if (this.failedAttempts >= MAX_CONN_ATTEMPTS) {
|
||||||
|
this.failed = true;
|
||||||
|
this.display.setStatus("Connection failed after " + MAX_CONN_ATTEMPTS + " attempts.");
|
||||||
|
} else {
|
||||||
|
this.display.setStatus("Disconnected");
|
||||||
|
setTimeout(this.connect.bind(this), RECONNECT_TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCircumference() {
|
||||||
|
if (!this.deviceAddress) return;
|
||||||
|
|
||||||
|
// Add sensor to settings if not present
|
||||||
|
if (!this.settings.sensors[this.deviceAddress]) {
|
||||||
|
this.settings.sensors[this.deviceAddress] = {
|
||||||
|
cm: 223,
|
||||||
|
mm: 0,
|
||||||
|
};
|
||||||
|
storage.writeJSON(SETTINGS_FILE, this.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
const high = this.settings.sensors[this.deviceAddress].cm || 223;
|
||||||
|
const low = this.settings.sensors[this.deviceAddress].mm || 0;
|
||||||
|
this.wheelCirc = (10*high + low) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.connected = false;
|
||||||
|
this.setLayout(0);
|
||||||
|
this.display.setStatus("Connecting");
|
||||||
|
console.log("Trying to connect to BLE CSC");
|
||||||
|
|
||||||
|
// Hook up events
|
||||||
|
this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this));
|
||||||
|
this.blecsc.on('disconnect', this.onDisconnect.bind(this));
|
||||||
|
|
||||||
|
// Scan for BLE device and connect
|
||||||
|
this.blecsc.connect()
|
||||||
|
.then(function() {
|
||||||
|
this.failedAttempts = 0;
|
||||||
|
this.failed = false;
|
||||||
|
this.connected = true;
|
||||||
|
this.deviceAddress = this.blecsc.getDeviceAddress();
|
||||||
|
console.log("Connected to " + this.deviceAddress);
|
||||||
|
|
||||||
|
this.display.setDeviceAddress(this.deviceAddress);
|
||||||
|
this.display.setStatus("Connected");
|
||||||
|
|
||||||
|
this.loadCircumference();
|
||||||
|
|
||||||
|
// Switch to speed screen in 2s
|
||||||
|
setTimeout(function() {
|
||||||
|
this.setLayout(1);
|
||||||
|
this.updateScreen();
|
||||||
|
}.bind(this), 2000);
|
||||||
|
}.bind(this))
|
||||||
|
.catch(function(e) {
|
||||||
|
this.failedAttempts++;
|
||||||
|
this.onDisconnect(e);
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.blecsc.disconnect();
|
||||||
|
this.reset();
|
||||||
|
this.setLayout(0);
|
||||||
|
this.display.setStatus("Disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayout(num) {
|
||||||
|
this.layout = num;
|
||||||
|
if (this.layout == 0) {
|
||||||
|
this.display.updateLayout("status");
|
||||||
|
} else if (this.layout == 1) {
|
||||||
|
this.display.updateLayout("speed");
|
||||||
|
} else if (this.layout == 2) {
|
||||||
|
this.display.updateLayout("distance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.connected = false;
|
||||||
|
this.failed = false;
|
||||||
|
this.failedAttempts = 0;
|
||||||
|
this.wheelCirc = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interact(d) {
|
||||||
|
// Only interested in tap / center button
|
||||||
|
if (d) return;
|
||||||
|
|
||||||
|
// Reconnect in failed state
|
||||||
|
if (this.failed) {
|
||||||
|
this.reset();
|
||||||
|
this.connect();
|
||||||
|
} else if (this.connected) {
|
||||||
|
this.setLayout((this.layout + 1) % 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScreen() {
|
||||||
|
var tripDist = this.cwrTrip * this.wheelCirc;
|
||||||
|
var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0;
|
||||||
|
|
||||||
|
this.display.setTotalDistance(this.cwr * this.wheelCirc);
|
||||||
|
this.display.setTripDistance(tripDist);
|
||||||
|
this.display.setSpeed(this.speed);
|
||||||
|
this.display.setAvg(avgSpeed);
|
||||||
|
this.display.setMax(this.maxSpeed);
|
||||||
|
this.display.setTime(Math.floor(this.movingTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent(event) {
|
||||||
|
// Calculate number of revolutions since last wheel event
|
||||||
|
var dRevs = (this.cwr > 0 ? event.cwr - this.cwr : 0);
|
||||||
|
this.cwr = event.cwr;
|
||||||
|
|
||||||
|
// Increment the trip revolutions counter
|
||||||
|
this.cwrTrip += dRevs;
|
||||||
|
|
||||||
|
// Calculate time delta since last wheel event
|
||||||
|
var dT = (event.lwet - this.lwet)/1024;
|
||||||
|
var now = Date.now();
|
||||||
|
var dBT = (now-this.lastBangleTime)/1000;
|
||||||
|
this.lastBangleTime = now;
|
||||||
|
if (dT<0) dT+=64; // wheel event time wraps every 64s
|
||||||
|
if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this
|
||||||
|
this.lwet = event.lwet;
|
||||||
|
|
||||||
|
// Recalculate current speed
|
||||||
|
if (dRevs>0 && dT>0) {
|
||||||
|
this.speed = dRevs * this.wheelCirc / dT;
|
||||||
|
this.speedFailed = 0;
|
||||||
|
this.movingTime += dT;
|
||||||
|
} else {
|
||||||
|
this.speedFailed++;
|
||||||
|
if (this.speedFailed>3) {
|
||||||
|
this.speed = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update max speed
|
||||||
|
if (this.speed>this.maxSpeed
|
||||||
|
&& (this.movingTime>3 || this.speed<20)
|
||||||
|
&& this.speed<50
|
||||||
|
) this.maxSpeed = this.speed;
|
||||||
|
|
||||||
|
this.updateScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CSCDisplay {
|
||||||
|
constructor() {
|
||||||
|
this.metric = true;
|
||||||
|
this.fontLabel = "6x8";
|
||||||
|
this.fontSmall = "15%";
|
||||||
|
this.fontMed = "18%";
|
||||||
|
this.fontLarge = "32%";
|
||||||
|
this.currentLayout = "status";
|
||||||
|
this.layouts = {};
|
||||||
|
this.layouts.speed = new Layout({
|
||||||
|
type: "v",
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "speed_g",
|
||||||
|
fillx: 1,
|
||||||
|
filly: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{type: undefined, width: 32, halign: -1},
|
||||||
|
{type: "txt", id: "speed", label: "00.0", font: this.fontLarge, bgCol: "#fff", col: "#000", width: 122},
|
||||||
|
{type: "txt", id: "speed_u", label: " km/h", font: this.fontLabel, col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "time_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#000",
|
||||||
|
height: 36,
|
||||||
|
c: [
|
||||||
|
{type: undefined, width: 32, halign: -1},
|
||||||
|
{type: "txt", id: "time", label: "00:00", font: this.fontMed, bgCol: "#000", col: "#fff", width: 122},
|
||||||
|
{type: "txt", id: "time_u", label: "mins", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "stats_g",
|
||||||
|
fillx: 1,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 36,
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "v",
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "max_l", label: "MAX", font: this.fontLabel, col: "#000"},
|
||||||
|
{type: "txt", id: "max", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "v",
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "avg_l", label: "AVG", font: this.fontLabel, col: "#000"},
|
||||||
|
{type: "txt", id: "avg", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{type: "txt", id: "stats_u", label: " km/h", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
this.layouts.distance = new Layout({
|
||||||
|
type: "v",
|
||||||
|
bgCol: "#fff",
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "tripd_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "tripd_l", label: "TRP", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36},
|
||||||
|
{type: "txt", id: "tripd", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118},
|
||||||
|
{type: "txt", id: "tripd_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "totald_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36},
|
||||||
|
{type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118},
|
||||||
|
{type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
this.layouts.status = new Layout({
|
||||||
|
type: "v",
|
||||||
|
c: [
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "status_g",
|
||||||
|
fillx: 1,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 100,
|
||||||
|
c: [
|
||||||
|
{type: "txt", id: "status", label: "Bangle Cycling", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 176, wrap: 1},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "h",
|
||||||
|
id: "addr_g",
|
||||||
|
fillx: 1,
|
||||||
|
pad: 4,
|
||||||
|
bgCol: "#fff",
|
||||||
|
height: 32,
|
||||||
|
c: [
|
||||||
|
{ type: "txt", id: "addr_l", label: "ADDR", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 },
|
||||||
|
{ type: "txt", id: "addr", label: "unknown", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 140 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLayout(layout) {
|
||||||
|
this.currentLayout = layout;
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
this.layouts[layout].update();
|
||||||
|
this.layouts[layout].render();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIfLayoutActive(layout, node) {
|
||||||
|
if (layout != this.currentLayout) return;
|
||||||
|
this.layouts[layout].render(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
useMetricUnits(metric) {
|
||||||
|
this.metric = metric;
|
||||||
|
|
||||||
|
// console.log("using " + (metric ? "metric" : "imperial") + " units");
|
||||||
|
|
||||||
|
var speedUnit = metric ? "km/h" : "mph";
|
||||||
|
this.layouts.speed.speed_u.label = speedUnit;
|
||||||
|
this.layouts.speed.stats_u.label = speedUnit;
|
||||||
|
|
||||||
|
var distanceUnit = metric ? "km" : "mi";
|
||||||
|
this.layouts.distance.tripd_u.label = distanceUnit;
|
||||||
|
this.layouts.distance.totald_u.label = distanceUnit;
|
||||||
|
|
||||||
|
this.updateLayout(this.currentLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertDistance(meters) {
|
||||||
|
if (this.metric) return meters / 1000;
|
||||||
|
return meters / 1609.344;
|
||||||
|
}
|
||||||
|
|
||||||
|
convertSpeed(mps) {
|
||||||
|
if (this.metric) return mps * 3.6;
|
||||||
|
return mps * 2.23694;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSpeed(speed) {
|
||||||
|
this.layouts.speed.speed.label = this.convertSpeed(speed).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.speed_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvg(speed) {
|
||||||
|
this.layouts.speed.avg.label = this.convertSpeed(speed).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.stats_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMax(speed) {
|
||||||
|
this.layouts.speed.max.label = this.convertSpeed(speed).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.stats_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTime(seconds) {
|
||||||
|
var time = '';
|
||||||
|
var hours = Math.floor(seconds/3600);
|
||||||
|
if (hours) {
|
||||||
|
time += hours + ":";
|
||||||
|
this.layouts.speed.time_u.label = " hrs";
|
||||||
|
} else {
|
||||||
|
this.layouts.speed.time_u.label = "mins";
|
||||||
|
}
|
||||||
|
|
||||||
|
time += String(Math.floor((seconds%3600)/60)).padStart(2, '0') + ":";
|
||||||
|
time += String(seconds % 60).padStart(2, '0');
|
||||||
|
|
||||||
|
this.layouts.speed.time.label = time;
|
||||||
|
this.renderIfLayoutActive("speed", this.layouts.speed.time_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTripDistance(distance) {
|
||||||
|
this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1);
|
||||||
|
this.renderIfLayoutActive("distance", this.layouts.distance.tripd_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTotalDistance(distance) {
|
||||||
|
distance = this.convertDistance(distance);
|
||||||
|
if (distance >= 1000) {
|
||||||
|
this.layouts.distance.totald.label = String(Math.round(distance));
|
||||||
|
} else {
|
||||||
|
this.layouts.distance.totald.label = distance.toFixed(1);
|
||||||
|
}
|
||||||
|
this.renderIfLayoutActive("distance", this.layouts.distance.totald_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeviceAddress(address) {
|
||||||
|
this.layouts.status.addr.label = address;
|
||||||
|
this.renderIfLayoutActive("status", this.layouts.status.addr_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(status) {
|
||||||
|
this.layouts.status.status.label = status;
|
||||||
|
this.renderIfLayoutActive("status", this.layouts.status.status_g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var BLECSC;
|
||||||
|
if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") {
|
||||||
|
// Emulator
|
||||||
|
BLECSC = require("blecsc-emu");
|
||||||
|
} else {
|
||||||
|
// Actual hardware
|
||||||
|
BLECSC = require("blecsc");
|
||||||
|
}
|
||||||
|
var blecsc = new BLECSC();
|
||||||
|
var display = new CSCDisplay();
|
||||||
|
var sensor = new CSCSensor(blecsc, display);
|
||||||
|
|
||||||
|
E.on('kill',()=>{
|
||||||
|
sensor.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.setUI("updown", d => {
|
||||||
|
sensor.interact(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
sensor.connect();
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"id": "cycling",
|
||||||
|
"name": "Bangle Cycling",
|
||||||
|
"shortName": "Cycling",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Display live values from a BLE CSC sensor",
|
||||||
|
"icon": "icons8-cycling-48.png",
|
||||||
|
"tags": "outdoors,exercise,ble,bluetooth",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"cycling.app.js","url":"cycling.app.js"},
|
||||||
|
{"name":"cycling.settings.js","url":"settings.js"},
|
||||||
|
{"name":"blecsc","url":"blecsc.js"},
|
||||||
|
{"name":"cycling.img","url":"cycling.icon.js","evaluate": true}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// This file should contain exactly one function, which shows the app's settings
|
||||||
|
/**
|
||||||
|
* @param {function} back Use back() to return to settings menu
|
||||||
|
*/
|
||||||
|
(function(back) {
|
||||||
|
const storage = require('Storage')
|
||||||
|
const SETTINGS_FILE = 'cycling.json'
|
||||||
|
|
||||||
|
// Set default values and merge with stored values
|
||||||
|
let settings = Object.assign({
|
||||||
|
metric: true,
|
||||||
|
sensors: {},
|
||||||
|
}, (storage.readJSON(SETTINGS_FILE, true) || {}));
|
||||||
|
|
||||||
|
const menu = {
|
||||||
|
'': { 'title': 'Cycling' },
|
||||||
|
'< Back': back,
|
||||||
|
'Units': {
|
||||||
|
value: settings.metric,
|
||||||
|
format: v => v ? 'metric' : 'imperial',
|
||||||
|
onchange: (metric) => {
|
||||||
|
settings.metric = metric;
|
||||||
|
storage.writeJSON(SETTINGS_FILE, settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const sensorMenus = {};
|
||||||
|
for (var addr of Object.keys(settings.sensors)) {
|
||||||
|
// Define sub menu
|
||||||
|
sensorMenus[addr] = {
|
||||||
|
'': { title: addr },
|
||||||
|
'< Back': () => E.showMenu(menu),
|
||||||
|
'cm': {
|
||||||
|
value: settings.sensors[addr].cm,
|
||||||
|
min: 80, max: 240, step: 1,
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.sensors[addr].cm = v;
|
||||||
|
storage.writeJSON(SETTINGS_FILE, settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'+ mm': {
|
||||||
|
value: settings.sensors[addr].mm,
|
||||||
|
min: 0, max: 9, step: 1,
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.sensors[addr].mm = v;
|
||||||
|
storage.writeJSON(SETTINGS_FILE, settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add entry to main menu
|
||||||
|
menu[addr] = () => E.showMenu(sensorMenus[addr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
E.showMenu(menu);
|
||||||
|
})
|
|
@ -8,3 +8,4 @@
|
||||||
0.08: Optimize line wrapping for Bangle 2
|
0.08: Optimize line wrapping for Bangle 2
|
||||||
0.09: fix the trasparent widget bar if there are no widgets for Bangle 2
|
0.09: fix the trasparent widget bar if there are no widgets for Bangle 2
|
||||||
0.10: added "one click exit" setting for Bangle 2
|
0.10: added "one click exit" setting for Bangle 2
|
||||||
|
0.11: Fix bangle.js 1 white icons not displaying
|
||||||
|
|
|
@ -48,6 +48,7 @@ function draw_icon(p,n,selected) {
|
||||||
var x = (n%3)*80;
|
var x = (n%3)*80;
|
||||||
var y = n>2?130:40;
|
var y = n>2?130:40;
|
||||||
(selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89);
|
(selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25});
|
g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25});
|
||||||
g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1);
|
g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1);
|
||||||
var txt = apps[p*6+n].name.split(" ");
|
var txt = apps[p*6+n].name.split(" ");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "dtlaunch",
|
"id": "dtlaunch",
|
||||||
"name": "Desktop Launcher",
|
"name": "Desktop Launcher",
|
||||||
"version": "0.10",
|
"version": "0.11",
|
||||||
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
||||||
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
0.01: Initial version
|
||||||
|
0.02: Temporary intermediate version
|
||||||
|
0.03: Basic colors
|
||||||
|
0.04: Bug fix score reset after Game Over, new icon
|
||||||
|
0.05: Chevron marker on the randomly added square
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
# Play the game of 1024
|
||||||
|
|
||||||
|
Move the tiles by swiping to the lefthand, righthand or up- and downward side of the watch.
|
||||||
|
|
||||||
|
When two tiles with the same number are squashed together they will add up as exponentials:
|
||||||
|
|
||||||
|
**1 + 1 = 2** or **A + A = D** which is a representation of **2^1 + 2^1 = 2^1 = 4**
|
||||||
|
|
||||||
|
**2 + 2 = 3** or **B + B = C** which is a representation of **2^2 + 2^2 = 2^3 = 8**
|
||||||
|
|
||||||
|
**3 + 3 = 4** or **C + C = D** which is a representation of **2^3 + 2^3 = 2^4 = 16**
|
||||||
|
|
||||||
|
After each move a new tile will be added on a random empty square. The value can be 1 or 2, and will be marked with a chevron.
|
||||||
|
|
||||||
|
So you can continue till you reach **1024** which equals **2^(10)**. So when you reach tile **10** you have won.
|
||||||
|
|
||||||
|
The score is maintained by adding the outcome of the sum of all pairs of squashed tiles (4+16+4+8 etc.)
|
||||||
|
|
||||||
|
Use the side **BTN** to exit the game, score and tile positions will be saved.
|
||||||
|
|
||||||
|
## Buttons on the screen
|
||||||
|
|
||||||
|
- Button **U**: Undo the last move. There are currently a maximum of 4 undo levels. The level is indicated with a small number in the lower righthand corner of the Undo button
|
||||||
|
- Button **\***: Change the text on the tile to number, capitals or Roman numbers
|
||||||
|
- Button **R**: Reset the game. The Higscore will be remembered. You will be prompted first.
|
||||||
|
|
||||||
|
### Credits
|
||||||
|
|
||||||
|
Game 1024 is based on Saming's 2048 and Misho M. Petkovic 1024game.org and conceptually similar to Threes by Asher Vollmer.
|
||||||
|
|
||||||
|
In Dark theme with numbers:
|
||||||
|

|
||||||
|
|
||||||
|
In Light theme with characters:
|
||||||
|

|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwkBkQAWkAyVgQXx5gAMCQOqAAeiC/4X/AAXdC6HP7gECn///oXH///+QXEn4XC4f/mf/AwQXEmczmQXD74QD7/8AQZHLFIPfC4QzC4ZICC5XPngXD/4CB5oXNIYQXG+YXSCYQXKkQXWU4oXbL5mjC5M/R5evC5PfniwBa5Gvd4gXE5/z7s/DQIXGl6PJ5v//5eCC46/F4YXCAgMzAoYXFkYXFABTvMC/4X0ACkCC/4XJu4AMCQOIAAeCC+0///zC6dz/8z/83C6V/CgN/+4XSn4DCF6ZcGC6Hyv53V+Z3WCgR3OkQAWA="))
|
|
@ -0,0 +1,691 @@
|
||||||
|
const debugMode = 'off'; // valid values are: off, test, production, development
|
||||||
|
const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)};
|
||||||
|
const rows = 4, cols = 4;
|
||||||
|
const borderWidth = 6;
|
||||||
|
const sqWidth = (Math.floor(Bangle.appRect.w - 48) / rows) - borderWidth;
|
||||||
|
const cellColors = [{bg:'#00FFFF', fg: '#000000'},
|
||||||
|
{bg:'#FF00FF', fg: '#000000'}, {bg:'#808000', fg: '#FFFFFF'}, {bg:'#0000FF', fg: '#FFFFFF'}, {bg:'#008000', fg: '#FFFFFF'},
|
||||||
|
{bg:'#800000', fg: '#FFFFFF'}, {bg:'#00FF00', fg: '#000000'}, {bg:'#000080', fg: '#FFFFFF'}, {bg:'#FFFF00', fg: '#000000'},
|
||||||
|
{bg:'#800080', fg: '#FFFFFF'}, {bg:'#FF0000', fg: '#FFFFFF'}];
|
||||||
|
const cellFonts = ["12x20", "12x20", "Vector:14"];
|
||||||
|
const cellChars = [
|
||||||
|
[0,1,2,3,4,5,6,7,8,9,10],
|
||||||
|
['0','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
|
||||||
|
['0','I', 'II', 'III', 'IV', 'V', 'VI', 'VII','VIII', 'IX', 'X']
|
||||||
|
];
|
||||||
|
// const numInitialCells = 2;
|
||||||
|
const maxUndoLevels = 4;
|
||||||
|
const noExceptions = true;
|
||||||
|
let charIndex = 0; // plain numbers on the grid
|
||||||
|
const themeBg = g.theme.bg;
|
||||||
|
|
||||||
|
|
||||||
|
const scores = {
|
||||||
|
currentScore: 0,
|
||||||
|
highScore: 0,
|
||||||
|
lastScores: [0],
|
||||||
|
add: function(val) {
|
||||||
|
this.currentScore = this.currentScore + Math.pow(2, val);
|
||||||
|
debug(() => console.log("new score=",this.currentScore));
|
||||||
|
},
|
||||||
|
addToUndo: function () {
|
||||||
|
this.lastScores.push(this.currentScore);
|
||||||
|
if (this.lastScores.length > maxUndoLevels) this.lastScores.shift();
|
||||||
|
},
|
||||||
|
undo: function () {
|
||||||
|
this.currentScore = this.lastScores.pop();
|
||||||
|
debug(() => console.log("undo score =", this.currentScore, "rest:", this.lastScores));
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
this.currentScore = 0;
|
||||||
|
this.lastScores = [0];
|
||||||
|
},
|
||||||
|
draw: function () {
|
||||||
|
g.setColor(btnAtribs.fg);
|
||||||
|
let ulCorner = {x: Bangle.appRect.x + 6, y: Bangle.appRect.y2 -22 };
|
||||||
|
let lrCorner = {x: Bangle.appRect.x2, y: Bangle.appRect.y2 - 1};
|
||||||
|
g.fillRect(ulCorner.x, ulCorner.y, lrCorner.x, lrCorner.y)
|
||||||
|
.setFont12x20(1)
|
||||||
|
.setFontAlign(0,0,0);
|
||||||
|
let scrX = Math.floor((ulCorner.x + lrCorner.x)/3);
|
||||||
|
let scrY = Math.floor((ulCorner.y + lrCorner.y)/2) + 1;
|
||||||
|
g.setColor('#000000')
|
||||||
|
.drawString(this.currentScore, scrX+1, scrY+1)
|
||||||
|
.setColor(btnAtribs.bg)
|
||||||
|
.drawString(this.currentScore, scrX, scrY);
|
||||||
|
scrX = Math.floor(4*(ulCorner.x + lrCorner.x)/5);
|
||||||
|
g.setFont("6x8:1x2")
|
||||||
|
.drawString(this.highScore, btnAtribs.x + Math.floor(btnAtribs.w/2), scrY);
|
||||||
|
},
|
||||||
|
hsContents: function () {
|
||||||
|
return {"highScore": this.highScore, "lastScore": this.currentScore};
|
||||||
|
},
|
||||||
|
check: function () {
|
||||||
|
this.highScore = (this.currentScore > this.highScore) ? this.currentScore : this.highScore;
|
||||||
|
debug(() => console.log('highScore =', this.highScore));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// snapshot interval is the number of moves after wich a snapshot is wriiten to file
|
||||||
|
const snInterval = 1;
|
||||||
|
|
||||||
|
const snReadOnInit = true;
|
||||||
|
// a snapshot contains a json file dump of the last positions of the tiles on the board, including the scores
|
||||||
|
const snapshot = {
|
||||||
|
interval: snInterval,
|
||||||
|
snFileName: 'game1024.json',
|
||||||
|
counter: 0,
|
||||||
|
updCounter: function() {
|
||||||
|
this.counter = ++this.counter > this.interval ? 0 : this.counter;
|
||||||
|
},
|
||||||
|
dump: {gridsize: rows * cols, expVals: [], score: 0, highScore: 0, charIndex: charIndex},
|
||||||
|
write: function() {
|
||||||
|
require("Storage").writeJSON(this.snFileName, this.dump);
|
||||||
|
},
|
||||||
|
read: function () {
|
||||||
|
let sn = require("Storage").readJSON(this.snFileName, noExceptions);
|
||||||
|
if ((typeof sn == "undefined") || (sn.gridsize !== rows * cols)) {
|
||||||
|
require("Storage").writeJSON(this.snFileName, this.dump);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if ((typeof sn !== "undefined") && (sn.gridsize == rows * cols)){
|
||||||
|
this.dump = sn;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setDump: function () {
|
||||||
|
this.dump.expVals = [];
|
||||||
|
allSquares.forEach(sq => {
|
||||||
|
this.dump.expVals.push(sq.expVal);
|
||||||
|
});
|
||||||
|
this.dump.score = scores.currentScore;
|
||||||
|
this.dump.highScore = scores.highScore;
|
||||||
|
this.dump.charIndex = charIndex;
|
||||||
|
},
|
||||||
|
make: function () {
|
||||||
|
this.updCounter();
|
||||||
|
if (this.counter == this.interval) {
|
||||||
|
this.setDump();
|
||||||
|
this.write();
|
||||||
|
debug(() => console.log("snapped the state of the game:", this.dump));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recover: function () {
|
||||||
|
if (this.read()) {
|
||||||
|
this.dump.expVals.forEach((val, idx) => {
|
||||||
|
allSquares[idx].setExpVal(val);
|
||||||
|
});
|
||||||
|
scores.currentScore = this.dump.score ? this.dump.score : 0;
|
||||||
|
scores.highScore = this.dump.highScore ? this.dump.highScore : 0 ;
|
||||||
|
charIndex = this.dump.charIndex ? this.dump.charIndex : 0 ;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
this.dump.gridsize = rows * cols;
|
||||||
|
this.dump.expVals = [];
|
||||||
|
for (let i = 0; i< this.dump.gridsize; i++) {
|
||||||
|
this.dump.expVals[i] = 0;
|
||||||
|
}
|
||||||
|
this.dump.score = 0;
|
||||||
|
this.dump.highScore = scores.highScore;
|
||||||
|
this.dump.charIndex = charIndex;
|
||||||
|
this.write();
|
||||||
|
debug(() => console.log("reset D U M P E D!", this.dump));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const btnAtribs = {x: 134, w: 42, h: 42, fg:'#C0C0C0', bg:'#800000'};
|
||||||
|
const buttons = {
|
||||||
|
all: [],
|
||||||
|
draw: function () {
|
||||||
|
this.all.forEach(btn => {
|
||||||
|
btn.draw();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
add: function(btn) {
|
||||||
|
this.all.push(btn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* to the right = -1
|
||||||
|
all tiles move to the left, begin with the outer righthand side tiles
|
||||||
|
moving 0 to max 3 places to the right
|
||||||
|
|
||||||
|
find first tile beginning with bottom row, righthand side
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mover = {
|
||||||
|
direction: {
|
||||||
|
up: {name: 'up', step: 1, innerBegin: 0, innerEnd: rows-1, outerBegin: 0, outerEnd: cols-1, iter: rows -1,
|
||||||
|
sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m < rows -1 ? (m+1)*(cols) + n : -1;}
|
||||||
|
},
|
||||||
|
down: {name: 'down', step:-1, innerBegin: rows-1, innerEnd: 0, outerBegin: cols-1, outerEnd: 0, iter: rows -1,
|
||||||
|
sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m > 0 ? (m-1)*(cols) + n : -1;}
|
||||||
|
},
|
||||||
|
left: {name: 'left', step: 1, innerBegin: 0, innerEnd: cols-1, outerBegin: 0, outerEnd: rows-1, iter: cols -1,
|
||||||
|
sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m < cols -1 ? n*(rows) + m +1 : -1;}
|
||||||
|
},
|
||||||
|
right: {name: 'right', step:-1, innerBegin: cols-1, innerEnd: 0, outerBegin: rows-1, outerEnd: 0, iter: cols -1,
|
||||||
|
sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m > 0 ? n*(rows) + m -1: -1;}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
anyLeft: function() {
|
||||||
|
let canContinue = false;
|
||||||
|
[this.direction.up,this.direction.left].forEach (dir => {
|
||||||
|
const step = dir.step;
|
||||||
|
// outer loop for all colums/rows
|
||||||
|
for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) {
|
||||||
|
// lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up
|
||||||
|
for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) {
|
||||||
|
const idx = dir.sqIndex(m,n);
|
||||||
|
const nextIdx = dir.sqNextIndex(m,n);
|
||||||
|
if (allSquares[idx].expVal == 0) {
|
||||||
|
canContinue = true; // there is an empty cell found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (nextIdx >= 0) {
|
||||||
|
if (allSquares[idx].expVal == allSquares[nextIdx].expVal) {
|
||||||
|
canContinue = true; // equal adjacent cells > 0 found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (allSquares[nextIdx].expVal == 0) {
|
||||||
|
canContinue = true; // there is an empty cell found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (canContinue) break;
|
||||||
|
}
|
||||||
|
if (canContinue) break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return canContinue;
|
||||||
|
},
|
||||||
|
nonEmptyCells: function (dir) {
|
||||||
|
debug(() => console.log("Move: ", dir.name));
|
||||||
|
const step = dir.step;
|
||||||
|
// outer loop for all colums/rows
|
||||||
|
for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) {
|
||||||
|
// let rowStr = '| ';
|
||||||
|
|
||||||
|
// Move a number of iteration with the squares to move them all to one side
|
||||||
|
for (let iter = 0; iter < dir.iter; iter++) {
|
||||||
|
|
||||||
|
// lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up
|
||||||
|
for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) {
|
||||||
|
// get the array of squares index for current cell
|
||||||
|
const idx = dir.sqIndex(m,n);
|
||||||
|
const nextIdx = dir.sqNextIndex(m,n);
|
||||||
|
|
||||||
|
if (allSquares[idx].expVal == 0 && nextIdx >= 0) {
|
||||||
|
allSquares[idx].setExpVal(allSquares[nextIdx].expVal);
|
||||||
|
allSquares[nextIdx].setExpVal(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// add up the conjacent squares with identical values en set next square to empty in the process
|
||||||
|
mergeEqlCells: function(dir) {
|
||||||
|
const step = dir.step;
|
||||||
|
// outer loop for all colums/rows
|
||||||
|
for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) {
|
||||||
|
// lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up
|
||||||
|
for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) {
|
||||||
|
const idx = dir.sqIndex(m,n);
|
||||||
|
const nextIdx = dir.sqNextIndex(m,n);
|
||||||
|
|
||||||
|
if ((allSquares[idx].expVal > 0) && nextIdx >= 0) {
|
||||||
|
if (allSquares[idx].expVal == allSquares[nextIdx].expVal) {
|
||||||
|
let expVal = allSquares[idx].expVal;
|
||||||
|
allSquares[idx].setExpVal(++expVal);
|
||||||
|
allSquares[idx].addToScore();
|
||||||
|
allSquares[nextIdx].setExpVal(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Minimum number of pixels to interpret it as drag gesture
|
||||||
|
const dragThreshold = 10;
|
||||||
|
|
||||||
|
// Maximum number of pixels to interpret a click from a drag event series
|
||||||
|
const clickThreshold = 3;
|
||||||
|
|
||||||
|
let allSquares = [];
|
||||||
|
// let buttons = [];
|
||||||
|
|
||||||
|
class Button {
|
||||||
|
constructor(name, x0, y0, width, height, text, bg, fg, cb, enabled) {
|
||||||
|
this.x0 = x0;
|
||||||
|
this.y0 = y0;
|
||||||
|
this.x1 = x0 + width;
|
||||||
|
this.y1 = y0 + height;
|
||||||
|
this.name = name;
|
||||||
|
this.cb = cb;
|
||||||
|
this.text = text;
|
||||||
|
this.bg = bg;
|
||||||
|
this.fg = fg;
|
||||||
|
this.font = "6x8:3";
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
disable() {
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
enable() {
|
||||||
|
this.enabled = true;
|
||||||
|
}
|
||||||
|
draw() {
|
||||||
|
g.setColor(this.bg)
|
||||||
|
.fillRect(this.x0, this.y0, this.x1, this.y1)
|
||||||
|
.setFont(this.font)
|
||||||
|
.setFontAlign(0,0,0);
|
||||||
|
let strX = Math.floor((this.x0+this.x1)/2);
|
||||||
|
let strY = Math.floor((this.y0+this.y1)/2);
|
||||||
|
g.setColor("#000000")
|
||||||
|
.drawString(this.text, strX+2, strY+2)
|
||||||
|
.setColor(this.fg)
|
||||||
|
.drawString(this.text, strX, strY);
|
||||||
|
// buttons.push(this);
|
||||||
|
}
|
||||||
|
onClick() {if (typeof this.cb === 'function' && this.enabled) {
|
||||||
|
this.cb(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cell {
|
||||||
|
constructor(x0, y0, width, idx, cb) {
|
||||||
|
this.x0 = x0;
|
||||||
|
this.y0 = y0;
|
||||||
|
this.x1 = x0 + width;
|
||||||
|
this.y1 = y0 + width;
|
||||||
|
this.expVal = 0;
|
||||||
|
this.previousExpVals=[];
|
||||||
|
this.idx = idx;
|
||||||
|
this.cb = cb;
|
||||||
|
this.isRndm = false;
|
||||||
|
this.ax = x0;
|
||||||
|
this.ay = Math.floor(0.2*width+y0);
|
||||||
|
this.bx = Math.floor(0.3*width+x0);
|
||||||
|
this.by = Math.floor(0.5*width+y0);
|
||||||
|
this.cx = x0;
|
||||||
|
this.cy = Math.floor(0.8*width+y0);
|
||||||
|
}
|
||||||
|
getColor(i) {
|
||||||
|
return cellColors[i >= cellColors.length ? cellColors.length -1 : i];
|
||||||
|
}
|
||||||
|
drawBg() {
|
||||||
|
debug(()=>console.log("Drawbg!!"));
|
||||||
|
if (this.isRndm == true) {
|
||||||
|
debug(()=>console.log('Random: (ax)', this.ax));
|
||||||
|
g.setColor(this.getColor(this.expVal).bg)
|
||||||
|
.fillRect(this.x0, this.y0, this.x1, this.y1)
|
||||||
|
.setColor(themeBg)
|
||||||
|
.fillPoly([this.cx,this.cy,this.bx,this.by,this.ax,this.ay]);
|
||||||
|
} else {
|
||||||
|
g.setColor(this.getColor(this.expVal).bg)
|
||||||
|
.fillRect(this.x0, this.y0, this.x1, this.y1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawNumber() {
|
||||||
|
if (this.expVal !== 0) {
|
||||||
|
g.setFont(cellFonts[charIndex])
|
||||||
|
.setFontAlign(0,0,0);
|
||||||
|
let char = cellChars[charIndex][this.expVal];
|
||||||
|
let strX = Math.floor((this.x0 + this.x1)/2);
|
||||||
|
let strY = Math.floor((this.y0 + this.y1)/2);
|
||||||
|
g.setColor(this.getColor(this.expVal).fg)
|
||||||
|
.drawString(char, strX, strY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setExpVal(val) {
|
||||||
|
this.expVal = val;
|
||||||
|
}
|
||||||
|
getIdx() {return this.idx;}
|
||||||
|
pushToUndo() {
|
||||||
|
// remember this new step
|
||||||
|
this.previousExpVals.push(this.expVal);
|
||||||
|
// keep the undo list not longer than max undo levels
|
||||||
|
if (this.previousExpVals.length > maxUndoLevels) this.previousExpVals.shift();
|
||||||
|
}
|
||||||
|
popFromUndo() {
|
||||||
|
// take one step back
|
||||||
|
if (this.previousExpVals.length > 0) {
|
||||||
|
this.expVal = this.previousExpVals.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeUndo() {
|
||||||
|
this.previousExpVals=[0];
|
||||||
|
}
|
||||||
|
addToScore() {if (typeof this.cb === 'function') {
|
||||||
|
this.cb(this.expVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRndmFalse() {
|
||||||
|
this.isRndm = false;
|
||||||
|
}
|
||||||
|
setRndmTrue() {
|
||||||
|
this.isRndm = true;
|
||||||
|
}
|
||||||
|
drawRndmIndicator(){
|
||||||
|
if (this.isRndm == true) {
|
||||||
|
debug(()=>console.log('Random: (ax)', this.ax));
|
||||||
|
g.setColor(this.getColor(0).bg)
|
||||||
|
.fillPoly(this.ax,this.ay,this.bx,this.by,this.cx,this.cy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function undoGame() {
|
||||||
|
g.clear();
|
||||||
|
if (scores.lastScores.length > 0) {
|
||||||
|
allSquares.forEach(sq => {
|
||||||
|
sq.popFromUndo();
|
||||||
|
sq.drawBg();
|
||||||
|
sq.drawNumber();
|
||||||
|
});
|
||||||
|
scores.undo();
|
||||||
|
scores.draw();
|
||||||
|
buttons.draw();
|
||||||
|
updUndoLvlIndex();
|
||||||
|
snapshot.make();
|
||||||
|
}
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
function addToUndo() {
|
||||||
|
allSquares.forEach(sq => {
|
||||||
|
sq.pushToUndo();
|
||||||
|
});
|
||||||
|
scores.addToUndo();
|
||||||
|
}
|
||||||
|
function addToScore (val) {
|
||||||
|
scores.add(val);
|
||||||
|
if (val == 10) messageYouWin();
|
||||||
|
}
|
||||||
|
function createGrid () {
|
||||||
|
let cn =0;
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
let x0 = borderWidth + c*(borderWidth + sqWidth) - (rows/2)*(2*borderWidth + sqWidth) + middle.x + Math.floor(sqWidth/3);
|
||||||
|
let y0 = borderWidth + r*(borderWidth + sqWidth) - (cols/2)*(2*borderWidth + sqWidth) + middle.y + Math.floor(sqWidth/3);
|
||||||
|
let cell = new Cell(x0, y0, sqWidth, c + r*cols, addToScore);
|
||||||
|
allSquares.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function messageGameOver () {
|
||||||
|
const c = (g.theme.dark) ? {"fg": "#FFFFFF", "bg": "#808080"} : {"fg": "#FF0000", "bg": "#000000"};
|
||||||
|
g.setColor(c.bg)
|
||||||
|
.setFont12x20(2).setFontAlign(0,0,0)
|
||||||
|
.drawString("G A M E", middle.x+13, middle.y-24)
|
||||||
|
.drawString("O V E R !", middle.x+13, middle.y+24);
|
||||||
|
g.setColor(c.fg)
|
||||||
|
.drawString("G A M E", middle.x+12, middle.y-25)
|
||||||
|
.drawString("O V E R !", middle.x+12, middle.y+25);
|
||||||
|
}
|
||||||
|
function messageYouWin () {
|
||||||
|
g.setColor("#1a0d00")
|
||||||
|
.setFont12x20(2)
|
||||||
|
.setFontAlign(0,0,0)
|
||||||
|
.drawString("YOU HAVE", middle.x+18, middle.y-24)
|
||||||
|
.drawString("W O N ! !", middle.x+18, middle.y+24);
|
||||||
|
g.setColor("#FF0808")
|
||||||
|
.drawString("YOU HAVE", middle.x+17, middle.y-25)
|
||||||
|
.drawString("W O N ! !", middle.x+17, middle.y+25);
|
||||||
|
Bangle.buzz(200, 1);
|
||||||
|
}
|
||||||
|
function makeRandomNumber () {
|
||||||
|
return Math.ceil(2*Math.random());
|
||||||
|
}
|
||||||
|
function addRandomNumber() {
|
||||||
|
let emptySquaresIdxs = [];
|
||||||
|
allSquares.forEach(sq => {
|
||||||
|
if (sq.expVal == 0) emptySquaresIdxs.push(sq.getIdx());
|
||||||
|
});
|
||||||
|
if (emptySquaresIdxs.length > 0) {
|
||||||
|
let randomIdx = Math.floor( emptySquaresIdxs.length * Math.random() );
|
||||||
|
allSquares[emptySquaresIdxs[randomIdx]].setExpVal(makeRandomNumber());
|
||||||
|
allSquares[emptySquaresIdxs[randomIdx]].setRndmTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function drawGrid() {
|
||||||
|
allSquares.forEach(sq => {
|
||||||
|
sq.drawBg();
|
||||||
|
// sq.drawRndmIndicator();
|
||||||
|
sq.drawNumber();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function initGame() {
|
||||||
|
g.clear();
|
||||||
|
// scores.read();
|
||||||
|
createGrid();
|
||||||
|
if (snReadOnInit) {
|
||||||
|
snapshot.recover();
|
||||||
|
debug(() => console.log("R E C O V E R E D !", snapshot.dump));
|
||||||
|
let sum = allSquares.reduce(function (tv, sq) {return (sq.expVal + tv) ;}, 0);
|
||||||
|
if (!sum) {
|
||||||
|
addRandomNumber();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addRandomNumber();
|
||||||
|
// addToUndo();
|
||||||
|
}
|
||||||
|
addRandomNumber();
|
||||||
|
drawGrid();
|
||||||
|
scores.draw();
|
||||||
|
buttons.draw();
|
||||||
|
// Clock mode allows short-press on button to exit
|
||||||
|
Bangle.setUI("clock");
|
||||||
|
// Load widgets
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
function drawPopUp(message,cb) {
|
||||||
|
g.setColor('#FFFFFF');
|
||||||
|
let rDims = Bangle.appRect;
|
||||||
|
g.fillPoly([rDims.x+10, rDims.y+20,
|
||||||
|
rDims.x+20, rDims.y+10,
|
||||||
|
rDims.x2-30, rDims.y+10,
|
||||||
|
rDims.x2-20, rDims.y+20,
|
||||||
|
rDims.x2-20, rDims.y2-40,
|
||||||
|
rDims.x2-30, rDims.y2-30,
|
||||||
|
rDims.x+20, rDims.y2-30,
|
||||||
|
rDims.x+10, rDims.y2-40
|
||||||
|
]);
|
||||||
|
buttons.all.forEach(btn => {btn.disable();});
|
||||||
|
const btnYes = new Button('yes', rDims.x+16, rDims.y2-80, 54, btnAtribs.h, 'YES', btnAtribs.fg, btnAtribs.bg, cb, true);
|
||||||
|
const btnNo = new Button('no', rDims.x2-80, rDims.y2-80, 54, btnAtribs.h, 'NO', btnAtribs.fg, btnAtribs.bg, cb, true);
|
||||||
|
btnYes.draw();
|
||||||
|
btnNo.draw();
|
||||||
|
g.setColor('#000000');
|
||||||
|
g.setFont12x20(1);
|
||||||
|
g.setFontAlign(-1,-1,0);
|
||||||
|
g.drawString(message, rDims.x+20, rDims.y+20);
|
||||||
|
buttons.add(btnYes);
|
||||||
|
buttons.add(btnNo);
|
||||||
|
}
|
||||||
|
function handlePopUpClicks(btn) {
|
||||||
|
const name = btn.name;
|
||||||
|
buttons.all.pop(); // remove the no button
|
||||||
|
buttons.all.pop(); // remove the yes button
|
||||||
|
buttons.all.forEach(b => {b.enable();}); // enable the remaining buttons again
|
||||||
|
debug(() => console.log("Button name =", name));
|
||||||
|
switch (name) {
|
||||||
|
case 'yes':
|
||||||
|
resetGame();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
g.clear();
|
||||||
|
drawGrid();
|
||||||
|
scores.draw();
|
||||||
|
buttons.draw();
|
||||||
|
updUndoLvlIndex();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resetGame() {
|
||||||
|
g.clear();
|
||||||
|
scores.reset();
|
||||||
|
allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();sq.setRndmFalse();});
|
||||||
|
addRandomNumber();
|
||||||
|
addRandomNumber();
|
||||||
|
drawGrid();
|
||||||
|
scores.draw();
|
||||||
|
buttons.draw();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that can be used in test or development environment, or production.
|
||||||
|
* Depends on global constant debugMode
|
||||||
|
* @param {function} func function to call like console.log()
|
||||||
|
*/
|
||||||
|
const debug = (func) => {
|
||||||
|
switch (debugMode) {
|
||||||
|
case "development":
|
||||||
|
if (typeof func === 'function') {
|
||||||
|
func();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "off":
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle a "click" event (only needed for menu button)
|
||||||
|
function handleclick(e) {
|
||||||
|
buttons.all.forEach(btn => {
|
||||||
|
if ((e.x >= btn.x0) && (e.x <= btn.x1) && (e.y >= btn.y0) && (e.y <= btn.y1)) {
|
||||||
|
btn.onClick();
|
||||||
|
debug(() => console.log(btn.name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a drag event (moving the stones around)
|
||||||
|
function handledrag(e) {
|
||||||
|
/*debug(Math.abs(e.dx) > Math.abs(e.dy) ?
|
||||||
|
(e.dx > 0 ? e => console.log('To the right') : e => console.log('To the left') ) :
|
||||||
|
(e.dy > 0 ? e => console.log('Move down') : e => console.log('Move up') ));
|
||||||
|
*/
|
||||||
|
// [move.right, move.left, move.up, move.down]
|
||||||
|
runGame((Math.abs(e.dx) > Math.abs(e.dy) ?
|
||||||
|
(e.dx > 0 ? mover.direction.right : mover.direction.left ) :
|
||||||
|
(e.dy > 0 ? mover.direction.down : mover.direction.up )));
|
||||||
|
}
|
||||||
|
// Evaluate "drag" events from the UI and call handlers for drags or clicks
|
||||||
|
// The UI sends a drag as a series of events indicating partial movements
|
||||||
|
// of the finger.
|
||||||
|
// This class combines such parts to a long drag from start to end
|
||||||
|
// If the drag is short, it is interpreted as click,
|
||||||
|
// otherwise as drag.
|
||||||
|
// The approprate method is called with the data of the drag.
|
||||||
|
class Dragger {
|
||||||
|
|
||||||
|
constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) {
|
||||||
|
this.clickHandler = clickHandler;
|
||||||
|
this.dragHandler = dragHandler;
|
||||||
|
this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold);
|
||||||
|
this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold);
|
||||||
|
this.dx = 0;
|
||||||
|
this.dy = 0;
|
||||||
|
this.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable or disable the Dragger
|
||||||
|
setEnabled(b) {
|
||||||
|
this.enabled = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a raw drag event from the UI
|
||||||
|
handleRawDrag(e) {
|
||||||
|
if (!this.enabled)
|
||||||
|
return;
|
||||||
|
this.dx += e.dx; // Always accumulate
|
||||||
|
this.dy += e.dy;
|
||||||
|
if (e.b === 0) { // Drag event ended: Evaluate full drag
|
||||||
|
if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold)
|
||||||
|
this.clickHandler({
|
||||||
|
x: e.x - this.dx,
|
||||||
|
y: e.y - this.dy
|
||||||
|
}); // take x and y from the drag start
|
||||||
|
else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold)
|
||||||
|
this.dragHandler({
|
||||||
|
x: e.x - this.dx,
|
||||||
|
y: e.y - this.dy,
|
||||||
|
dx: this.dx,
|
||||||
|
dy: this.dy
|
||||||
|
});
|
||||||
|
this.dx = 0; // Clear the drag accumulator
|
||||||
|
this.dy = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the drag evaluator to the UI
|
||||||
|
attach() {
|
||||||
|
Bangle.on("drag", e => this.handleRawDrag(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragger is needed for interaction during the game
|
||||||
|
var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold);
|
||||||
|
|
||||||
|
// Disable dragger as board is not yet initialized
|
||||||
|
dragger.setEnabled(false);
|
||||||
|
|
||||||
|
// Nevertheless attach it so that it is ready once the game starts
|
||||||
|
dragger.attach();
|
||||||
|
|
||||||
|
function runGame(dir){
|
||||||
|
addToUndo();
|
||||||
|
updUndoLvlIndex();
|
||||||
|
mover.nonEmptyCells(dir);
|
||||||
|
mover.mergeEqlCells(dir);
|
||||||
|
mover.nonEmptyCells(dir);
|
||||||
|
allSquares.forEach(sq => {sq.setRndmFalse();});
|
||||||
|
addRandomNumber();
|
||||||
|
drawGrid();
|
||||||
|
scores.check();
|
||||||
|
scores.draw();
|
||||||
|
// scores.write();
|
||||||
|
snapshot.make();
|
||||||
|
dragger.setEnabled(true);
|
||||||
|
if (!(mover.anyLeft())) {
|
||||||
|
debug(() => console.log("G A M E O V E R !!"));
|
||||||
|
snapshot.reset();
|
||||||
|
messageGameOver();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updUndoLvlIndex() {
|
||||||
|
let x = 170;
|
||||||
|
let y = 60;
|
||||||
|
g.setColor(btnAtribs.fg)
|
||||||
|
.fillRect(x-6,y-6, 176, 67);
|
||||||
|
if (scores.lastScores.length > 0) {
|
||||||
|
g.setColor("#000000")
|
||||||
|
.setFont("4x6:2")
|
||||||
|
.drawString(scores.lastScores.length, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function incrCharIndex() {
|
||||||
|
charIndex++;
|
||||||
|
if (charIndex >= cellChars.length) charIndex = 0;
|
||||||
|
drawGrid();
|
||||||
|
}
|
||||||
|
buttons.add(new Button('undo', btnAtribs.x, 25, btnAtribs.w, btnAtribs.h, 'U', btnAtribs.fg, btnAtribs.bg, undoGame, true));
|
||||||
|
buttons.add(new Button('chars', btnAtribs.x, 71, btnAtribs.w, 31, '*', btnAtribs.fg, btnAtribs.bg, function(){incrCharIndex();}, true));
|
||||||
|
buttons.add(new Button('restart', btnAtribs.x, 106, btnAtribs.w, btnAtribs.h, 'R', btnAtribs.fg, btnAtribs.bg, function(){drawPopUp('Do you want\nto restart?',handlePopUpClicks);}, true));
|
||||||
|
|
||||||
|
initGame();
|
||||||
|
|
||||||
|
dragger.setEnabled(true);
|
||||||
|
|
||||||
|
E.on('kill',function() {
|
||||||
|
this.write();
|
||||||
|
debug(() => console.log("1024 game got killed!"));
|
||||||
|
});
|
|
@ -0,0 +1,6 @@
|
||||||
|
require("Storage").write("timer.info",{
|
||||||
|
"id":"game1024",
|
||||||
|
"name":"1024 Game",
|
||||||
|
"src":"game1024.app.js",
|
||||||
|
"icon":"game1024.img"
|
||||||
|
});
|
|
@ -0,0 +1 @@
|
||||||
|
{"gridsize": 16, "expVals": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], "score": 0, "highScore": 0, "charIndex": 1}
|
After Width: | Height: | Size: 582 B |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,17 @@
|
||||||
|
{ "id": "game1024",
|
||||||
|
"name": "1024 Game",
|
||||||
|
"shortName" : "1024 Game",
|
||||||
|
"version": "0.05",
|
||||||
|
"icon": "game1024.png",
|
||||||
|
"screenshots": [ {"url":"screenshot.png" } ],
|
||||||
|
"readme":"README.md",
|
||||||
|
"description": "Swipe the squares up, down, to the left or right, join the numbers and get to the 10 (2^1024), J or X tile!",
|
||||||
|
"type": "app",
|
||||||
|
"tags": "game,puzzle",
|
||||||
|
"allow_emulator": true,
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"game1024.app.js","url":"app.js"},
|
||||||
|
{"name":"game1024.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 5.9 KiB |
|
@ -1 +1,2 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
|
0.02: Make Bangle.js 2 compatible
|
||||||
|
|
|
@ -1,7 +1,86 @@
|
||||||
var storage = require('Storage');
|
const storage = require('Storage');
|
||||||
|
const Layout = require("Layout");
|
||||||
const settings = storage.readJSON('setting.json',1) || { HID: false };
|
const settings = storage.readJSON('setting.json',1) || { HID: false };
|
||||||
|
const BANGLEJS2 = process.env.HWVERSION == 2;
|
||||||
|
const sidebarWidth=18;
|
||||||
|
const buttonWidth = (Bangle.appRect.w-sidebarWidth)/2;
|
||||||
|
const buttonHeight = (Bangle.appRect.h-16)/2*0.85; // subtract text row and add a safety margin
|
||||||
|
|
||||||
var sendInProgress = false; // Only send one message at a time, do not flood
|
var sendInProgress = false; // Only send one message at a time, do not flood
|
||||||
|
var touchBtn2 = 0;
|
||||||
|
var touchBtn3 = 0;
|
||||||
|
var touchBtn4 = 0;
|
||||||
|
var touchBtn5 = 0;
|
||||||
|
|
||||||
|
function renderBtnArrows(l) {
|
||||||
|
const d = g.getWidth() - l.width;
|
||||||
|
|
||||||
|
function c(a) {
|
||||||
|
return {
|
||||||
|
width: 8,
|
||||||
|
height: a.length,
|
||||||
|
bpp: 1,
|
||||||
|
buffer: (new Uint8Array(a)).buffer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
g.drawImage(c([0,8,12,14,255,14,12,8]),d,g.getHeight()/2);
|
||||||
|
if (!BANGLEJS2) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const layoutChilden = [];
|
||||||
|
if (BANGLEJS2) { // add virtual buttons in display
|
||||||
|
layoutChilden.push({type:"h", c:[
|
||||||
|
{type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN2", id:"touchBtn2" },
|
||||||
|
{type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN3", id:"touchBtn3" },
|
||||||
|
]});
|
||||||
|
}
|
||||||
|
layoutChilden.push({type:"h", c:[
|
||||||
|
{type:"txt", font:"6x8:2", label:"Joystick" },
|
||||||
|
]});
|
||||||
|
if (BANGLEJS2) { // add virtual buttons in display
|
||||||
|
layoutChilden.push({type:"h", c:[
|
||||||
|
{type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN4", id:"touchBtn4" },
|
||||||
|
{type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN5", id:"touchBtn5" },
|
||||||
|
]});
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = new Layout(
|
||||||
|
{type:"h", c:[
|
||||||
|
{type:"v", width:Bangle.appRect.w-sidebarWidth, c: layoutChilden},
|
||||||
|
{type:"custom", width:18, height: Bangle.appRect.h, render:renderBtnArrows }
|
||||||
|
]}
|
||||||
|
);
|
||||||
|
|
||||||
|
function isInBox(box, x, y) {
|
||||||
|
return x >= box.x && x < box.x+box.w && y >= box.y && y < box.y+box.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BANGLEJS2) {
|
||||||
|
Bangle.on('drag', function(event) {
|
||||||
|
if (event.b == 0) { // release
|
||||||
|
touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0;
|
||||||
|
} else if (isInBox(layout.touchBtn2, event.x, event.y)) {
|
||||||
|
touchBtn2 = 1;
|
||||||
|
touchBtn3 = touchBtn4 = touchBtn5 = 0;
|
||||||
|
} else if (isInBox(layout.touchBtn3, event.x, event.y)) {
|
||||||
|
touchBtn3 = 1;
|
||||||
|
touchBtn2 = touchBtn4 = touchBtn5 = 0;
|
||||||
|
} else if (isInBox(layout.touchBtn4, event.x, event.y)) {
|
||||||
|
touchBtn4 = 1;
|
||||||
|
touchBtn2 = touchBtn3 = touchBtn5 = 0;
|
||||||
|
} else if (isInBox(layout.touchBtn5, event.x, event.y)) {
|
||||||
|
touchBtn5 = 1;
|
||||||
|
touchBtn2 = touchBtn3 = touchBtn4 = 0;
|
||||||
|
} else {
|
||||||
|
// outside any buttons, release all
|
||||||
|
touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
|
const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
|
||||||
try {
|
try {
|
||||||
|
@ -20,31 +99,17 @@ const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
|
||||||
|
|
||||||
function drawApp() {
|
function drawApp() {
|
||||||
g.clear();
|
g.clear();
|
||||||
g.setFont("6x8",2);
|
Bangle.loadWidgets();
|
||||||
g.setFontAlign(0,0);
|
Bangle.drawWidgets();
|
||||||
g.drawString("Joystick", 120, 120);
|
layout.render();
|
||||||
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() {
|
function update() {
|
||||||
const btn1 = BTN1.read();
|
const btn1 = BTN1 ? BTN1.read() : 0;
|
||||||
const btn2 = BTN2.read();
|
const btn2 = !BANGLEJS2 ? BTN2.read() : touchBtn2;
|
||||||
const btn3 = BTN3.read();
|
const btn3 = !BANGLEJS2 ? BTN3.read() : touchBtn3;
|
||||||
const btn4 = BTN4.read();
|
const btn4 = !BANGLEJS2 ? BTN4.read() : touchBtn4;
|
||||||
const btn5 = BTN5.read();
|
const btn5 = !BANGLEJS2 ? BTN5.read() : touchBtn5;
|
||||||
const acc = Bangle.getAccel();
|
const acc = Bangle.getAccel();
|
||||||
var x = acc.x*-127;
|
var x = acc.x*-127;
|
||||||
var y = acc.y*-127;
|
var y = acc.y*-127;
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"id": "hidjoystick",
|
"id": "hidjoystick",
|
||||||
"name": "Bluetooth Joystick",
|
"name": "Bluetooth Joystick",
|
||||||
"shortName": "Joystick",
|
"shortName": "Joystick",
|
||||||
"version": "0.01",
|
"version": "0.02",
|
||||||
"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.",
|
"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. On Bangle.js 2 buttons 2-5 are emulated with the touchscreen.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "bluetooth",
|
"tags": "bluetooth",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"hidjoystick.app.js","url":"app.js"},
|
{"name":"hidjoystick.app.js","url":"app.js"},
|
||||||
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
|
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
0.06: Add widgets
|
0.06: Add widgets
|
||||||
0.07: Update scaling for new firmware
|
0.07: Update scaling for new firmware
|
||||||
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
||||||
|
0.09: Grey out BPM until confidence is over 50%
|
||||||
|
|
|
@ -35,9 +35,9 @@ function onHRM(h) {
|
||||||
g.clearRect(0,24,g.getWidth(),80);
|
g.clearRect(0,24,g.getWidth(),80);
|
||||||
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
|
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
|
||||||
var str = hrmInfo.bpm;
|
var str = hrmInfo.bpm;
|
||||||
g.setFontVector(40).drawString(str,px,45);
|
g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45);
|
||||||
px += g.stringWidth(str)/2;
|
px += g.stringWidth(str)/2;
|
||||||
g.setFont("6x8");
|
g.setFont("6x8").setColor(g.theme.fg);
|
||||||
g.drawString("BPM",px+15,45);
|
g.drawString("BPM",px+15,45);
|
||||||
}
|
}
|
||||||
Bangle.on('HRM', onHRM);
|
Bangle.on('HRM', onHRM);
|
||||||
|
@ -101,4 +101,3 @@ function readHRM() {
|
||||||
lastHrmPt = [hrmOffset, y];
|
lastHrmPt = [hrmOffset, y];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "hrm",
|
"id": "hrm",
|
||||||
"name": "Heart Rate Monitor",
|
"name": "Heart Rate Monitor",
|
||||||
"version": "0.08",
|
"version": "0.09",
|
||||||
"description": "Measure your heart rate and see live sensor data",
|
"description": "Measure your heart rate and see live sensor data",
|
||||||
"icon": "heartrate.png",
|
"icon": "heartrate.png",
|
||||||
"tags": "health",
|
"tags": "health",
|
||||||
|
|
|
@ -17,3 +17,4 @@
|
||||||
0.17: Settings for mph/kph and other minor improvements.
|
0.17: Settings for mph/kph and other minor improvements.
|
||||||
0.18: Fullscreen mode can now be enabled or disabled in the settings.
|
0.18: Fullscreen mode can now be enabled or disabled in the settings.
|
||||||
0.19: Use qalarm for alarm functionality instead of own implementation.
|
0.19: Use qalarm for alarm functionality instead of own implementation.
|
||||||
|
0.19: Alarms can not go bigger than 100.
|
||||||
|
|
|
@ -612,6 +612,12 @@ Bangle.on('charging',function(charging) {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function increaseAlarm(){
|
||||||
|
if(isAlarmEnabled() && getAlarmMinutes() < 95){
|
||||||
|
settings.alarm += 5;
|
||||||
|
} else {
|
||||||
|
settings.alarm = getCurrentTimeInMinutes() + 5;
|
||||||
|
}
|
||||||
|
|
||||||
function feedback(){
|
function feedback(){
|
||||||
Bangle.buzz(40, 0.3);
|
Bangle.buzz(40, 0.3);
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Add the option to enable touching the widget only on clock and settings.
|
0.02: Add the option to enable touching the widget only on clock and settings.
|
||||||
|
0.03: Settings page now uses built-in min/max/wrap (fix #1607)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "lightswitch",
|
"id": "lightswitch",
|
||||||
"name": "Light Switch Widget",
|
"name": "Light Switch Widget",
|
||||||
"shortName": "Light Switch",
|
"shortName": "Light Switch",
|
||||||
"version": "0.02",
|
"version": "0.03",
|
||||||
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
|
"description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.",
|
||||||
"icon": "images/app.png",
|
"icon": "images/app.png",
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
|
|
|
@ -44,9 +44,11 @@
|
||||||
// return entry for string value
|
// return entry for string value
|
||||||
return {
|
return {
|
||||||
value: entry.value.indexOf(settings[key]),
|
value: entry.value.indexOf(settings[key]),
|
||||||
|
min : 0,
|
||||||
|
max : entry.value.length-1,
|
||||||
|
wrap : true,
|
||||||
format: v => entry.title ? entry.title[v] : entry.value[v],
|
format: v => entry.title ? entry.title[v] : entry.value[v],
|
||||||
onchange: function(v) {
|
onchange: function(v) {
|
||||||
this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v;
|
|
||||||
writeSetting(key, entry.value[v], entry.drawWidgets);
|
writeSetting(key, entry.value[v], entry.drawWidgets);
|
||||||
if (entry.exec) entry.exec(entry.value[v]);
|
if (entry.exec) entry.exec(entry.value[v]);
|
||||||
}
|
}
|
||||||
|
@ -57,8 +59,10 @@
|
||||||
value: settings[key] * entry.factor,
|
value: settings[key] * entry.factor,
|
||||||
step: entry.step,
|
step: entry.step,
|
||||||
format: v => v > 0 ? v + entry.unit : "off",
|
format: v => v > 0 ? v + entry.unit : "off",
|
||||||
|
min : entry.min,
|
||||||
|
max : entry.max,
|
||||||
|
wrap : true,
|
||||||
onchange: function(v) {
|
onchange: function(v) {
|
||||||
this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v;
|
|
||||||
writeSetting(key, v / entry.factor, entry.drawWidgets);
|
writeSetting(key, v / entry.factor, entry.drawWidgets);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -133,16 +137,16 @@
|
||||||
title: "Light Switch"
|
title: "Light Switch"
|
||||||
},
|
},
|
||||||
"< Back": () => back(),
|
"< Back": () => back(),
|
||||||
"-- Widget --------": 0,
|
"-- Widget": 0,
|
||||||
"Bulb col": getEntry("colors"),
|
"Bulb col": getEntry("colors"),
|
||||||
"Image": getEntry("image"),
|
"Image": getEntry("image"),
|
||||||
"-- Control -------": 0,
|
"-- Control": 0,
|
||||||
"Touch": getEntry("touchOn"),
|
"Touch": getEntry("touchOn"),
|
||||||
"Drag Delay": getEntry("dragDelay"),
|
"Drag Delay": getEntry("dragDelay"),
|
||||||
"Min Value": getEntry("minValue"),
|
"Min Value": getEntry("minValue"),
|
||||||
"-- Unlock --------": 0,
|
"-- Unlock": 0,
|
||||||
"TapSide": getEntry("unlockSide"),
|
"TapSide": getEntry("unlockSide"),
|
||||||
"-- Flash ---------": 0,
|
"-- Flash": 0,
|
||||||
"TapSide ": getEntry("tapSide"),
|
"TapSide ": getEntry("tapSide"),
|
||||||
"Tap": getEntry("tapOn"),
|
"Tap": getEntry("tapOn"),
|
||||||
"Timeout": getEntry("tOut"),
|
"Timeout": getEntry("tOut"),
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
0.01: First release
|
0.01: First release
|
||||||
|
0.02: Make sure to reset turns
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Classic Mind Game",
|
"name": "Classic Mind Game",
|
||||||
"shortName":"Master Mind",
|
"shortName":"Master Mind",
|
||||||
"icon": "mmind.png",
|
"icon": "mmind.png",
|
||||||
"version":"0.01",
|
"version":"0.02",
|
||||||
"description": "This is the classic game for masterminds",
|
"description": "This is the classic game for masterminds",
|
||||||
"screenshots": [{"url":"screenshot_mmind.png"}],
|
"screenshots": [{"url":"screenshot_mmind.png"}],
|
||||||
"type": "app",
|
"type": "app",
|
||||||
|
|
|
@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) {
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
//new game
|
//new game
|
||||||
|
turn = 0;
|
||||||
play = [-1,-1,-1,-1];
|
play = [-1,-1,-1,-1];
|
||||||
game = [];
|
game = [];
|
||||||
endgame=false;
|
endgame=false;
|
||||||
|
@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) {
|
||||||
game = [];
|
game = [];
|
||||||
get_secret();
|
get_secret();
|
||||||
draw();
|
draw();
|
||||||
//Bangle.loadWidgets();
|
|
||||||
//Bangle.drawWidgets();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -219,7 +219,7 @@ function viewTrack(filename, info) {
|
||||||
f.erase();
|
f.erase();
|
||||||
viewTracks();
|
viewTracks();
|
||||||
} else
|
} else
|
||||||
viewTrack(n, info);
|
viewTrack(filename, info);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
menu['< Back'] = () => { viewTracks(); };
|
menu['< Back'] = () => { viewTracks(); };
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mUygP/AC5BlH4MAn/gAwN/4EP/AFBsEMhkBwEAjEDgYJBgEGgHA4EYDwOAmEwBIIYyj/wgf+AoMH/kA/4eBJXwYLVxgAjh//AC3w"))
|
require("heatshrink").decompress(atob("mEqgInkn/gg/8Ao/AjEYgYF/AoZT/Kb4AiA="))
|
||||||
|
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 4.0 KiB |
|
@ -1 +1,2 @@
|
||||||
0.01: Initial Release
|
0.01: Initial Release
|
||||||
|
0.02: Shrink hand images to save memory
|
||||||
|
|
|
@ -7,17 +7,17 @@ var imgBg = {
|
||||||
/* Set hour hand image */
|
/* Set hour hand image */
|
||||||
|
|
||||||
var imgHour = {
|
var imgHour = {
|
||||||
width : 16, height : 176, bpp : 2,
|
width : 14, height : 114, bpp : 2,
|
||||||
transparent : 0,
|
transparent : 0,
|
||||||
buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AEk//gDp///gEDAYPAh4DB+E/AYP8AaYbDEYYrDLdgD/Af4DXh/wAYIA/AGwA="))
|
buffer : require("heatshrink").decompress(atob("AH4A/AB8P/4DB//wAz8D//8BIIKBn4DB54CBACPzAQP8EoImBD4PAJkQG/A34GIgbUBA"))
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Set minute hand image */
|
/* Set minute hand image */
|
||||||
|
|
||||||
var imgMin = {
|
var imgMin = {
|
||||||
width : 8, height : 176, bpp : 2,
|
width : 4, height : 168, bpp : 2,
|
||||||
transparent : 0,
|
transparent : 0,
|
||||||
buffer : require("heatshrink").decompress(atob("AH4A/AB8P+AB/AP4B/AIcA4DPHA="))
|
buffer : require("heatshrink").decompress(atob("AH4AE/4A/AEI"))
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Set second hand image */
|
/* Set second hand image */
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"shortName":"5actus",
|
"shortName":"5actus",
|
||||||
"icon": "seiko-5actus.png",
|
"icon": "seiko-5actus.png",
|
||||||
"screenshots": [{"url":"screenshot.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
"version":"0.01",
|
"version":"0.02",
|
||||||
"description": "A watch designed after then Seiko 5actus from the 1970's",
|
"description": "A watch designed after then Seiko 5actus from the 1970's",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
|
|
|
@ -29,11 +29,11 @@ function calc_ess(val) {
|
||||||
if (nonmot) {
|
if (nonmot) {
|
||||||
slsnds+=1;
|
slsnds+=1;
|
||||||
if (slsnds >= sleepthresh) {
|
if (slsnds >= sleepthresh) {
|
||||||
return true; // awake
|
return true; // sleep
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
slsnds=0;
|
slsnds=0;
|
||||||
return false; // sleep
|
return false; // awake
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("oFAwhC/ABOIABgfymYAKD+Z/9hGDL5c4wAf/XzjASTxqgQhAfPMB2IPxiACIBo+BDxqACIBg+CLxpANHwQPBABgvCIBT8CJ5owDD5iPOOAQfLBojiDCYQGFGIQfICIQfdBYJNMOI6SHD8jeNOIYzID8hfRD9LfEAoTdFBIifLAAIffBoQRBAJpxMD84JCD+S/GL56fID8ALBb6ZhID8qtJCZ4fgT4YDBABq/PD7RNEL6IRKD8WID5pfCD5kzNhKSFmYfMBwSeOGBoPDABgvCJ5wAON5pADABivPIAIAOd5xABABweOD4J+OD58IQBj8LD/6gUDyAfhXzgfiP/wA2"))
|
require("heatshrink").decompress(atob("mEw4UA///7k8//GnldDZ9RosUqNABQsFqoACqALFg2qAAWQBaMVEYdUBYseC4e0BYsaBYekBYt6BYetBYouDAAIKEgPqC4erNgkFBQYABNgke2oiDrxIEvXUBYcXHgl7FIkB9oEDBYKYBTwILEi4LCoEBBYUQHQX7EYyRCBYJrF95ICBYNFBQdRBYcWEYwLDit7otUHQMVqIvDL4L5BgL8CI4YLDqILDO4gXGBQUEEZQ7CEYprEI4prFoLGBqkFoILFNZaPFF4ZHCR4hrFa5ILMfYJeDfYse2ovDrxGCAAMF1QAEMgIpD9QKD1Y1EgBfFBQg8BC4Y6EAAMaBYekBYseBYZGESIQuDfIYACgwXDyALRgojDNQhsCBYZqFABI="))
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
0.01: Initial version
|
0.01: Initial version
|
||||||
0.02: Add battery level
|
0.02: Add battery level
|
||||||
0.03: Fix battery display when full
|
0.03: Fix battery display when full (incorporating code by Ronin0000)
|
||||||
0.04: Add support for settings
|
0.04: Add support for settings
|
||||||
|
0.05: Add ability to change background (3bit or 4bit)
|
||||||
|
0.06: Replace battery text with image
|
||||||
|
|
|
@ -4,13 +4,16 @@ Just a simple watch face for the Banglejs2.
|
||||||
|
|
||||||
It shows battery level in the upper left corner, date information in the upper right, and time information in the bottom.
|
It shows battery level in the upper left corner, date information in the upper right, and time information in the bottom.
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## Settings
|
## Settings
|
||||||
|
|
||||||
**Analog Clock:**
|
**Analog Clock:** *Not yet implemented.*
|
||||||
|
|
||||||
**Human Readable Date:** When the setting is on, the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off.
|
**Background:** When the setting is set as "3bit", a background with more accurate colors is chosen for the watchface. Otherwise, it uses a background following the 16-bit Mac Color Palette.
|
||||||
|
|
||||||
|
**Date Format:** When the setting is set as "Long", the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off.
|
||||||
|
|
||||||
**Show Week Info:** When the setting is on, the weekday and week number are shown in the upper right box. When the setting is off, the full year is shown instead. Default is off.
|
**Show Week Info:** When the setting is on, the weekday and week number are shown in the upper right box. When the setting is off, the full year is shown instead. Default is off.
|
||||||
|
|
||||||
|
@ -20,4 +23,4 @@ It shows battery level in the upper left corner, date information in the upper r
|
||||||
|
|
||||||
Monogram Watch Face can be selected as the default clock or it can be run manually from the launcher. Its settings can be accessed and changed via the relevant menu.
|
Monogram Watch Face can be selected as the default clock or it can be run manually from the launcher. Its settings can be accessed and changed via the relevant menu.
|
||||||
|
|
||||||
Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert.
|
*Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert.* - *Feature not implemented yet.*
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
const SETTINGSFILE = "smclock.json";
|
const SETTINGSFILE = "smclock.json";
|
||||||
const background = {
|
const image3bit = {
|
||||||
width: 176,
|
width : 176, height : 176, bpp : 3,
|
||||||
height: 176,
|
transparent : 1,
|
||||||
bpp: 3,
|
buffer : require("heatshrink").decompress(atob("/4A/AH4AC23btoCct/pkmSpICcIP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5Bp/4A/AH4AC/kAAH0/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5BpA="))
|
||||||
transparent: 1,
|
};
|
||||||
buffer: require("heatshrink").decompress(
|
const image4bit = {
|
||||||
atob(
|
width : 176, height : 176, bpp : 4,
|
||||||
"/4A/AH4ACUb8H9MkyVJAThB/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INP/AH4A/AAX8Yz4Afn5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INI="
|
transparent : 1,
|
||||||
)
|
buffer : require("heatshrink").decompress(atob("/4A/AH4Au1QAp1/2swApK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K+//AH4A/AF8AAH4AUK/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/AA=="))
|
||||||
),
|
|
||||||
};
|
};
|
||||||
const monthName = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
const monthName = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
||||||
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
|
||||||
// dynamic variables
|
// dynamic variables
|
||||||
var batLevel = -1;
|
var batLevel = -1;
|
||||||
var batColor = [0, 0, 0];
|
var batColor = "";
|
||||||
|
|
||||||
// settings variables
|
// settings variables
|
||||||
|
var backgroundImage;
|
||||||
var dateFormat;
|
var dateFormat;
|
||||||
var drawInterval;
|
var drawInterval;
|
||||||
var pollInterval;
|
var pollInterval;
|
||||||
|
@ -31,6 +31,7 @@ function loadSettings() {
|
||||||
function def(value, def) {return value !== undefined ? value : def;}
|
function def(value, def) {return value !== undefined ? value : def;}
|
||||||
var settings = require("Storage").readJSON(SETTINGSFILE, true) || {};
|
var settings = require("Storage").readJSON(SETTINGSFILE, true) || {};
|
||||||
|
|
||||||
|
backgroundImage = def(settings.backgroundImage, "3bit");
|
||||||
dateFormat = def(settings.dateFormat, "Short");
|
dateFormat = def(settings.dateFormat, "Short");
|
||||||
drawInterval = def(settings.drawInterval, 10);
|
drawInterval = def(settings.drawInterval, 10);
|
||||||
pollInterval = def(settings.pollInterval, 60);
|
pollInterval = def(settings.pollInterval, 60);
|
||||||
|
@ -67,23 +68,29 @@ function getBatteryColor(level) {
|
||||||
level = batLevel;
|
level = batLevel;
|
||||||
}
|
}
|
||||||
if (level > 80) {
|
if (level > 80) {
|
||||||
color = [0, 0, 1];
|
color = "#00f";
|
||||||
} else if (level > 60) {
|
} else if (level > 60) {
|
||||||
color = [0, 1, 1];
|
color = "#0ff";
|
||||||
} else if (level > 40) {
|
} else if (level > 40) {
|
||||||
color = [0, 1, 0];
|
color = "#0f0";
|
||||||
} else if (level > 20) {
|
} else if (level > 20) {
|
||||||
color = [1, 1, 0];
|
color = "#f40";
|
||||||
} else {
|
} else {
|
||||||
color = [1, 0, 0];
|
color = "f00";
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
|
var background;
|
||||||
|
if (backgroundImage == "3bit") {
|
||||||
|
background = image3bit;
|
||||||
|
} else {
|
||||||
|
background = image4bit;
|
||||||
|
}
|
||||||
g.drawImage(background);
|
g.drawImage(background);
|
||||||
|
|
||||||
const color = getBatteryColor(batLevel);
|
batColor = getBatteryColor(batLevel);
|
||||||
var bat = "";
|
var bat = "";
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
const day = d.getDate();
|
const day = d.getDate();
|
||||||
|
@ -95,32 +102,38 @@ function draw() {
|
||||||
const m = d.getMinutes();
|
const m = d.getMinutes();
|
||||||
const time = d02(h) + ":" + d02(m);
|
const time = d02(h) + ":" + d02(m);
|
||||||
|
|
||||||
if (E.getBattery() < 100) {
|
|
||||||
bat = d02(E.getBattery()) + "%";
|
|
||||||
} else {
|
|
||||||
bat = E.getBattery() + "%";
|
|
||||||
}
|
|
||||||
|
|
||||||
g.reset();
|
g.reset();
|
||||||
|
|
||||||
// draw battery info
|
// draw battery info
|
||||||
g.setColor(1, 1, 1);
|
var x = 12;
|
||||||
|
var y = 16;
|
||||||
|
if (Bangle.isCharging()) {
|
||||||
|
g.setColor("#ff0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
|
||||||
|
} else {
|
||||||
|
g.clearRect(x,y,x+14,y+24);
|
||||||
|
g.setColor("#000").fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2);
|
||||||
|
g.setColor(batColor).fillRect(x+4,y+20-(batLevel*16/100),x+10,y+20);
|
||||||
|
}
|
||||||
|
if (Bangle.isCharging()) {
|
||||||
|
g.setColor("#ff0");
|
||||||
|
} else {
|
||||||
|
g.setColor(batColor);
|
||||||
|
}
|
||||||
if (useVectorFont == true) {
|
if (useVectorFont == true) {
|
||||||
g.setFont("Vector", 16);
|
g.setFont("Vector", 16);
|
||||||
g.drawString("Bat:", 12, 22, false);
|
|
||||||
} else {
|
} else {
|
||||||
g.setFont("4x6", 2);
|
g.setFont("4x6", 3);
|
||||||
g.drawString("Bat:", 10, 22, false);
|
|
||||||
}
|
}
|
||||||
g.setColor(color[0], color[1], color[2]);
|
|
||||||
if (batLevel < 100) {
|
if (batLevel < 100) {
|
||||||
g.drawString(bat, 52, 22, false);
|
bat = d02(batLevel) + "%";
|
||||||
|
g.drawString(bat, 50, 22, false);
|
||||||
} else {
|
} else {
|
||||||
g.drawString(bat, 46, 22, false);
|
bat = "100%";
|
||||||
|
g.drawString(bat, 40, 22, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw date info
|
// draw date info
|
||||||
g.setColor(0, 0, 0);
|
g.setColor("#000");
|
||||||
if (useVectorFont == true) {
|
if (useVectorFont == true) {
|
||||||
g.setFont("Vector", 20);
|
g.setFont("Vector", 20);
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,7 +149,7 @@ function draw() {
|
||||||
|
|
||||||
// draw week info
|
// draw week info
|
||||||
if (showWeekInfo == true) {
|
if (showWeekInfo == true) {
|
||||||
date2 = weekday[d.getDay()] + " " + d02(week)
|
date2 = weekday[d.getDay()] + " " + d02(week);
|
||||||
if (useVectorFont == true) {
|
if (useVectorFont == true) {
|
||||||
g.setFont("Vector", 18);
|
g.setFont("Vector", 18);
|
||||||
} else {
|
} else {
|
||||||
|
@ -155,7 +168,7 @@ function draw() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw time
|
// draw time
|
||||||
g.setColor(1, 1, 1);
|
g.setColor("#fff");
|
||||||
if (useVectorFont == true) {
|
if (useVectorFont == true) {
|
||||||
g.setFont("Vector", 60);
|
g.setFont("Vector", 60);
|
||||||
g.drawString(time, 10, 108, false);
|
g.drawString(time, 10, 108, false);
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
"name": "Monogram Watch Face",
|
"name": "Monogram Watch Face",
|
||||||
"shortName": "MonoClock",
|
"shortName": "MonoClock",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{ "url": "screenshot.png" }],
|
"screenshots": [{ "url": "screenshot0.png" }, {"url": "screenshot1.png" }],
|
||||||
"version": "0.04",
|
"version": "0.04",
|
||||||
"description": "A simple watchface based on my stylised monogram.",
|
"description": "A simple watchface based on my stylised monogram.",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
"storage": [
|
"storage": [
|
||||||
{ "name": "smclock.app.js", "url": "app.js" },
|
{ "name": "smclock.app.js", "url": "app.js" },
|
||||||
|
|
Before Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
|
@ -52,6 +52,7 @@
|
||||||
writeSettings();
|
writeSettings();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"Background": stringInSettings("backgroundImage", ["3bit", "4bit"]),
|
||||||
Date: stringInSettings("dateFormat", ["Long", "Short"]),
|
Date: stringInSettings("dateFormat", ["Long", "Short"]),
|
||||||
"Draw Interval": {
|
"Draw Interval": {
|
||||||
value: settings.drawInterval,
|
value: settings.drawInterval,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Sun Clock
|
||||||
|
Clock showing date/time, sunset/sunrise, H = current sun height/noon sun height, Az = sun azimuth
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Location set with mylocation app, time zone set with settings app.
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("kEgwhC/AC8N6APo7oPJBQndBQYPEhoaFAogZIEokO93u8AuGAAYOCCAgOLCBQOFAAIeNEBAPPBw4wHB5wuIGAwPthGIxwIC8UowUuB4eIwAPBxEk91CAgIGGwAhBBYeCAwMoA4ZwEBIIOCAxAA/ABwA="))
|
|
@ -0,0 +1,79 @@
|
||||||
|
/* sclock.app.js for Bangle2
|
||||||
|
Peter Bernschneider 30.12.2021
|
||||||
|
Update current latitude and longitude in My Location app
|
||||||
|
Update current Timezone in Settings app, menu item "System"
|
||||||
|
Update for summer time by incrementing Timezone += 1 */
|
||||||
|
setting = require("Storage").readJSON("setting.json",1);
|
||||||
|
E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ
|
||||||
|
SunCalc = require("suncalc.js");
|
||||||
|
loc = require('locale');
|
||||||
|
const LOCATION_FILE = "mylocation.json";
|
||||||
|
const xyCenter = g.getWidth() / 2 + 3;
|
||||||
|
const yposTime = 60;
|
||||||
|
const yposDate = 100;
|
||||||
|
const yposRS = 135;
|
||||||
|
const yposPos = 160;
|
||||||
|
var rise = "07:00";
|
||||||
|
var set = "20:00";
|
||||||
|
var pos = {altitude: 20, azimuth: 135};
|
||||||
|
var noonpos = {altitude: 37, azimuth: 180};
|
||||||
|
let idTimeout = null;
|
||||||
|
|
||||||
|
function updatePos() {
|
||||||
|
coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":53.3,"lon":10.1,"location":"Pattensen"};
|
||||||
|
pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon);
|
||||||
|
times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon);
|
||||||
|
rise = times.sunrise.toString().split(" ")[4].substr(0,5);
|
||||||
|
set = times.sunset.toString().split(" ")[4].substr(0,5);
|
||||||
|
noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSimpleClock() {
|
||||||
|
var d = new Date(); // get date
|
||||||
|
var da = d.toString().split(" ");
|
||||||
|
g.clear();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
g.reset(); // default draw styles
|
||||||
|
g.setFontAlign(0, 0); // drawSting centered
|
||||||
|
|
||||||
|
var time = da[4].substr(0, 5); // draw time
|
||||||
|
|
||||||
|
g.setFont("Vector",60);
|
||||||
|
g.drawString(time, xyCenter, yposTime, true);
|
||||||
|
|
||||||
|
var date = [loc.dow(new Date(),1), loc.date(d,1)].join(" "); // draw day of week, date
|
||||||
|
g.setFont("Vector",24);
|
||||||
|
g.drawString(date, xyCenter, yposDate, true);
|
||||||
|
|
||||||
|
g.setFont("Vector",25);
|
||||||
|
g.drawString(`${rise} ${set}`, xyCenter, yposRS, true); // draw riseset
|
||||||
|
g.drawImage(require("Storage").read("sunrise.img"), xyCenter-16, yposRS-16);
|
||||||
|
|
||||||
|
g.setFont("Vector",21);
|
||||||
|
g.drawString(`H${pos.altitude}/${noonpos.altitude} Az${pos.azimuth}`, xyCenter, yposPos, true); // draw sun pos
|
||||||
|
|
||||||
|
let t = d.getSeconds()*1000 + d.getMilliseconds();
|
||||||
|
idTimeout = setTimeout(drawSimpleClock, 60000 - t); // time till next minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// special function to handle display switch on
|
||||||
|
Bangle.on('lcdPower', function(on){
|
||||||
|
if (on) {
|
||||||
|
drawSimpleClock();
|
||||||
|
} else {
|
||||||
|
if(idTimeout) {
|
||||||
|
clearTimeout(idTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
g.clear(); // clean app screen
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
setInterval(updatePos, 60*5E3); // refesh every 5 mins
|
||||||
|
|
||||||
|
updatePos();
|
||||||
|
drawSimpleClock(); // draw now
|
||||||
|
|
||||||
|
setWatch(Bangle.showLauncher, BTN1, { repeat: false, edge: "falling" }); // Show launcher when button pressed
|
After Width: | Height: | Size: 776 B |
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"id": "sunclock",
|
||||||
|
"name": "Sun Clock",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "A clock with sunset/sunrise, sun height/azimuth",
|
||||||
|
"icon": "app.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"storage": [
|
||||||
|
{"name":"sunclock.app.js","url":"app.js"},
|
||||||
|
{"name":"sunclock.img","url":"app-icon.js","evaluate":true},
|
||||||
|
{"name":"suncalc.js","url":"suncalc.js"}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,298 @@
|
||||||
|
/* Module suncalc.js
|
||||||
|
(c) 2011-2015, Vladimir Agafonkin
|
||||||
|
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
|
||||||
|
https://github.com/mourner/suncalc
|
||||||
|
|
||||||
|
PB: Usage:
|
||||||
|
E.setTimeZone(2); // 1 = MEZ, 2 = MESZ
|
||||||
|
SunCalc = require("suncalc.js");
|
||||||
|
pos = SunCalc.getPosition(Date.now(), 53.3, 10.1);
|
||||||
|
times = SunCalc.getTimes(Date.now(), 53.3, 10.1);
|
||||||
|
rise = times.sunrise; // Date object
|
||||||
|
rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm
|
||||||
|
*/
|
||||||
|
var exports={};
|
||||||
|
|
||||||
|
// shortcuts for easier to read formulas
|
||||||
|
|
||||||
|
var PI = Math.PI,
|
||||||
|
sin = Math.sin,
|
||||||
|
cos = Math.cos,
|
||||||
|
tan = Math.tan,
|
||||||
|
asin = Math.asin,
|
||||||
|
atan = Math.atan2,
|
||||||
|
acos = Math.acos,
|
||||||
|
rad = PI / 180;
|
||||||
|
|
||||||
|
// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
|
||||||
|
|
||||||
|
// date/time constants and conversions
|
||||||
|
|
||||||
|
var dayMs = 1000 * 60 * 60 * 24,
|
||||||
|
J1970 = 2440588,
|
||||||
|
J2000 = 2451545;
|
||||||
|
|
||||||
|
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
|
||||||
|
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021
|
||||||
|
function toDays(date) { return toJulian(date) - J2000; }
|
||||||
|
|
||||||
|
|
||||||
|
// general calculations for position
|
||||||
|
|
||||||
|
var e = rad * 23.4397; // obliquity of the Earth
|
||||||
|
|
||||||
|
function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
|
||||||
|
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
|
||||||
|
|
||||||
|
function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
|
||||||
|
function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
|
||||||
|
|
||||||
|
function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
|
||||||
|
|
||||||
|
function astroRefraction(h) {
|
||||||
|
if (h < 0) // the following formula works for positive altitudes only.
|
||||||
|
h = 0; // if h = -0.08901179 a div/0 would occur.
|
||||||
|
|
||||||
|
// formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
|
||||||
|
// 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
|
||||||
|
return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
|
||||||
|
}
|
||||||
|
|
||||||
|
// general sun calculations
|
||||||
|
|
||||||
|
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
|
||||||
|
|
||||||
|
function eclipticLongitude(M) {
|
||||||
|
|
||||||
|
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
|
||||||
|
P = rad * 102.9372; // perihelion of the Earth
|
||||||
|
|
||||||
|
return M + C + P + PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sunCoords(d) {
|
||||||
|
|
||||||
|
var M = solarMeanAnomaly(d),
|
||||||
|
L = eclipticLongitude(M);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dec: declination(L, 0),
|
||||||
|
ra: rightAscension(L, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculates sun position for a given date and latitude/longitude
|
||||||
|
|
||||||
|
exports.getPosition = function (date, lat, lng) {
|
||||||
|
|
||||||
|
var lw = rad * -lng,
|
||||||
|
phi = rad * lat,
|
||||||
|
d = toDays(date),
|
||||||
|
|
||||||
|
c = sunCoords(d),
|
||||||
|
H = siderealTime(d, lw) - c.ra;
|
||||||
|
|
||||||
|
return {
|
||||||
|
azimuth: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg
|
||||||
|
altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// sun times configuration (angle, morning name, evening name)
|
||||||
|
|
||||||
|
var times = [
|
||||||
|
[-0.833, 'sunrise', 'sunset' ]
|
||||||
|
];
|
||||||
|
|
||||||
|
// calculations for sun times
|
||||||
|
var J0 = 0.0009;
|
||||||
|
|
||||||
|
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
|
||||||
|
|
||||||
|
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
|
||||||
|
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
|
||||||
|
|
||||||
|
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
|
||||||
|
function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
|
||||||
|
|
||||||
|
// returns set time for the given sun altitude
|
||||||
|
function getSetJ(h, lw, phi, dec, n, M, L) {
|
||||||
|
|
||||||
|
var w = hourAngle(h, phi, dec),
|
||||||
|
a = approxTransit(w, lw, n);
|
||||||
|
return solarTransitJ(a, M, L);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// calculates sun times for a given date, latitude/longitude, and, optionally,
|
||||||
|
// the observer height (in meters) relative to the horizon
|
||||||
|
|
||||||
|
exports.getTimes = function (date, lat, lng, height) {
|
||||||
|
|
||||||
|
height = height || 0;
|
||||||
|
|
||||||
|
var lw = rad * -lng,
|
||||||
|
phi = rad * lat,
|
||||||
|
|
||||||
|
dh = observerAngle(height),
|
||||||
|
|
||||||
|
d = toDays(date),
|
||||||
|
n = julianCycle(d, lw),
|
||||||
|
ds = approxTransit(0, lw, n),
|
||||||
|
|
||||||
|
M = solarMeanAnomaly(ds),
|
||||||
|
L = eclipticLongitude(M),
|
||||||
|
dec = declination(L, 0),
|
||||||
|
|
||||||
|
Jnoon = solarTransitJ(ds, M, L),
|
||||||
|
|
||||||
|
i, len, time, h0, Jset, Jrise;
|
||||||
|
|
||||||
|
|
||||||
|
var result = {
|
||||||
|
solarNoon: fromJulian(Jnoon),
|
||||||
|
nadir: fromJulian(Jnoon - 0.5)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i = 0, len = times.length; i < len; i += 1) {
|
||||||
|
time = times[i];
|
||||||
|
h0 = (time[0] + dh) * rad;
|
||||||
|
|
||||||
|
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
|
||||||
|
Jrise = Jnoon - (Jset - Jnoon);
|
||||||
|
|
||||||
|
result[time[1]] = fromJulian(Jrise);
|
||||||
|
result[time[2]] = fromJulian(Jset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
|
||||||
|
|
||||||
|
function moonCoords(d) { // geocentric ecliptic coordinates of the moon
|
||||||
|
|
||||||
|
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
|
||||||
|
M = rad * (134.963 + 13.064993 * d), // mean anomaly
|
||||||
|
F = rad * (93.272 + 13.229350 * d), // mean distance
|
||||||
|
|
||||||
|
l = L + rad * 6.289 * sin(M), // longitude
|
||||||
|
b = rad * 5.128 * sin(F), // latitude
|
||||||
|
dt = 385001 - 20905 * cos(M); // distance to the moon in km
|
||||||
|
|
||||||
|
return {
|
||||||
|
ra: rightAscension(l, b),
|
||||||
|
dec: declination(l, b),
|
||||||
|
dist: dt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getMoonPosition = function (date, lat, lng) {
|
||||||
|
|
||||||
|
var lw = rad * -lng,
|
||||||
|
phi = rad * lat,
|
||||||
|
d = toDays(date),
|
||||||
|
|
||||||
|
c = moonCoords(d),
|
||||||
|
H = siderealTime(d, lw) - c.ra,
|
||||||
|
h = altitude(H, phi, c.dec),
|
||||||
|
// formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
|
||||||
|
pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
|
||||||
|
|
||||||
|
h = h + astroRefraction(h); // altitude correction for refraction
|
||||||
|
|
||||||
|
return {
|
||||||
|
azimuth: azimuth(H, phi, c.dec),
|
||||||
|
altitude: h,
|
||||||
|
distance: c.dist,
|
||||||
|
parallacticAngle: pa
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// calculations for illumination parameters of the moon,
|
||||||
|
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
|
||||||
|
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
|
||||||
|
|
||||||
|
getMoonIllumination = function (date) {
|
||||||
|
|
||||||
|
var d = toDays(date || new Date()),
|
||||||
|
s = sunCoords(d),
|
||||||
|
m = moonCoords(d),
|
||||||
|
|
||||||
|
sdist = 149598000, // distance from Earth to Sun in km
|
||||||
|
|
||||||
|
phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
|
||||||
|
inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
|
||||||
|
angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
|
||||||
|
cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
|
||||||
|
|
||||||
|
return {
|
||||||
|
fraction: (1 + cos(inc)) / 2,
|
||||||
|
phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
|
||||||
|
angle: angle
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function hoursLater(date, h) {
|
||||||
|
return new Date(date.valueOf() + h * dayMs / 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
|
||||||
|
|
||||||
|
getMoonTimes = function (date, lat, lng, inUTC) {
|
||||||
|
var t = new Date(date);
|
||||||
|
if (inUTC) t.setUTCHours(0, 0, 0, 0);
|
||||||
|
else t.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
var hc = 0.133 * rad,
|
||||||
|
h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
|
||||||
|
h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
|
||||||
|
|
||||||
|
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
|
||||||
|
for (var i = 1; i <= 24; i += 2) {
|
||||||
|
h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
|
||||||
|
h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
|
||||||
|
|
||||||
|
a = (h0 + h2) / 2 - h1;
|
||||||
|
b = (h2 - h0) / 2;
|
||||||
|
xe = -b / (2 * a);
|
||||||
|
ye = (a * xe + b) * xe + h1;
|
||||||
|
d = b * b - 4 * a * h1;
|
||||||
|
roots = 0;
|
||||||
|
|
||||||
|
if (d >= 0) {
|
||||||
|
dx = Math.sqrt(d) / (Math.abs(a) * 2);
|
||||||
|
x1 = xe - dx;
|
||||||
|
x2 = xe + dx;
|
||||||
|
if (Math.abs(x1) <= 1) roots++;
|
||||||
|
if (Math.abs(x2) <= 1) roots++;
|
||||||
|
if (x1 < -1) x1 = x2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roots === 1) {
|
||||||
|
if (h0 < 0) rise = i + x1;
|
||||||
|
else set = i + x1;
|
||||||
|
|
||||||
|
} else if (roots === 2) {
|
||||||
|
rise = i + (ye < 0 ? x2 : x1);
|
||||||
|
set = i + (ye < 0 ? x1 : x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rise && set) break;
|
||||||
|
|
||||||
|
h0 = h2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = {};
|
||||||
|
|
||||||
|
if (rise) result.rise = hoursLater(t, rise);
|
||||||
|
if (set) result.set = hoursLater(t, set);
|
||||||
|
|
||||||
|
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Rename "Activity" in "Motion" and display the true values for it
|
0.02: Rename "Activity" in "Motion" and display the true values for it
|
||||||
|
0.03: Add Banglejs 1 compatibility
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
var locale = require("locale");
|
var locale = require("locale");
|
||||||
var fontColor = g.theme.dark ? "#0f0" : "#000";
|
var fontColor = g.theme.dark ? "#0f0" : "#000";
|
||||||
var paddingY = 2;
|
|
||||||
var font6x8At4Size = 32;
|
|
||||||
var font6x8At2Size = 18;
|
|
||||||
var heartRate = 0;
|
var heartRate = 0;
|
||||||
|
|
||||||
|
// handling the differents versions of the Banglejs smartwatch
|
||||||
|
if (process.env.HWVERSION == 1){
|
||||||
|
var paddingY = 3;
|
||||||
|
var font6x8At4Size = 48;
|
||||||
|
var font6x8At2Size = 27;
|
||||||
|
var font6x8FirstTextSize = 6;
|
||||||
|
var font6x8DefaultTextSize = 3;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
var paddingY = 2;
|
||||||
|
var font6x8At4Size = 32;
|
||||||
|
var font6x8At2Size = 18;
|
||||||
|
var font6x8FirstTextSize = 4;
|
||||||
|
var font6x8DefaultTextSize = 2;
|
||||||
|
}
|
||||||
|
|
||||||
function setFontSize(pos){
|
function setFontSize(pos){
|
||||||
if(pos == 1)
|
if(pos == 1)
|
||||||
g.setFont("6x8", 4);
|
g.setFont("6x8", font6x8FirstTextSize);
|
||||||
else
|
else
|
||||||
g.setFont("6x8", 2);
|
g.setFont("6x8", font6x8DefaultTextSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearField(pos){
|
function clearField(pos){
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
"name": "Terminal Clock",
|
"name": "Terminal Clock",
|
||||||
"shortName":"Terminal Clock",
|
"shortName":"Terminal Clock",
|
||||||
"description": "A terminal cli like clock displaying multiple sensor data",
|
"description": "A terminal cli like clock displaying multiple sensor data",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name": "terminalclock.app.js","url": "app.js"},
|
{"name": "terminalclock.app.js","url": "app.js"},
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
0.01: Initial creation of the touch timer app
|
0.01: Initial creation of the touch timer app
|
||||||
0.02: Add settings menu
|
0.02: Add settings menu
|
||||||
|
0.03: Add ability to repeat last timer
|
||||||
|
0.04: Add 5 second count down buzzer
|
||||||
|
|
|
@ -126,6 +126,14 @@ var main = () => {
|
||||||
timerIntervalId = setInterval(() => {
|
timerIntervalId = setInterval(() => {
|
||||||
timerCountDown.draw();
|
timerCountDown.draw();
|
||||||
|
|
||||||
|
// Buzz lightly when there are less then 5 seconds left
|
||||||
|
if (settings.countDownBuzz) {
|
||||||
|
var remainingSeconds = timerCountDown.getAdjustedTime().seconds;
|
||||||
|
if (remainingSeconds <= 5 && remainingSeconds > 0) {
|
||||||
|
Bangle.buzz();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (timerCountDown.isFinished()) {
|
if (timerCountDown.isFinished()) {
|
||||||
buttonStartPause.value = "FINISHED!";
|
buttonStartPause.value = "FINISHED!";
|
||||||
buttonStartPause.draw();
|
buttonStartPause.draw();
|
||||||
|
@ -141,6 +149,13 @@ var main = () => {
|
||||||
if (buzzCount >= settings.buzzCount) {
|
if (buzzCount >= settings.buzzCount) {
|
||||||
clearInterval(buzzIntervalId);
|
clearInterval(buzzIntervalId);
|
||||||
buzzIntervalId = undefined;
|
buzzIntervalId = undefined;
|
||||||
|
|
||||||
|
buttonStartPause.value = "REPEAT";
|
||||||
|
buttonStartPause.draw();
|
||||||
|
buttonStartPause.value = "START";
|
||||||
|
timerCountDown = undefined;
|
||||||
|
timerEdit.draw();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
Bangle.buzz(settings.buzzDuration * 1000, 1);
|
Bangle.buzz(settings.buzzDuration * 1000, 1);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "touchtimer",
|
"id": "touchtimer",
|
||||||
"name": "Touch Timer",
|
"name": "Touch Timer",
|
||||||
"shortName": "Touch Timer",
|
"shortName": "Touch Timer",
|
||||||
"version": "0.02",
|
"version": "0.04",
|
||||||
"description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.",
|
"description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tools",
|
"tags": "tools",
|
||||||
|
|
|
@ -31,6 +31,14 @@
|
||||||
writeSettings(settings);
|
writeSettings(settings);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"CountDown Buzz": {
|
||||||
|
value: !!settings.countDownBuzz,
|
||||||
|
format: value => value?"On":"Off",
|
||||||
|
onchange: (value) => {
|
||||||
|
settings.countDownBuzz = value;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
"Pause Between": {
|
"Pause Between": {
|
||||||
value: settings.pauseBetween,
|
value: settings.pauseBetween,
|
||||||
min: 1,
|
min: 1,
|
||||||
|
|
|
@ -59,6 +59,7 @@ options is an object containing:
|
||||||
* `label` - the text on the button
|
* `label` - the text on the button
|
||||||
* `cb` - a callback function
|
* `cb` - a callback function
|
||||||
* `cbl` - a callback function for long presses
|
* `cbl` - a callback function for long presses
|
||||||
|
* `back` - a callback function, passed as `back` into Bangle.setUI
|
||||||
|
|
||||||
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically
|
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically
|
||||||
determine what objects have changed or moved, clear their previous locations, and re-render just those objects.
|
determine what objects have changed or moved, clear their previous locations, and re-render just those objects.
|
||||||
|
@ -89,7 +90,7 @@ function Layout(layout, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this.lazy = options.lazy || false;
|
this.lazy = options.lazy || false;
|
||||||
|
|
||||||
var btnList;
|
var btnList, uiSet;
|
||||||
Bangle.setUI(); // remove all existing input handlers
|
Bangle.setUI(); // remove all existing input handlers
|
||||||
if (process.env.HWVERSION!=2) {
|
if (process.env.HWVERSION!=2) {
|
||||||
// no touchscreen, find any buttons in 'layout'
|
// no touchscreen, find any buttons in 'layout'
|
||||||
|
@ -104,7 +105,7 @@ function Layout(layout, options) {
|
||||||
this.physBtns = 0;
|
this.physBtns = 0;
|
||||||
this.buttons = btnList;
|
this.buttons = btnList;
|
||||||
this.selectedButton = -1;
|
this.selectedButton = -1;
|
||||||
Bangle.setUI("updown", dir=>{
|
Bangle.setUI({mode:"updown", back:options.back}, dir=>{
|
||||||
var s = this.selectedButton, l=this.buttons.length;
|
var s = this.selectedButton, l=this.buttons.length;
|
||||||
if (dir===undefined && this.buttons[s])
|
if (dir===undefined && this.buttons[s])
|
||||||
return this.buttons[s].cb();
|
return this.buttons[s].cb();
|
||||||
|
@ -119,8 +120,10 @@ function Layout(layout, options) {
|
||||||
}
|
}
|
||||||
this.selectedButton = s;
|
this.selectedButton = s;
|
||||||
});
|
});
|
||||||
|
uiSet = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (options.back && !uiSet) Bangle.setUI({mode: "custom", back: options.back});
|
||||||
|
|
||||||
if (options.btns) {
|
if (options.btns) {
|
||||||
var buttons = options.btns;
|
var buttons = options.btns;
|
||||||
|
|