Merge remote-tracking branch 'upstream/master'

pull/1661/head
David Peer 2022-03-23 18:02:10 +01:00
commit fb7e5501d4
82 changed files with 2259 additions and 124 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Fixed issue with wrong device informations
0.03: Ensure manufacturer:undefined doesn't overflow screen
0.04: Set Bangle.js 2 compatible, show widgets

View File

@ -5,6 +5,7 @@ let menu = {
function showMainMenu() {
menu["< Back"] = () => load();
Bangle.drawWidgets();
return E.showMenu(menu);
}
@ -55,5 +56,6 @@ function waitMessage() {
E.showMessage("scanning");
}
Bangle.loadWidgets();
scan();
waitMessage();

View File

@ -2,11 +2,11 @@
"id": "bledetect",
"name": "BLE Detector",
"shortName": "BLE Detector",
"version": "0.03",
"version": "0.04",
"description": "Detect BLE devices and show some informations.",
"icon": "bledetect.png",
"tags": "app,bluetooth,tool",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bledetect.app.js","url":"bledetect.js"},

View File

@ -48,3 +48,4 @@
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.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)

View File

@ -213,7 +213,7 @@ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
var fileSize = boot.length + bootPost.length;
bootFiles.forEach(bootFile=>{
// 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)
require('Storage').write('.boot0',boot,0,fileSize);
@ -231,8 +231,8 @@ bootFiles.forEach(bootFile=>{
var bf = require('Storage').read(bootFile);
require('Storage').write('.boot0',bf,fileOffset);
fileOffset+=bf.length;
require('Storage').write('.boot0',"\n",fileOffset);
fileOffset+=1;
require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2;
});
require('Storage').write('.boot0',bootPost,fileOffset);

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.44",
"version": "0.45",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",

View File

@ -1,2 +1,3 @@
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.03: Fix the clock for dark mode.

View File

@ -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, 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];
ticks.forEach((item)=>{
let agl = item+180;
@ -92,13 +92,13 @@ function draw_clock(){
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(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.setColor(g.theme.bg);
g.setColor(g.theme.dark ? g.theme.fg : g.theme.bg);
g.fillCircle(clock_center.x, clock_center.y, 3);
// 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++){
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});

View File

@ -1,7 +1,7 @@
{
"id": "crowclk",
"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",
"icon": "crow_clock.png",
"screenshots": [{"url":"screenshot_crow.png"}],

1
apps/cycling/ChangeLog Normal file
View File

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

34
apps/cycling/README.md Normal file
View File

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

111
apps/cycling/blecsc-emu.js Normal file
View File

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

150
apps/cycling/blecsc.js Normal file
View File

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

453
apps/cycling/cycling.app.js Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

57
apps/cycling/settings.js Normal file
View File

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

View File

@ -8,3 +8,4 @@
0.08: Optimize line wrapping 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.11: Fix bangle.js 1 white icons not displaying

View File

@ -48,6 +48,7 @@ function draw_icon(p,n,selected) {
var x = (n%3)*80;
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);
g.setColor(g.theme.fg);
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);
var txt = apps[p*6+n].name.split(" ");

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"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.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

5
apps/game1024/ChangeLog Normal file
View File

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

36
apps/game1024/README.md Normal file
View File

@ -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:
![Screenshot from the Banglejs 2 watch with the game in dark theme](./game1024_sc_dump_dark.png)
In Light theme with characters:
![Screenshot from the Banglejs 2 watch with the game in light theme](./game1024_sc_dump_light.png)

View File

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

691
apps/game1024/app.js Normal file
View File

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

View File

@ -0,0 +1,6 @@
require("Storage").write("timer.info",{
"id":"game1024",
"name":"1024 Game",
"src":"game1024.app.js",
"icon":"game1024.img"
});

View File

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

BIN
apps/game1024/game1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))
require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA="))

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Make Bangle.js 2 compatible

View File

@ -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 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 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) {
try {
@ -20,31 +99,17 @@ const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) {
function drawApp() {
g.clear();
g.setFont("6x8",2);
g.setFontAlign(0,0);
g.drawString("Joystick", 120, 120);
const d = g.getWidth() - 18;
function c(a) {
return {
width: 8,
height: a.length,
bpp: 1,
buffer: (new Uint8Array(a)).buffer
};
}
g.drawImage(c([16,56,124,254,16,16,16,16]),d,40);
g.drawImage(c([16,16,16,16,254,124,56,16]),d,194);
g.drawImage(c([0,8,12,14,255,14,12,8]),d,116);
Bangle.loadWidgets();
Bangle.drawWidgets();
layout.render();
}
function update() {
const btn1 = BTN1.read();
const btn2 = BTN2.read();
const btn3 = BTN3.read();
const btn4 = BTN4.read();
const btn5 = BTN5.read();
const btn1 = BTN1 ? BTN1.read() : 0;
const btn2 = !BANGLEJS2 ? BTN2.read() : touchBtn2;
const btn3 = !BANGLEJS2 ? BTN3.read() : touchBtn3;
const btn4 = !BANGLEJS2 ? BTN4.read() : touchBtn4;
const btn5 = !BANGLEJS2 ? BTN5.read() : touchBtn5;
const acc = Bangle.getAccel();
var x = acc.x*-127;
var y = acc.y*-127;

View File

@ -2,11 +2,11 @@
"id": "hidjoystick",
"name": "Bluetooth Joystick",
"shortName": "Joystick",
"version": "0.01",
"description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.",
"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. On Bangle.js 2 buttons 2-5 are emulated with the touchscreen.",
"icon": "app.png",
"tags": "bluetooth",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"hidjoystick.app.js","url":"app.js"},
{"name":"hidjoystick.img","url":"app-icon.js","evaluate":true}

View File

@ -6,3 +6,4 @@
0.06: Add widgets
0.07: Update scaling for new firmware
0.08: Don't force backlight on/watch unlocked on Bangle 2
0.09: Grey out BPM until confidence is over 50%

View File

@ -35,9 +35,9 @@ function onHRM(h) {
g.clearRect(0,24,g.getWidth(),80);
g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75);
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;
g.setFont("6x8");
g.setFont("6x8").setColor(g.theme.fg);
g.drawString("BPM",px+15,45);
}
Bangle.on('HRM', onHRM);
@ -101,4 +101,3 @@ function readHRM() {
lastHrmPt = [hrmOffset, y];
}
}

View File

@ -1,7 +1,7 @@
{
"id": "hrm",
"name": "Heart Rate Monitor",
"version": "0.08",
"version": "0.09",
"description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png",
"tags": "health",

View File

@ -16,4 +16,5 @@
0.16: Improved stability. Wind can now be shown.
0.17: Settings for mph/kph and other minor improvements.
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.

View File

@ -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(){
Bangle.buzz(40, 0.3);

View File

@ -1,2 +1,3 @@
0.01: New App!
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)

View File

@ -2,7 +2,7 @@
"id": "lightswitch",
"name": "Light Switch Widget",
"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.",
"icon": "images/app.png",
"screenshots": [

View File

@ -44,9 +44,11 @@
// return entry for string value
return {
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],
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);
if (entry.exec) entry.exec(entry.value[v]);
}
@ -57,8 +59,10 @@
value: settings[key] * entry.factor,
step: entry.step,
format: v => v > 0 ? v + entry.unit : "off",
min : entry.min,
max : entry.max,
wrap : true,
onchange: function(v) {
this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v;
writeSetting(key, v / entry.factor, entry.drawWidgets);
},
};
@ -133,16 +137,16 @@
title: "Light Switch"
},
"< Back": () => back(),
"-- Widget --------": 0,
"-- Widget": 0,
"Bulb col": getEntry("colors"),
"Image": getEntry("image"),
"-- Control -------": 0,
"-- Control": 0,
"Touch": getEntry("touchOn"),
"Drag Delay": getEntry("dragDelay"),
"Min Value": getEntry("minValue"),
"-- Unlock --------": 0,
"-- Unlock": 0,
"TapSide": getEntry("unlockSide"),
"-- Flash ---------": 0,
"-- Flash": 0,
"TapSide ": getEntry("tapSide"),
"Tap": getEntry("tapOn"),
"Timeout": getEntry("tOut"),

View File

@ -1 +1,2 @@
0.01: First release
0.02: Make sure to reset turns

View File

@ -3,7 +3,7 @@
"name": "Classic Mind Game",
"shortName":"Master Mind",
"icon": "mmind.png",
"version":"0.01",
"version":"0.02",
"description": "This is the classic game for masterminds",
"screenshots": [{"url":"screenshot_mmind.png"}],
"type": "app",

View File

@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) {
break;
case 4:
//new game
turn = 0;
play = [-1,-1,-1,-1];
game = [];
endgame=false;
@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) {
game = [];
get_secret();
draw();
//Bangle.loadWidgets();
//Bangle.drawWidgets();

View File

@ -219,7 +219,7 @@ function viewTrack(filename, info) {
f.erase();
viewTracks();
} else
viewTrack(n, info);
viewTrack(filename, info);
});
};
menu['< Back'] = () => { viewTracks(); };

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1 +1,2 @@
0.01: Initial Release
0.02: Shrink hand images to save memory

View File

@ -7,17 +7,17 @@ var imgBg = {
/* Set hour hand image */
var imgHour = {
width : 16, height : 176, bpp : 2,
width : 14, height : 114, bpp : 2,
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 */
var imgMin = {
width : 8, height : 176, bpp : 2,
width : 4, height : 168, bpp : 2,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AH4A/AB8P+AB/AP4B/AIcA4DPHA="))
buffer : require("heatshrink").decompress(atob("AH4AE/4A/AEI"))
};
/* Set second hand image */

View File

@ -3,7 +3,7 @@
"shortName":"5actus",
"icon": "seiko-5actus.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.01",
"version":"0.02",
"description": "A watch designed after then Seiko 5actus from the 1970's",
"tags": "clock",
"type": "clock",

View File

@ -29,11 +29,11 @@ function calc_ess(val) {
if (nonmot) {
slsnds+=1;
if (slsnds >= sleepthresh) {
return true; // awake
return true; // sleep
}
} else {
slsnds=0;
return false; // sleep
return false; // awake
}
}
}

View File

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

View File

@ -1,4 +1,6 @@
0.01: Initial version
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.05: Add ability to change background (3bit or 4bit)
0.06: Replace battery text with image

View File

@ -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.
![](screenshot.png)
![](screenshot0.png)
![](screenshot1.png)
## 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.
@ -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.
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.*

View File

@ -1,23 +1,23 @@
const SETTINGSFILE = "smclock.json";
const background = {
width: 176,
height: 176,
bpp: 3,
transparent: 1,
buffer: require("heatshrink").decompress(
atob(
"/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="
)
),
const image3bit = {
width : 176, height : 176, bpp : 3,
transparent : 1,
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="))
};
const image4bit = {
width : 176, height : 176, bpp : 4,
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 weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
// dynamic variables
var batLevel = -1;
var batColor = [0, 0, 0];
var batColor = "";
// settings variables
var backgroundImage;
var dateFormat;
var drawInterval;
var pollInterval;
@ -31,6 +31,7 @@ function loadSettings() {
function def(value, def) {return value !== undefined ? value : def;}
var settings = require("Storage").readJSON(SETTINGSFILE, true) || {};
backgroundImage = def(settings.backgroundImage, "3bit");
dateFormat = def(settings.dateFormat, "Short");
drawInterval = def(settings.drawInterval, 10);
pollInterval = def(settings.pollInterval, 60);
@ -67,23 +68,29 @@ function getBatteryColor(level) {
level = batLevel;
}
if (level > 80) {
color = [0, 0, 1];
color = "#00f";
} else if (level > 60) {
color = [0, 1, 1];
color = "#0ff";
} else if (level > 40) {
color = [0, 1, 0];
color = "#0f0";
} else if (level > 20) {
color = [1, 1, 0];
color = "#f40";
} else {
color = [1, 0, 0];
color = "f00";
}
return color;
}
function draw() {
var background;
if (backgroundImage == "3bit") {
background = image3bit;
} else {
background = image4bit;
}
g.drawImage(background);
const color = getBatteryColor(batLevel);
batColor = getBatteryColor(batLevel);
var bat = "";
const d = new Date();
const day = d.getDate();
@ -95,32 +102,38 @@ function draw() {
const m = d.getMinutes();
const time = d02(h) + ":" + d02(m);
if (E.getBattery() < 100) {
bat = d02(E.getBattery()) + "%";
} else {
bat = E.getBattery() + "%";
}
g.reset();
// 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) {
g.setFont("Vector", 16);
g.drawString("Bat:", 12, 22, false);
} else {
g.setFont("4x6", 2);
g.drawString("Bat:", 10, 22, false);
g.setFont("4x6", 3);
}
g.setColor(color[0], color[1], color[2]);
if (batLevel < 100) {
g.drawString(bat, 52, 22, false);
bat = d02(batLevel) + "%";
g.drawString(bat, 50, 22, false);
} else {
g.drawString(bat, 46, 22, false);
bat = "100%";
g.drawString(bat, 40, 22, false);
}
// draw date info
g.setColor(0, 0, 0);
g.setColor("#000");
if (useVectorFont == true) {
g.setFont("Vector", 20);
} else {
@ -136,7 +149,7 @@ function draw() {
// draw week info
if (showWeekInfo == true) {
date2 = weekday[d.getDay()] + " " + d02(week)
date2 = weekday[d.getDay()] + " " + d02(week);
if (useVectorFont == true) {
g.setFont("Vector", 18);
} else {
@ -155,7 +168,7 @@ function draw() {
}
// draw time
g.setColor(1, 1, 1);
g.setColor("#fff");
if (useVectorFont == true) {
g.setFont("Vector", 60);
g.drawString(time, 10, 108, false);

View File

@ -3,13 +3,13 @@
"name": "Monogram Watch Face",
"shortName": "MonoClock",
"icon": "app.png",
"screenshots": [{ "url": "screenshot.png" }],
"screenshots": [{ "url": "screenshot0.png" }, {"url": "screenshot1.png" }],
"version": "0.04",
"description": "A simple watchface based on my stylised monogram.",
"type": "clock",
"tags": "clock",
"readme": "README.md",
"supports": ["BANGLEJS", "BANGLEJS2"],
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{ "name": "smclock.app.js", "url": "app.js" },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -52,6 +52,7 @@
writeSettings();
},
},
"Background": stringInSettings("backgroundImage", ["3bit", "4bit"]),
Date: stringInSettings("dateFormat", ["Long", "Short"]),
"Draw Interval": {
value: settings.drawInterval,

6
apps/sunclock/README.md Normal file
View File

@ -0,0 +1,6 @@
# Sun Clock
Clock showing date/time, sunset/sunrise, H = current sun height/noon sun height, Az = sun azimuth
![](screenshot_sunclock.png)
Location set with mylocation app, time zone set with settings app.

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("kEgwhC/AC8N6APo7oPJBQndBQYPEhoaFAogZIEokO93u8AuGAAYOCCAgOLCBQOFAAIeNEBAPPBw4wHB5wuIGAwPthGIxwIC8UowUuB4eIwAPBxEk91CAgIGGwAhBBYeCAwMoA4ZwEBIIOCAxAA/ABwA="))

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

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

BIN
apps/sunclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

View File

@ -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"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

298
apps/sunclock/suncalc.js Normal file
View File

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

View File

@ -1,2 +1,3 @@
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

View File

@ -1,16 +1,28 @@
var locale = require("locale");
var fontColor = g.theme.dark ? "#0f0" : "#000";
var paddingY = 2;
var font6x8At4Size = 32;
var font6x8At2Size = 18;
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){
if(pos == 1)
g.setFont("6x8", 4);
g.setFont("6x8", font6x8FirstTextSize);
else
g.setFont("6x8", 2);
g.setFont("6x8", font6x8DefaultTextSize);
}
function clearField(pos){

View File

@ -3,11 +3,12 @@
"name": "Terminal Clock",
"shortName":"Terminal Clock",
"description": "A terminal cli like clock displaying multiple sensor data",
"version":"0.02",
"version":"0.03",
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name": "terminalclock.app.js","url": "app.js"},

View File

@ -1,2 +1,4 @@
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

View File

@ -126,6 +126,14 @@ var main = () => {
timerIntervalId = setInterval(() => {
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()) {
buttonStartPause.value = "FINISHED!";
buttonStartPause.draw();
@ -141,6 +149,13 @@ var main = () => {
if (buzzCount >= settings.buzzCount) {
clearInterval(buzzIntervalId);
buzzIntervalId = undefined;
buttonStartPause.value = "REPEAT";
buttonStartPause.draw();
buttonStartPause.value = "START";
timerCountDown = undefined;
timerEdit.draw();
return;
} else {
Bangle.buzz(settings.buzzDuration * 1000, 1);

View File

@ -2,7 +2,7 @@
"id": "touchtimer",
"name": "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.",
"icon": "app.png",
"tags": "tools",

View File

@ -31,6 +31,14 @@
writeSettings(settings);
},
},
"CountDown Buzz": {
value: !!settings.countDownBuzz,
format: value => value?"On":"Off",
onchange: (value) => {
settings.countDownBuzz = value;
writeSettings(settings);
},
},
"Pause Between": {
value: settings.pauseBetween,
min: 1,

View File

@ -59,6 +59,7 @@ options is an object containing:
* `label` - the text on the button
* `cb` - a callback function
* `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
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 || {};
this.lazy = options.lazy || false;
var btnList;
var btnList, uiSet;
Bangle.setUI(); // remove all existing input handlers
if (process.env.HWVERSION!=2) {
// no touchscreen, find any buttons in 'layout'
@ -104,7 +105,7 @@ function Layout(layout, options) {
this.physBtns = 0;
this.buttons = btnList;
this.selectedButton = -1;
Bangle.setUI("updown", dir=>{
Bangle.setUI({mode:"updown", back:options.back}, dir=>{
var s = this.selectedButton, l=this.buttons.length;
if (dir===undefined && this.buttons[s])
return this.buttons[s].cb();
@ -119,8 +120,10 @@ function Layout(layout, options) {
}
this.selectedButton = s;
});
uiSet = true;
}
}
if (options.back && !uiSet) Bangle.setUI({mode: "custom", back: options.back});
if (options.btns) {
var buttons = options.btns;