pull/218/head
msdeibel 2020-04-04 08:48:16 +02:00
commit a55efc5740
109 changed files with 3970 additions and 566 deletions

6
CHANGELOG.md Normal file
View File

@ -0,0 +1,6 @@
App Loader ChangeLog
====================
Changed for individual apps are listed in `apps/appname/ChangeLog`
* `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong

View File

@ -30,8 +30,9 @@ easily distinguish between file types, we use the following:
* `stuff.info` is JSON that describes an app - this is auto-generated by the App Loader
* `stuff.img` is an image
* `stuff.app.js` is JS code
* `stuff.app.js` is JS code for applications
* `stuff.wid.js` is JS code for widgets
* `stuff.boot.js` is JS code that automatically gets run at boot time
* `stuff.json` is used for JSON settings for an app
## Developing your own app

245
apps.json
View File

@ -2,7 +2,7 @@
{ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
"version":"0.11",
"version":"0.13",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"tags": "tool,system",
"type":"bootloader",
@ -12,6 +12,31 @@
],
"sortorder" : -10
},
{ "id": "moonphase",
"name": "Moonphase",
"icon": "app.png",
"version":"0.02",
"description": "Shows current moon phase. Now with GPS function.",
"tags": "",
"allow_emulator":true,
"storage": [
{"name":"moonphase.app.js","url":"app.js"},
{"name":"moonphase.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "daysl",
"name": "Days left",
"icon": "app.png",
"version":"0.02",
"description": "Shows you the days left until a certain date. Date can be set with a settings app and is written to a file.",
"tags": "",
"allow_emulator":false,
"storage": [
{"name":"daysl.app.js","url":"app.js"},
{"name":"daysl.img","url":"app-icon.js","evaluate":true},
{"name":"daysl.wid.js","url":"widget.js"}
]
},
{ "id": "launch",
"name": "Default Launcher",
"shortName":"Launcher",
@ -40,7 +65,7 @@
{ "id": "locale",
"name": "Languages",
"icon": "locale.png",
"version":"0.04",
"version":"0.05",
"description": "Translations for different countries",
"tags": "tool,system,locale,translate",
"type": "locale",
@ -66,7 +91,7 @@
{ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
"version":"0.04",
"version":"0.06",
"description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget",
"storage": [
@ -92,7 +117,7 @@
{ "id": "setting",
"name": "Settings",
"icon": "settings.png",
"version":"0.07",
"version":"0.08",
"description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"storage": [
@ -106,11 +131,12 @@
"name": "Default Alarm",
"shortName":"Alarms",
"icon": "app.png",
"version":"0.04",
"version":"0.05",
"description": "Set and respond to alarms",
"tags": "tool,alarm,widget",
"storage": [
{"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.boot.js","url":"boot.js"},
{"name":"alarm.js","url":"alarm.js"},
{"name":"alarm.json","content":"[]"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
@ -133,7 +159,7 @@
{ "id": "aclock",
"name": "Analog Clock",
"icon": "clock-analog.png",
"version":"0.02",
"version":"0.10",
"description": "An Analog Clock",
"tags": "clock",
"type":"clock",
@ -308,6 +334,17 @@
{"name":"widbat.wid.js","url":"widget.js"}
]
},
{ "id": "widbatpc",
"name": "Battery Level Widget (with percentage)",
"icon": "widget.png",
"version":"0.06",
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
"tags": "widget,battery",
"type":"widget",
"storage": [
{"name":"widbatpc.wid.js","url":"widget.js"}
]
},
{ "id": "widbt",
"name": "Bluetooth Widget",
"icon": "widget.png",
@ -355,8 +392,9 @@
{ "id": "swatch",
"name": "Stopwatch",
"icon": "stopwatch.png",
"version":"0.01",
"description": "Simple stopwatch with Lap Time recording",
"version":"0.05",
"interface": "interface.html",
"description": "Simple stopwatch with Lap Time logging to a JSON file",
"tags": "health",
"allow_emulator":true,
"storage": [
@ -508,6 +546,19 @@
{"name":"sclock.img","url":"clock-simple-icon.js","evaluate":true}
]
},
{ "id": "dclock",
"name": "Dev Clock",
"icon": "clock-dev.png",
"version":"0.09",
"description": "A Digital Clock including timestamp (tst), beats(@), days in current month (dm) and days since new moon (l)",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"dclock.app.js","url":"clock-dev.js"},
{"name":"dclock.img","url":"clock-dev-icon.js","evaluate":true}
]
},
{ "id": "gesture",
"name": "Gesture Test",
"icon": "gesture.png",
@ -611,7 +662,7 @@
{ "id": "miclock",
"name": "Mixed Clock",
"icon": "clock-mixed.png",
"version":"0.03",
"version":"0.04",
"description": "A mix of analog and digital Clock",
"tags": "clock",
"type":"clock",
@ -790,7 +841,7 @@
"id": "pipboy",
"name": "Pipboy",
"icon": "app.png",
"version": "0.01",
"version": "0.02",
"description": "Pipboy themed clock",
"tags": "clock",
"type":"clock",
@ -837,10 +888,25 @@
{"name":"widid.wid.js","url":"widget.js"}
]
},
{
"id": "grocery",
"name": "Grocery",
"icon": "grocery.png",
"version":"0.01",
"description": "Simple grocery list - Display a list of product and track if you already put them in your cart.",
"tags": "tool,outdoors",
"type": "app",
"custom":"grocery.html",
"storage": [
{"name":"grocery"},
{"name":"grocery.app.js"},
{"name":"grocery.img","url":"grocery-icon.js","evaluate":true}
]
},
{ "id": "marioclock",
"name": "Mario Clock",
"icon": "marioclock.png",
"version":"0.01",
"version":"0.05",
"description": "Animated Mario clock, jumps to change the time!",
"tags": "clock,mario,retro",
"type": "clock",
@ -849,5 +915,162 @@
{"name":"marioclock.app.js","url":"marioclock-app.js"},
{"name":"marioclock.img","url":"marioclock-icon.js","evaluate":true}
]
},
{ "id": "cliock",
"name": "Commandline-Clock",
"shortName":"CLI-Clock",
"icon": "app.png",
"version":"0.07",
"description": "Simple CLI-Styled Clock",
"tags": "clock,cli,command,bash,shell",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"cliock.app.js","url":"app.js"},
{"name":"cliock.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "widver",
"name": "Firmware Version Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Display the version of the installed firmware in the top widget section.",
"tags": "widget,tool,system",
"type":"widget",
"storage": [
{"name":"widver.wid.js","url":"widget.js"}
]
},
{ "id": "barclock",
"name": "Bar Clock",
"icon": "clock-bar.png",
"version":"0.04",
"description": "A simple digital clock showing seconds as a bar",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"barclock.app.js","url":"clock-bar.js"},
{"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true}
]
},
{ "id": "dotclock",
"name": "Dot Clock",
"icon": "clock-dot.png",
"version":"0.01",
"description": "A Minimal Dot Analog Clock",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"dotclock.app.js","url":"clock-dot.js"},
{"name":"dotclock.img","url":"clock-dot-icon.js","evaluate":true}
]
},
{ "id": "widtbat",
"name": "Tiny Battery Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Tiny blueish battery widget, vibs and changes level color when charging",
"tags": "widget,tool,system",
"type":"widget",
"storage": [
{"name":"widtbat.wid.js","url":"widget.js"}
]
},
{ "id": "chrono",
"name": "Chrono",
"shortName":"Chrono",
"icon": "chrono.png",
"version":"0.01",
"description": "Single click BTN1 to add 5 minutes. Single click BTN2 to add 30 seconds. Single click BTN3 to add 5 seconds. Tap to pause or play to timer. Double click BTN1 to reset. When timer finishes the watch vibrates.",
"tags": "Tools",
"storage": [
{"name":"chrono.app.js","url":"chrono.js"},
{"name":"chrono.img","url":"chrono-icon.js","evaluate":true}
]
},
{ "id": "astrocalc",
"name": "Astrocalc",
"icon": "astrocalc.png",
"version":"0.01",
"description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
"tags": "app,sun,moon,cycles,tool,outdoors",
"allow_emulator":true,
"storage": [
{"name":"astrocalc.app.js","url":"astrocalc-app.js"},
{"name":"suncalc.js","url":"suncalc.js"},
{"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true},
{"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true},
{"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true},
{"name":"waning-crescent.img","url":"waning-crescent-icon.js","evaluate":true},
{"name":"waning-gibbous.img","url":"waning-gibbous-icon.js","evaluate":true},
{"name":"full.img","url":"full-icon.js","evaluate":true},
{"name":"new.img","url":"new-icon.js","evaluate":true},
{"name":"waxing-gibbous.img","url":"waxing-gibbous-icon.js","evaluate":true},
{"name":"waxing-crescent.img","url":"waxing-crescent-icon.js","evaluate":true}
]
},
{ "id": "widhwt",
"name": "Hand Wash Timer",
"icon": "widget.png",
"version":"0.01",
"description": "Swipe your wrist over the watch face to start your personal Bangle.js hand wash timer for 35 sec. Start washing after the short buzz and stop after the long buzz.",
"tags": "widget,tool",
"type":"widget",
"storage": [
{"name":"widhwt.wid.js","url":"widget.js"}
]
},
{ "id": "toucher",
"name": "Touch Launcher",
"shortName":"Menu",
"icon": "app.png",
"version":"0.02",
"description": "Touch enable left to right launcher.",
"tags": "tool,system,launcher",
"type":"launch",
"storage": [
{"name":"toucher.app.js","url":"app.js"}
],
"sortorder" : -10
},
{
"id": "balltastic",
"name": "Balltastic",
"icon": "app.png",
"version": "0.01",
"description": "Simple but fun ball eats dots game.",
"tags": "game,fun",
"type": "app",
"storage": [
{"name":"balltastic.app.js","url":"app.js"},
{"name":"balltastic.img","url":"app-icon.js","evaluate":true}
]
},
{
"id": "rpgdice",
"name": "RPG dice",
"icon": "rpgdice.png",
"version": "0.01",
"description": "Simple RPG dice rolling app.",
"tags": "game,fun",
"type": "app",
"allow_emulator": true,
"storage": [
{"name":"rpgdice.app.js","url": "app.js"},
{"name":"rpgdice.img","url": "app-icon.js","evaluate":true}
]
},
{ "id": "widmp",
"name": "Moon Phase Widget",
"icon": "widget.png",
"version":"0.01",
"description": "Display the current moon phase in blueish for the northern hemisphere in eight phases",
"tags": "widget,tools",
"type":"widget",
"storage": [
{"name":"widmp.wid.js","url":"widget.js"}
]
}
]

View File

@ -1 +1,7 @@
0.02: Modified for use with new bootloader and firmware
0.03: add hour ticks, remove timers
0.04: add day-date display
0.07: make date and face bigger
0.08: make dots bigger and date more readable
0.09: center date, remove box around it, internal refactor to remove redundant code.
0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed

View File

@ -1,94 +1,146 @@
const p = Math.PI/2;
const PRad = Math.PI/180;
let g;
let Bangle;
let intervalRefMin = null;
let intervalRefSec = null;
// http://forum.espruino.com/conversations/345155/#comment15172813
const locale = require('locale');
const p = Math.PI / 2;
const pRad = Math.PI / 180;
const faceWidth = 100; // watch face radius
let timer = null;
let currentDate = new Date();
const centerPx = g.getWidth() / 2;
let minuteDate = new Date();
let secondDate = new Date();
const seconds = (angle) => {
const a = angle * pRad;
const x = centerPx + Math.sin(a) * faceWidth;
const y = centerPx - Math.cos(a) * faceWidth;
function seconds(angle, r) {
const a = angle*PRad;
const x = 120+Math.sin(a)*r;
const y = 120-Math.cos(a)*r;
g.fillRect(x-1,y-1,x+1,y+1);
}
function hand(angle, r1,r2) {
const a = angle*PRad;
// if 15 degrees, make hour marker larger
const radius = (angle % 15) ? 2 : 4;
g.fillCircle(x, y, radius);
};
const hand = (angle, r1, r2) => {
const a = angle * pRad;
const r3 = 3;
g.fillPoly([
120+Math.sin(a)*r1,
120-Math.cos(a)*r1,
120+Math.sin(a+p)*r3,
120-Math.cos(a+p)*r3,
120+Math.sin(a)*r2,
120-Math.cos(a)*r2,
120+Math.sin(a-p)*r3,
120-Math.cos(a-p)*r3]);
}
function drawAll() {
g.fillPoly([
Math.round(centerPx + Math.sin(a) * r1),
Math.round(centerPx - Math.cos(a) * r1),
Math.round(centerPx + Math.sin(a + p) * r3),
Math.round(centerPx - Math.cos(a + p) * r3),
Math.round(centerPx + Math.sin(a) * r2),
Math.round(centerPx - Math.cos(a) * r2),
Math.round(centerPx + Math.sin(a - p) * r3),
Math.round(centerPx - Math.cos(a - p) * r3)
]);
};
const drawAll = () => {
g.clear();
secondDate = minuteDate = new Date();
currentDate = new Date();
// draw hands first
onMinute();
// draw seconds
g.setColor(0,0,0.6);
for (let i=0;i<60;i++)
seconds(360*i/60, 90);
const currentSec = currentDate.getSeconds();
// draw all secs
for (let i = 0; i < 60; i++) {
if (i > currentSec) {
g.setColor(0, 0, 0.6);
} else {
g.setColor(0.3, 0.3, 1);
}
seconds((360 * i) / 60);
}
onSecond();
}
};
function onSecond() {
g.setColor(0,0,0.6);
seconds(360*secondDate.getSeconds()/60, 90);
g.setColor(1,0,0);
secondDate = new Date();
seconds(360*secondDate.getSeconds()/60, 90);
g.setColor(1,1,1);
const resetSeconds = () => {
g.setColor(0, 0, 0.6);
for (let i = 0; i < 60; i++) {
seconds((360 * i) / 60);
}
};
}
const onSecond = () => {
g.setColor(0.3, 0.3, 1);
seconds((360 * currentDate.getSeconds()) / 60);
if (currentDate.getSeconds() === 59) {
resetSeconds();
onMinute();
}
g.setColor(1, 0.7, 0.2);
currentDate = new Date();
seconds((360 * currentDate.getSeconds()) / 60);
g.setColor(1, 1, 1);
};
function onMinute() {
g.setColor(0,0,0);
hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -10, 50);
hand(360*minuteDate.getMinutes()/60, -10, 82);
minuteDate = new Date();
g.setColor(1,1,1);
hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -10, 50);
hand(360*minuteDate.getMinutes()/60, -10, 82);
if(minuteDate.getHours() >= 0 && minuteDate.getMinutes() === 0) {
const drawDate = () => {
g.reset();
g.setColor(1, 0, 0);
g.setFont('6x8', 2);
const dayString = locale.dow(currentDate, true);
// pad left date
const dateString = (currentDate.getDate() < 10) ? '0' : '' + currentDate.getDate().toString();
const dateDisplay = `${dayString}-${dateString}`;
// console.log(`${dayString}|${dateString}`);
// center date
const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2;
const t = centerPx + 37;
g.drawString(dateDisplay, l, t);
// console.log(l, t);
};
const onMinute = () => {
if (currentDate.getHours() === 0 && currentDate.getMinutes() === 0) {
g.clear();
resetSeconds();
}
// clear existing hands
g.setColor(0, 0, 0);
// Hour
hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35);
// Minute
hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10);
// get new date, then draw new hands
currentDate = new Date();
g.setColor(1, 0.9, 0.9);
// Hour
hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35);
g.setColor(1, 1, 0.9);
// Minute
hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10);
if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) {
Bangle.buzz();
}
}
drawDate();
};
function clearTimers() {
if(intervalRefMin) {clearInterval(intervalRefMin);}
if(intervalRefSec) {clearInterval(intervalRefSec);}
}
const startTimers = () => {
timer = setInterval(onSecond, 1000);
};
function startTimers() {
minuteDate = new Date();
secondDate = new Date();
intervalRefSec = setInterval(onSecond,1000);
intervalRefMin = setInterval(onMinute,60*1000);
drawAll();
}
Bangle.on('lcdPower',function(on) {
Bangle.on('lcdPower', (on) => {
if (on) {
g.clear();
Bangle.drawWidgets();
// g.clear();
drawAll();
startTimers();
}else {
clearTimers();
Bangle.drawWidgets();
} else {
if (timer) {
clearInterval(timer);
}
}
});
g.clear();
resetSeconds();
startTimers();
drawAll();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawAll();
startTimers();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });

View File

@ -2,3 +2,4 @@
0.02: Fix issues with alarm scheduling
0.03: More alarm scheduling issues
0.04: Tweaks for variable size widget system
0.05: Add alarm.boot.js and move code from the bootloader

24
apps/alarm/boot.js Normal file
View File

@ -0,0 +1,24 @@
// check for alarms
(function() {
var alarms = require('Storage').readJSON('alarm.json',1)||[];
var time = new Date();
var active = alarms.filter(a=>a.on&&(a.last!=time.getDate()));
if (active.length) {
active = active.sort((a,b)=>a.hr-b.hr);
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
if (!require('Storage').read("alarm.js")) {
console.log("No alarm app!");
require('Storage').write('alarm.json',"[]")
} else {
var t = 3600000*(active[0].hr-hr);
if (t<1000) t=1000;
/* execute alarm at the correct time. We avoid execing immediately
since this code will get called AGAIN when alarm.js is loaded. alarm.js
will then clearInterval() to get rid of this call so it can proceed
normally. */
setTimeout(function() {
load("alarm.js");
},t);
}
}
})()

1
apps/astrocalc/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Create astrocalc app

View File

@ -0,0 +1,348 @@
/**
* Inspired by: https://www.timeanddate.com
*/
const SunCalc = require("suncalc.js");
function drawMoon(phase, x, y) {
const moonImgFiles = [
"new",
"waxing-crescent",
"first-quarter",
"waxing-gibbous",
"full",
"waning-gibbous",
"last-quarter",
"waning-crescent",
];
img = require("Storage").read(`${moonImgFiles[phase]}.img`);
// image width & height = 92px
g.drawImage(img, x - parseInt(92 / 2), y);
}
// linear interpolation between two values a and b
// u controls amount of a/b and is in range [0.0,1.0]
function lerp(a,b,u) {
return (1-u) * a + u * b;
}
function titlizeKey(key) {
return (key[0].toUpperCase() + key.slice(1)).match(/[A-Z][a-z]+/g).join(" ");
}
function dateToTimeString(date) {
const hrs = ("0" + date.getHours()).substr(-2);
const mins = ("0" + date.getMinutes()).substr(-2);
const secs = ("0" + date.getMinutes()).substr(-2);
return `${hrs}:${mins}:${secs}`;
}
function drawTitle(key) {
const fontHeight = 16;
const x = 0;
const x2 = g.getWidth() - 1;
const y = fontHeight + 26;
const y2 = g.getHeight() - 1;
const title = titlizeKey(key);
g.setFont("6x8", 2);
g.setFontAlign(0,-1);
g.drawString(title,(x+x2)/2,y-fontHeight-2);
g.drawLine(x,y-2,x2,y-2);
}
/**
* @params {Number} angle Angle of point around a radius
* @params {Number} radius Radius of the point to be drawn, default 2
* @params {Object} color Color of the point
* @params {Number} color.r Red 0-1
* @params {Number} color.g Green 0-1
* @params {Number} color.b Blue 0-1
*/
function drawPoint(angle, radius, color) {
const pRad = Math.PI / 180;
const faceWidth = 80; // watch face radius
const centerPx = g.getWidth() / 2;
const a = angle * pRad;
const x = centerPx + Math.sin(a) * faceWidth;
const y = centerPx - Math.cos(a) * faceWidth;
if (!radius) radius = 2;
g.setColor(color.r, color.g, color.b);
g.fillCircle(x, y + 20, radius);
}
function drawPoints() {
const startColor = {r: 140, g: 255, b: 255}; // light blue
const endColor = {r: 0, g: 0, b: 140}; // dark turquoise
const steps = 60;
const step_u = 1.0 / (steps / 2);
let u = 0.0;
for (let i = 0; i < steps; i++) {
const colR = lerp(startColor.r, endColor.r, u) / 255;
const colG = lerp(startColor.g, endColor.g, u) / 255;
const colB = lerp(startColor.b, endColor.b, u) / 255;
const col = {r: colR, g: colG, b: colB};
if (i >= 0 && i <= 30) {
u += step_u;
} else {
u -= step_u;
}
drawPoint((360 * i) / steps, 2, col);
}
}
function drawData(title, obj, startX, startY) {
g.clear();
drawTitle(title);
let xPos, yPos;
if (typeof(startX) === "undefined" || startX === null) {
// Center text
g.setFontAlign(0,-1);
xPos = (0 + g.getWidth() - 2) / 2;
} else {
xPos = startX;
}
if (typeof(startY) === "undefined") {
yPos = 5;
} else {
yPos = startY;
}
g.setFont("6x8", 1);
Object.keys(obj).forEach((key) => {
g.drawString(`${key}: ${obj[key]}`, xPos, yPos += 20);
});
g.flip();
}
function drawMoonPositionPage(gps, title) {
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
const pageData = {
Azimuth: pos.azimuth.toFixed(2),
Altitude: pos.altitude.toFixed(2),
Distance: `${pos.distance.toFixed(0)} km`,
"Parallactic Ang": pos.parallacticAngle.toFixed(2),
};
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
drawData(title, pageData, null, 80);
drawPoints();
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1});
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
}, BTN3, {repeat: false, edge: "falling"});
}
function drawMoonIlluminationPage(gps, title) {
const phaseNames = [
"New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
"Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent",
];
const phase = SunCalc.getMoonIllumination(new Date());
const pageData = {
Phase: phaseNames[phase.phase],
};
drawData(title, pageData, null, 35);
drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2);
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
}, BTN3, {repease: false, edge: "falling"});
}
function drawMoonTimesPage(gps, title) {
const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon);
const pageData = {
Rise: dateToTimeString(times.rise),
Set: dateToTimeString(times.set),
};
drawData(title, pageData, null, 105);
drawPoints();
// Draw the moon rise position
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
// Draw the moon set position
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
}, BTN3, {repease: false, edge: "falling"});
}
function drawSunShowPage(gps, key, date) {
const pos = SunCalc.getPosition(date, gps.lat, gps.lon);
const hrs = ("0" + date.getHours()).substr(-2);
const mins = ("0" + date.getMinutes()).substr(-2);
const secs = ("0" + date.getMinutes()).substr(-2);
const time = `${hrs}:${mins}:${secs}`;
const azimuth = Number(pos.azimuth.toFixed(2));
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
const altitude = Number(pos.altitude.toFixed(2));
const pageData = {
Time: time,
Altitude: altitude,
Azimumth: azimuth,
Degrees: azimuthDegrees
};
drawData(key, pageData, null, 85);
drawPoints();
// Draw the suns position
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0});
m = setWatch(() => {
m = sunIndexPageMenu(gps);
}, BTN3, {repeat: false, edge: "falling"});
return null;
}
function sunIndexPageMenu(gps) {
const sunTimes = SunCalc.getTimes(new Date(), gps.lat, gps.lon);
const sunMenu = {
"": {
"title": "-- Sun --",
},
"Current Pos": () => {
m = E.showMenu();
drawSunShowPage(gps, "Current Pos", new Date());
},
};
Object.keys(sunTimes).sort().reduce((menu, key) => {
const title = titlizeKey(key);
menu[title] = () => {
m = E.showMenu();
drawSunShowPage(gps, key, sunTimes[key]);
};
return menu;
}, sunMenu);
sunMenu["< Back"] = () => m = indexPageMenu(gps);
return E.showMenu(sunMenu);
}
function moonIndexPageMenu(gps) {
const moonMenu = {
"": {
"title": "-- Moon --",
},
"Times": () => {
m = E.showMenu();
drawMoonTimesPage(gps, "Times");
},
"Position": () => {
m = E.showMenu();
drawMoonPositionPage(gps, "Position");
},
"Illumination": () => {
m = E.showMenu();
drawMoonIlluminationPage(gps, "Illumination");
},
"< Back": () => m = indexPageMenu(gps),
};
return E.showMenu(moonMenu);
}
function indexPageMenu(gps) {
const menu = {
"": {
"title": "Select",
},
"Sun": () => {
m = sunIndexPageMenu(gps);
},
"Moon": () => {
m = moonIndexPageMenu(gps);
},
"< Exit": () => { load(); }
};
return E.showMenu(menu);
}
/**
* GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page
*/
function drawGPSWaitPage() {
const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="))
g.clear();
g.drawImage(img, 100, 50);
g.setFont("6x8", 1);
g.drawString("Astrocalc v0.01", 80, 105);
g.drawString("Locating GPS", 85, 140);
g.drawString("Please wait...", 80, 155);
g.flip();
const DEBUG = false;
if (DEBUG) {
const gps = {
"lat": 56.45783133333,
"lon": -3.02188583333,
"alt": 75.3,
"speed": 0.070376,
"course": NaN,
"time":new Date(),
"satellites": 4,
"fix": 1
};
m = indexPageMenu(gps);
return;
}
Bangle.on('GPS', (gps) => {
if (gps.fix === 0) return;
Bangle.setGPSPower(0);
Bangle.buzz();
Bangle.setLCDPower(true);
m = indexPageMenu(gps);
});
}
function init() {
Bangle.setGPSPower(1);
drawGPSWaitPage();
}
let m;
init();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgI1ygf4BZM/BZMD//wCxP/8AWJ/+ACxP+CxQ6ICwP/4AWJERAWCEQ4WCERAWCEQ4WDOg4WCNA4WD/gWKRYwWDHI4WDHIwWDHI4WDHIwWEOYwWDHIwWEKAwWD/4WKKAwWEKAoWEYgwWPM4wWEM4oWQM4oWEPwwWbPwoWESowW/C34WOZ1vACxP8Cyv4CxWACyoKFCwiUFCwhmGCwh9FCwhmGCwhmFCwhPGCwgKFCwg4GCwZPGCwg4GCwY4GCwgKGCwY4GCwZxGCwjBFCwghHCwQhHCwYhHCwQhHCwRlHCwSHHCwYKICwI3HCwQKJAFAA=="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgJC/AD8B//4BRILJBQP/+AKGn4LC4AKFh4KC/4KFgYKD/gLFv4LD8AKEj4KD/+AEJAiGEIgiFIYhFFOAQADOghlDNA0HBQv+Q4wADRYZaFLgg4GHIg4GHIY4GHIhxFOYhxGOYgKHKARPHKARPHKAZPHKATBFYgoWKMw5nDMw5nCCyx9IPwQKIPwIW/C34WJZ1sDBQ/8CwM/BY/ACxkfBY+AgEBBQ/4CwJ+IBQJ+IPoJnIMwRnIMwJQIJ4RQIJ4JQIJ4RQIBQQ5HHAQ5HHAY5HHARzHOIRzHOIbEHYIIACLgpaDEQwhFEQohEIopDENAplERYwKGOgZwEBYoKIAH4AXA=="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgI0xgP8BRP/4ALI/4WJv4WJj4WJg//CxA3BCxM/CxIhCCw4hCCxAhCCw4hCCxAKCCw5lBCxEDCxSHBCxA4DCw4KCCw44DCww4DCw5xCCw44DCw5PDCw0PCxQKDCwxPDCwzBDCyRmECwxmDCyRmDCwx9ECzoKDCwyUEC34W/CyDOtn4WJgYWVgIWKj4WVPwgWFSogWGM4gWGPwYWGM4gWGM4YWGKAgWGKAYWGHIgWGKAYWHHIYWGHIYWHHIYWGHIYWHOYYWHYgQWHEQYWHEQQWIEQQWHEQQWINAQWIRYIWIOgQWIHQIWJBYIWJAFI="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgIGDh///4RHBQQLHg4KC/wKFgIKC//4BYt/BYfgBQkfBQf/wAsHFw4HCBwXwBQc/AwYLB4AhEIARIBEQn//gECgYiEIYJ2FIoQQBE4YzBDgd/NoguBNAUPKoo/BB4YhEEQIdCAYYiECQMHUwwHDEIweBLgMPWIwiBAQSlENwQTBDIQAFFQMDHAw5BOYN/HAwfB8ANCAAofCHA45B+EPHA4UBKQQAGMgMfUYQAFv+DJ45QCn5PHKAPDJ45QB/hmICwPnT4yhC/1/Mw5nBCxZmIM4P/PpB+BC34WEVZCsB/7CIYYIWWOX4WbfiwWL/gKHgf+n/ABY8/4YWJ/k/VhF/4LDIg/4j5nI/+APxEP+EPM48BCgN/KA5CBg5QHMwINCJ4/AgY5Hh4fBj45GHAKeBAQSfFMgIZCHAoqCv45GA4QOBEQsfDwQDDEIgSC/4iFv6dCg4iFj60Dn4iEEIKRCL4K5E/5uDh4QDDgKFEv4uDj4/EE4IRCDYIzEAwIvBAQKnFEQIADMIhFBAAayFNAIACMoZtDBYa9GFwbrHBQR2EBYoKEA=="))

328
apps/astrocalc/suncalc.js Normal file
View File

@ -0,0 +1,328 @@
/*
(c) 2011-2015, Vladimir Agafonkin
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
*/
(function () { 'use strict';
// 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 (j + 0.5 - J1970) * dayMs; }
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)
};
}
var SunCalc = {};
// calculates sun position for a given date and latitude/longitude
SunCalc.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: azimuth(H, phi, c.dec),
altitude: altitude(H, phi, c.dec)
};
};
// sun times configuration (angle, morning name, evening name)
var times = SunCalc.times = [
[-0.833, 'sunrise', 'sunset' ],
[ -0.3, 'sunriseEnd', 'sunsetStart' ],
[ -6, 'dawn', 'dusk' ],
[ -12, 'nauticalDawn', 'nauticalDusk'],
[ -18, 'nightEnd', 'night' ],
[ 6, 'goldenHourEnd', 'goldenHour' ]
];
// adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// 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
SunCalc.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: new Date(fromJulian(Jnoon)),
nadir: new Date(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]] = new Date(fromJulian(Jrise) - (dayMs / 2));
result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2));
}
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
};
}
SunCalc.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.
// Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c
SunCalc.getMoonIllumination = function (date) {
let month = date.getMonth();
let year = date.getFullYear();
let day = date.getDate();
let c = 0;
let e = 0;
let jd = 0;
let b = 0;
if (month < 3) {
year--;
month += 12;
}
++month;
c = 365.25 * year;
e = 30.6 * month;
jd = c + e + day - 694039.09; // jd is total days elapsed
jd /= 29.5305882; // divide by the moon cycle
b = parseInt(jd); // int(jd) -> b, take integer part of jd
jd -= b; // subtract integer part to leave fractional part of original jd
b = Math.round(jd * 8); // scale fraction from 0-8 and round
if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0
return {phase: b};
};
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
SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
var t = 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;
};
// export as Node module / AMD module / browser variable
if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc;
else if (typeof define === 'function' && define.amd) define(SunCalc);
else global.SunCalc = SunCalc;
}());

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgJC/ABHgBRN8BRMfwAKIg/4CxP/BRM/HBMH/wKIgP/4AhJ/ghJ/5PJ/5PJj4WJgf/+AWIv5mJHAIWJ/5mJHAJ9IHAIWJn59JHAJ9JJ4IWIh4WK/4WJJ4KUIYIKUJJ4IWIMwIWgMwIWIPoLCJCwLCICxYKBCxCUBC34W/Cya3WCxr8In78JgYWhj4WJgIWKPwP8SpXAM5IWJPwIWIKAIWJM4PgKBP+CxBQBCxA5CBRBQBYZA5CBRA5BSpA5CSpA5BCxJzBPxDEBPxIiBM5MDPxJFBM5IiBKBMBKBKLBKBMAhwKJAH4ABA="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgI0xgP/wAKJ/wWI///+AKHv4LBEQ8fBQP8BQ0HBQP/8A3HAAQWGn4KCHIwhDHIwhE/AhJ//AEJJQGBQZQGMoQABRQsDCwhQFQ4RnHHAgWGBQhnFHAhnFHAoWFOIhnFHAp+FJ4oWEh4WKBQp+EJ4qVEYIgWRMwwWEMwoWLVghmFVgh9GCzYKGCwaUGC34W/CxzOtn4WJgYKF/wWK8AKCgIWKj4WVPwwWDSo38BQZnG4B+JCwhnGCwhnF/AKDKA2AKBIWEHIwKEKAqrDHI4KEHIp9EHIqUEHIxmEOYp9EYgxmEEQpmFEQoKFEQhmFEQhPGNAhPFRYg4GOggKHHQSIFBYghIAFQ="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgJC/ABdwBRMD8ALJj+ABREB/wWJh/wBZN/4AKIg4iKn/4KBP/ERMfERMB/5FJj//NBP//hnJ/6LJ/45Jg45Kv45JCwI5Jn5zJPwI5JCwJQICwP/CxRQISoJQJSoLEICwRQICwJnICzJnIYYJ+JCzB+ICwKVJC34W/CxbOffgIWIfgXACxP8Cyv4CxWACyUDPpU/ShIWBPpIWBPpEHMxMAv5mJCwJPICwQKIYQI4IYQJPJCwI4ISgI4JSgIKICwI4Jn5xJSgLBIMwIhJg4hJMwIKJj4hJgJlJgE+BRMHBRIA+A"))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("rlcgI1yj/4BREH/4LJ/4LJj4LB8AKGgYKB/+ABY1/BQP+BQ0PCwQuHBQX/4A4IEQ8BCwYiGn4iJJ4YiHJ4QAB+CIGAAZoFBQn8MxCLHBQg5FMwY5GMwg5GCwo5EMwhzGPog5FCwxQECwv/PpJQFSghQFCwzEECyJnECwxnDVYoWFBQpnECwx+ECzp+DCwyVEC34W/CyDOt4AKCg4KF/gWDv4WQ/AWKwAWVBQcDShMAn5mJCwx9DCwxmEgJmJgEfJ5IWGBQasGHAisFJ4gWGHAh+FHAiVGBQhnFHAp+EOIhnGYIZnGEIpQEEIxnEEIpQEEIxQDMoo5EQ4o5FFgyKDBRAiBBRAApA="))

View File

@ -0,0 +1 @@
0.01: Initial version of Balltastic released! Happy!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEogAIkUzmciBpIVIkYWBAAUyCx0hiIXFAAMkCxhUBC4fzDAYWLiAXFAAP//5KKoMRC4UTC4k/DAPzJJERiKcCC5H/GA4uBWwp6DC4YwHCwMBDI0SMAYwHoIWBiXxdIwYCMJCjBiM/C46VDC4M/GAkRgMf//ySAQAEgKrDC4lBgMCHIQXHSwxICIwIuBAAIXIGAYABiQXBkEBTYcgC473FkQXBiETTQZ4IgECC4cholCiJGDMAIXIWgIXCmMkC4JGDJBbEDC4UACwn/mAtGSYsxilCgIXFSAqDBkMRiIFBkcxiUiC4sxXowIBC4QGBkIXBiJ2EFwsDBIPyC4ILBgMRiUyiCmJgSCC+YXDgAXDR4YuEcAn/MAIXEmcgBoXyFwjIEMAQXFkIOCUgoXF+J3CC4cxBwR1IQQx3BkUzmUSBQKkFC5IuBkVDJAJeGRwLhHFwUkC4Mxl6lFC48gFwYXCmcTOwomBC4swYIMikU0C4UxkJ3FC40xFoIXCogXBmaxDC5MyCwUiogXDmIXTJASSBC4kRU4oXDkgXFmQwDNwIWEBoIXFJAYKBZggWFC4YWCC4g7BkIWBkYWBBYYXCkYXDJAYjDkQUEEYZGEGA4XIIwwwGDAQuOGAomCFo4uGGARoBE4ZOGFxAABBwgAICxAABCyxJBGJJFJJRgVNPggsMA="))

186
apps/balltastic/app.js Normal file
View File

@ -0,0 +1,186 @@
Bangle.setLCDBrightness(1);
Bangle.setLCDMode("doublebuffered");
let points = 0;
let level = 1;
let levelSpeedStart = 0.8;
let nextLevelPoints = 20;
let levelSpeedFactor = 0.2;
let counterWidth = 10;
let gWidth = g.getWidth() - counterWidth;
let gHeight = g.getHeight();
let counter = 160;
let counterMax = 160;
let ballDims = 20;
let ballx = g.getWidth() / 2 - ballDims;
let bally = g.getHeight() / 2 - ballDims;
let dotx = g.getWidth() / 2;
let doty = g.getWidth() / 2;
let ballBuzzTime = 5;
let ballSpeedFactor = 40;
let redrawspeed = 5;
let dotwidth = 5;
let running = false;
let drawInterval;
let xBuzzed = false;
let yBuzzed = false;
let BALL = require("heatshrink").decompress(
atob(
"ikUyAROvkQ3v4405AIYHBGq9KpMhktz1/W7feAJAtBEZ9jhkhs0ZgkQ8lKxW+jAdB516627E4X8AIPWzelmolKlpJBjMFEYIpC4kQ0YBBqWKynTFYPe7gpE3ec6gnHkNFrXL7372u2E4WjhGCAIliqWrUIPeKoIpB7h9HoUoqWq999///FIJ3BhGDEIIBBgFBAoWCoUI3vY62aQIW7ymSJooLBEoIADwkQEYVhEoInEGIOjR4O1y/OrIrBUYdr198iH/74nF88cE4gpCA4MY8k59CzBAINrx2164nBtduufPWYIlF++/xkxNoMAAIJPBoSdB52a30ZkNGE4IvBoUpwkxLIOMyWEmAmE7+MqKbEsLLBH4P3zw1BAYJFBFIMY8sQ4cx44nB0tVHYITBEoO967lDgDDC1tVQ4QBD37xBjMmJ4I3BE4IxBPoOMuSrBHYL1BJYbrDvfPLoYBD889jMlEoMhkpJBwkRE4O+jB7B405LoJPEYYUx0xPG7/3vxvBmOnrXsdIOc6jxBE4JfBvfwHIafDFoMRgh3H99+zsUDIOMqWU2YlBAAO1/AnBToN76EhgpTBFYKPBGIIhBEovOrWliuc2YlBE4oABE4etu2UyVrpqJBMoKvBEIPnjvWze97ATBE4YPBEopRC64BC27nBzn0znTAIOlimtq21y4BCEoM1HYOMqIVBE44AB0tVCYIBEigVBE4U1GYIFBymywkwEoJzHABIRBMIIXBWoIDCqOEmOEiABCmIjPAA51BFoVSEoUwAIIZNA"
)
);
function reset() {
g.clear();
level = 1;
points = 0;
ballx = g.getWidth() / 2 - ballDims;
bally = g.getHeight() / 2 - ballDims;
counter = counterMax;
createRandomDot();
drawInterval = setInterval(play, redrawspeed);
running = true;
}
function collide() {
try {
Bangle.buzz(ballBuzzTime, 0.8);
} catch (e) {}
}
function createRandomDot() {
dotx = Math.floor(
Math.random() * Math.floor(gWidth - dotwidth / 2) + dotwidth / 2
);
doty = Math.floor(
Math.random() * Math.floor(gHeight - dotwidth / 2) + dotwidth / 2
);
}
function checkIfDotEaten() {
if (
ballx + ballDims > dotx &&
ballx <= dotx + dotwidth &&
bally + ballDims > doty &&
bally <= doty + dotwidth
) {
collide();
createRandomDot();
counter = counterMax;
points++;
if (points % nextLevelPoints == 0) {
level++;
}
}
}
function drawLevelText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
g.setFont("4x6", 5);
g.drawString("Level " + level, 120, 80);
}
function draw() {
//bg
g.setColor("#71c6cf");
g.fillRect(0, 0, g.getWidth(), g.getHeight());
//counter
drawCounter();
//draw level
drawLevelText();
//dot
g.setColor("#ff0000");
g.fillCircle(dotx, doty, dotwidth);
//ball
g.drawImage(BALL, ballx, bally);
g.flip();
}
function drawCounter() {
g.setColor("#000000");
g.fillRect(g.getWidth() - counterWidth, 0, g.getWidth(), gHeight);
if(counter < 40 ) g.setColor("#fc0303");
else if (counter < 80 ) g.setColor("#fc9803");
else g.setColor("#0318fc");
g.fillRect(
g.getWidth() - counterWidth,
gHeight,
g.getWidth(),
gHeight - counter
);
}
function checkCollision() {
if (ballx < 0) {
ballx = 0;
if (!xBuzzed) collide();
xBuzzed = true;
} else if (ballx > gWidth - ballDims) {
ballx = gWidth - ballDims;
if (!xBuzzed) collide();
xBuzzed = true;
} else {
xBuzzed = false;
}
if (bally < 0) {
bally = 0;
if (!yBuzzed) collide();
yBuzzed = true;
} else if (bally > gHeight - ballDims) {
bally = gHeight - ballDims;
if (!yBuzzed) collide();
yBuzzed = true;
} else {
yBuzzed = false;
}
}
function count() {
counter -= levelSpeedStart + level * levelSpeedFactor;
if (counter <= 0) {
running = false;
clearInterval(drawInterval);
setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Gameover!");},50);
}
}
function accel(values) {
ballx -= values.x * ballSpeedFactor;
bally -= values.y * ballSpeedFactor;
}
function play() {
if (running) {
accel(Bangle.getAccel());
checkCollision();
checkIfDotEaten();
count();
draw();
}
}
setTimeout(() => {
reset();
drawInterval = setInterval(play, redrawspeed);
setWatch(
() => {
if(!running) reset();
},
BTN1,
{ repeat: true }
);
running = true;
}, 10);

BIN
apps/balltastic/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

4
apps/barclock/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: Created Bar Clock
0.02: Apply locale, 12-hour setting
0.03: Fix dates drawing over each other at midnight
0.04: Small bugfix

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgJC/AD8Mgfwh/AhgFFngHBOIM8AovMDIXA5gFFDoUAmYjDAocMSoMz/4FF//P/g1CAopTLDAIABwAFGAH4AfA"))

166
apps/barclock/clock-bar.js Normal file
View File

@ -0,0 +1,166 @@
/* jshint esversion: 6 */
/**
* A simple digital clock showing seconds as a bar
**/
{
// Check settings for what type our clock should be
const is12Hour = (require('Storage').readJSON('setting.json', 1) || {})['12hour']
let locale = require('locale')
{ // add some more info to locale
let date = new Date()
date.setFullYear(1111)
date.setMonth(1, 3) // februari: months are zero-indexed
const localized = locale.date(date, true)
locale.dayFirst = /3.*2/.test(localized)
locale.hasMeridian = (locale.meridian(date) !== '')
}
const screen = {
width: g.getWidth(),
height: g.getWidth(),
middle: g.getWidth() / 2,
center: g.getHeight() / 2,
}
// hardcoded "settings"
const settings = {
time: {
color: -1,
font: '6x8',
size: (is12Hour && locale.hasMeridian) ? 6 : 8,
middle: screen.middle,
center: screen.center,
ampm: {
color: -1,
font: '6x8',
size: 2,
},
},
date: {
color: -1,
font: 'Vector',
size: 20,
middle: screen.height - 20, // at bottom of screen
center: screen.center,
},
bar: {
color: -1,
top: 155, // just below time
thickness: 6, // matches 24h time "pixel" size
},
}
const SECONDS_PER_MINUTE = 60
const timeText = function (date) {
if (!is12Hour) {
return locale.time(date, true)
}
const date12 = new Date(date.getTime())
const hours = date12.getHours()
if (hours === 0) {
date12.setHours(12)
} else if (hours > 12) {
date12.setHours(hours - 12)
}
return locale.time(date12, true)
}
const ampmText = function (date) {
return is12Hour ? locale.meridian(date) : ''
}
const dateText = function (date) {
const dayName = locale.dow(date, true),
month = locale.month(date, true),
day = date.getDate()
const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`
return `${dayName} ${dayMonth}`
}
const drawDateTime = function (date) {
const t = settings.time
g.setColor(t.color)
g.setFont(t.font, t.size)
g.setFontAlign(0, 0) // centered
g.drawString(timeText(date), t.center, t.middle, true)
if (is12Hour && locale.hasMeridian) {
const a = settings.time.ampm
g.setColor(a.color)
g.setFont(a.font, a.size)
g.setFontAlign(1, -1) // right top
// at right edge of screen, aligned with time bottom
const left = screen.width - a.size * 2,
top = t.middle + t.size - a.size
g.drawString(ampmText(date), left, top, true)
}
const d = settings.date
g.setColor(d.color)
g.setFont(d.font, d.size)
g.setFontAlign(0, 0) // centered
g.drawString(dateText(date), d.center, d.middle, true)
}
const drawBar = function (date) {
const b = settings.bar
const seconds = date.getSeconds()
if (seconds === 0) {
// zero-size rect stills draws one line of pixels, we don't want that
return
}
const fraction = seconds / SECONDS_PER_MINUTE,
width = fraction * screen.width
g.setColor(b.color)
g.fillRect(0, b.top, width, b.top + b.thickness)
}
const clearScreen = function () {
g.setColor(0)
const timeTop = settings.time.middle - (settings.time.size * 4)
g.fillRect(0, timeTop, screen.width, screen.height)
}
let lastSeconds
const tick = function () {
g.reset()
const date = new Date()
const seconds = date.getSeconds()
if (lastSeconds > seconds) {
// new minute
clearScreen()
drawDateTime(date)
}
// the bar only gets larger, so drawing on top of the previous one is fine
drawBar(date)
lastSeconds = seconds
}
let iTick
const start = function () {
lastSeconds = 99 // force redraw
tick()
iTick = setInterval(tick, 1000)
}
const stop = function () {
if (iTick) {
clearInterval(iTick)
iTick = undefined
}
}
// clean app screen
g.clear()
Bangle.loadWidgets()
Bangle.drawWidgets()
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'})
Bangle.on('lcdPower', function (on) {
if (on) {
start()
} else {
stop()
}
})
start()
}

BIN
apps/barclock/clock-bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

View File

@ -9,3 +9,6 @@
0.10: Stop users calling save() (fix #125)
If Debug info is set to 'show' don't move to Terminal if connected!
0.11: Added vibrate as beep workaround
0.12: Add an event on BTN2 to open launcher when no clock detected (fix #147)
0.13: Now automatically load *.boot.js at startup
Move alarm code into alarm.boot.js

View File

@ -39,25 +39,7 @@ E.setTimeZone(s.timezone);
delete s;
// stop users doing bad things!
global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }
// check for alarms
var alarms = require('Storage').readJSON('alarm.json',1)||[];
var time = new Date();
var active = alarms.filter(a=>a.on&&(a.last!=time.getDate()));
if (active.length) {
active = active.sort((a,b)=>a.hr-b.hr);
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
if (!require('Storage').read("alarm.js")) {
console.log("No alarm app!");
require('Storage').write('alarm.json',"[]")
} else {
var t = 3600000*(active[0].hr-hr);
if (t<1000) t=1000;
/* execute alarm at the correct time. We avoid execing immediately
since this code will get called AGAIN when alarm.js is loaded. alarm.js
will then clearInterval() to get rid of this call so it can proceed
normally. */
setTimeout(function() {
load("alarm.js");
},t);
}
}
// Load *.boot.js files
var clockApps = require('Storage').list(/\.boot\.js/).map(bootFile=>{
eval(require('Storage').read(bootFile));
});

View File

@ -12,7 +12,11 @@ if (!settings.welcomed && require("Storage").read("welcome.js")!==undefined) {
clockApp = require("Storage").read(clockApps[0].src);
delete clockApps;
}
if (!clockApp) clockApp='E.showMessage("No Clock Found")';
if (!clockApp) clockApp=`E.showMessage("No Clock Found");
setWatch(() => {
Bangle.showLauncher();
}, BTN2, {repeat:false,edge:"falling"});)
`;
delete settings;
// check to see if our clock is wrong - if it is use GPS time
if ((new Date()).getFullYear()==1970) {

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwglihGIxAWUwADBDCYTDhAXSFwQEGIxowBL4QXTx///AXWF6qnBwCTDO6EIF4KnEDwLWO/4QFx7FNdwQQEGwP4GBYUB/4QBDIYXMIgQAEDIIKCVwItJFggFEx4uKCAQUBX4QDC/B2KhASCAQP/AQQcDLpQlCLgQsCCoIGBC5IkCFon/xwxCDgIXJFwYxFHIR3ILwIkBCIeIFwQHBHgReIJAgCBOoP+MYZIHhB1EDgIRBA4ZIJC4LrEMYvoAgQXJxHvI4gtDC5OIF4QSDbYY3EC5QAKG4QXNPwg0BSBAJCIQhLCDwgXKIAwXUMo4XPFwrwKC4YOCUooVCR453DIxIXJU4IqDxwXJa45FDdgxnEC40IC4TbINQYXIRQZwDAAXv/xuBCwoXBVAgXDA4wXGSARcEC4o7BRwx4DOon+C4YiCLwxIDDAobDEYJGIGAYYBxDAD9AJDC5IwCDIYACJARGIDAapDaooWLDAZhEAoIWNMggADCqAAPA"))

73
apps/chrono/chrono.js Normal file
View File

@ -0,0 +1,73 @@
function msToTime(duration) {
var milliseconds = parseInt((duration % 1000) / 100),
seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = (hours < 10) ? "0" + hours : hours;
minutes = (minutes < 10) ? "0" + minutes : minutes;
seconds = (seconds < 10) ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
}
var counter = 0;
var started = false;
function drawInterface() {
g.clear();
g.setFontAlign(0, 0);
g.setFont("6x8", 2);
g.drawString("+5m", g.getWidth() - 30, 30);
g.drawString("+30s", g.getWidth() - 30, g.getHeight() / 2);
g.drawString("+5s", g.getWidth() - 30, g.getHeight() - 30);
g.setFontAlign(0, 0); // center font
g.setFont("6x8", 3);
// draw the current counter value
g.drawString(msToTime(counter * 1000), g.getWidth() / 2 - 30, g.getHeight() / 2);
// optional - this keeps the watch LCD lit up
g.flip();
}
function countDown() {
if (counter > 0) {
if (started) {
counter--;
drawInterface();
}
} else {
if (started) {
Bangle.buzz();
}
}
}
setWatch((p) => {
if (p.time - p.lastTime < 0.1) {
counter = 0;
started = false;
} else {
counter += 60 * 5;
}
drawInterface();
}, BTN1, { repeat: true });
setWatch(() => {
counter += 30;
drawInterface();
}, BTN2, { repeat: true });
setWatch(() => {
counter += 5;
drawInterface();
}, BTN3, { repeat: true });
Bangle.on('touch', function (button) {
started = !started;
});
var interval = setInterval(countDown, 1000);
drawInterface();

BIN
apps/chrono/chrono.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

1
apps/cliock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.07: Submitted to App Loader

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("kEgwkBiIA/ACBhLB6gqKB6g//B6I4DiDqCB40QB4MBAoIXDB40BAIIPNG44PLAoQvMB5RPEB5JvEBAav1f7wA/ABoA=="))

51
apps/cliock/app.js Normal file
View File

@ -0,0 +1,51 @@
var fontsize = 3;
var locale = require("locale");
var marginTop = 40;
var flag = false;
var WeekDays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
function drawAll(){
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
updateTime();
updateRest(new Date());
}
function updateRest(now){
let date = locale.date(now,false);
writeLine(WeekDays[now.getDay()],1);
writeLine(date,2);
}
function updateTime(){
if (!Bangle.isLCDOn()) return;
let now = new Date();
let h = now.getHours();
let m = now.getMinutes();
h = h>=10?h:"0"+h;
m = m>=10?m:"0"+m;
writeLine(h+":"+m,0);
writeLine(flag?" ":"_",3);
flag = !flag;
if(now.getMinutes() == 0)
updateRest(now);
}
function writeLineStart(line){
g.drawString(">",4,marginTop+line*30);
}
function writeLine(str,line){
g.setFont("6x8",fontsize);
g.setColor(0,1,0);
g.setFontAlign(-1,-1);
g.clearRect(0,marginTop+line*30,((str.length+1)*20),marginTop+25+line*30);
writeLineStart(line);
g.drawString(str,25,marginTop+line*30);
}
drawAll();
Bangle.on('lcdPower',function(on) {
if (on)
drawAll();
});
var click = setInterval(updateTime, 1000);
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});

BIN
apps/cliock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

2
apps/daysl/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New Widget!
0.02: Improved calculation, new image for app

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgmIAH4A/AH4A/AEEAAAgGOC/4XLAgoGIDgYXTwEIBY4JEAw8YCIOAEY4+EAwwTCL44XNO5IX/C6i6LC8YABa5AXOF67vIwA5DAw5GDMhg7HjAXWIwQLFZIoGNC/4XKAH4A/AH4A/ADoA="))

67
apps/daysl/app.js Normal file
View File

@ -0,0 +1,67 @@
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
const storage = require('Storage');
let settings;
function updateSettings() {
storage.write('daysleft.json', settings);
}
function resetSettings() {
settings = {
day : 17,
month : 6,
year: 1981
};
updateSettings();
}
settings = storage.readJSON('daysleft.json',1);
if (!settings) resetSettings();
function showMenu() {
const datemenu = {
'': {
'title': 'Set Date',
'predraw': function() {
datemenu.Date.value = settings.day;
datemenu.Month.value = settings.month;
datemenu.Year.value = settings.year;
}
},
'Day': {
value: settings.day,
min: 1,
max: 31,
step: 1,
onchange: v => {
settings.day = v;
updateSettings();
}
},
'Month': {
value: settings.month,
min: 1,
max: 12,
step: 1,
onchange: v => {
settings.month = v;
updateSettings();
}
},
'Year': {
value: settings.year,
step: 1,
onchange: v => {
settings.year = v;
updateSettings();
}
}
};
datemenu['-Exit-'] = ()=>{load();};
return E.showMenu(datemenu);
}
showMenu();

BIN
apps/daysl/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

39
apps/daysl/widget.js Normal file
View File

@ -0,0 +1,39 @@
const storage = require('Storage');
let settings;
function updateSettings() {
storage.write('daysleft.json', settings);
}
function resetSettings() {
settings = {
day : 17,
month : 6,
year: 2020
};
updateSettings();
}
settings = storage.readJSON('daysleft.json',1);
if (!settings) resetSettings();
var dd = settings.day,
mm = settings.month-1, //month is zero-based
yy = settings.year;
const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
const targetDate = new Date(yy, mm, dd);
const today = new Date();
//create date object with today, but 00:00:00
const currentYear = today.getFullYear();
const currentMonth = today.getMonth();
const currentDay = today.getDate();
const todayMorning = new Date (currentYear, currentMonth, currentDay, 0, 0, 0);
const diffDays = (targetDate - todayMorning) / oneDay;
WIDGETS["daysl"]={area:"tl",width:40,draw:function(){
g.setFont("6x8", 1);
g.drawString(diffDays,this.x+12,this.y+12);
}};

9
apps/dclock/ChangeLog Normal file
View File

@ -0,0 +1,9 @@
0.01: branched from simple clock and added seconds
0.02: add timestamp (tst)
0.03: fix timestamp round to whole number
0.04: add iso datetime and move day of the week (d) / month names (m)
0.05: add beats (@)
0.06: tidy up
0.07: add days in current month (md) and days since new moon (l)
0.08: update icon
0.09: Use localised month and day of the week from locale

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEIf4A5/8wgf/AwUB/8gh/zA4QMCl/xA4cAichgIaBiEDgMgmECDQMAkMA+EgiYvDkQJBkcQgMQDwMggUiiECG4MikEBmQWCgURiEREQIXBCIMxkIIBAoMSiQ4BGoIABKgPykRSBI4JfC+c/iARBl8zmBfEAAUvIgIAUkbAtgalB+ADDBIKSBHgUgmYJCAAa6BmCoBAYMiBIMRC4UQmEAAoQvFmUDAYUSmcxWIKMBEQKrBOw0yh8wmcyj4nBIYQDB+cwBAQA/ABUxgUDkBqBgchkMiiUikMRgSOBkR3BkEhC4MgiQHBiADBC4UQAYMRiUxkECAAITBC4MSiUQF4MTiQTBBAIDBkcCiMxkUTAYIvCAH4A/AH4AKiIPPgMxiESgUQgECgMBdAMiiUgC48ikUBiEBiIXDGQURiIbBF48RkAvCEwIvCkERgQMBRHpDBOoRhBNoJOBJIkiKYMjgcTOoMhLQMQmMDDIMjQQInEC4MhiUSkQHCC4MAkAXCiUjiZ5UiR5jLwLaBAQJ1BAgIAMCgMxMwMgkciAoMjC5pqBRwPxCoMiiUyGBsgiBBBiESVAKzBf+YACA=="))

112
apps/dclock/clock-dev.js Normal file
View File

@ -0,0 +1,112 @@
var locale = require("locale");
/* jshint esversion: 6 */
const timeFontSize = 4;
const dateFontSize = 3;
const smallFontSize = 2;
const font = "6x8";
const xyCenter = g.getWidth() / 2;
const yposTime = 50;
const yposDate = 85;
const yposTst = 115;
const yposDml = 170;
const yposDayMonth = 195;
const yposGMT = 220;
// Check settings for what type our clock should be
var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
function getUTCTime(d) {
return d.toUTCString().split(' ')[4].split(':').map(function(d){return Number(d)});
}
function drawSimpleClock() {
// get date
var d = new Date();
var da = d.toString().split(" ");
var dutc = getUTCTime(d);
g.reset(); // default draw styles
// drawSting centered
g.setFontAlign(0, 0);
// draw time
var time = da[4].split(":");
var hours = time[0],
minutes = time[1],
seconds = time[2];
var meridian = "";
if (is12Hour) {
hours = parseInt(hours,10);
meridian = "AM";
if (hours == 0) {
hours = 12;
meridian = "AM";
} else if (hours >= 12) {
meridian = "PM";
if (hours>12) hours -= 12;
}
hours = (" "+hours).substr(-2);
}
// Time
g.setFont(font, timeFontSize);
g.drawString(`${hours}:${minutes}:${seconds}`, xyCenter, yposTime, true);
g.setFont(font, smallFontSize);
g.drawString(meridian, xyCenter + 102, yposTime + 10, true);
// Date String
g.setFont(font, dateFontSize);
g.drawString(`${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()}`, xyCenter, yposDate, true);
// Timestamp
var tst = Math.round(d.getTime());
g.setFont(font, smallFontSize);
g.drawString(`tst:${tst}`, xyCenter, yposTst, true);
//Days in month
var dom = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate();
//Days since full moon
var knownnew = new Date(2020,02,24,09,28,0);
// Get millisecond difference and divide down to cycles
var cycles = (d.getTime()-knownnew.getTime())/1000/60/60/24/29.53;
// Multiply decimal component back into days since new moon
var sincenew = (cycles % 1)*29.53;
// Draw days in month and sime since new moon
g.setFont(font, smallFontSize);
g.drawString(`md:${dom} l:${sincenew.toFixed(2)}`, xyCenter, yposDml, true);
// draw Month name, Day of the week and beats
var beats = Math.floor((((dutc[0] + 1) % 24) + dutc[1] / 60 + dutc[2] / 3600) * 1000 / 24);
g.setFont(font, smallFontSize);
g.drawString(`m:${locale.month(d,true)} d:${locale.dow(d,true)} @${beats}`, xyCenter, yposDayMonth, true);
// draw gmt
var gmt = da[5];
g.setFont(font, smallFontSize);
g.drawString(gmt, xyCenter, yposGMT, true);
}
// handle switch display on by pressing BTN1
Bangle.on('lcdPower', function(on) {
if (on) drawSimpleClock();
});
// clean app screen
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
// refesh every 100 milliseconds
setInterval(drawSimpleClock, 100);
// draw now
drawSimpleClock();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});

BIN
apps/dclock/clock-dev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

1
apps/dotclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Based on the Analog Clock app, minimal dot interface

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBIf4A/AGUJyAXtACeZBCAOJh/wC6IADC4gA/XEINJC64A/AHcP+ACD/4CBTB0Ph8A+ACBAIKoKC65HKC4gA/AAfACysM5gvjTBgNKC64A/AEWZBCAXdADa4XaH4A/AAgA=="))

162
apps/dotclock/clock-dot.js Normal file
View File

@ -0,0 +1,162 @@
let g;
let Bangle;
const locale = require('locale');
const p = Math.PI / 2;
const pRad = Math.PI / 180;
const faceWidth = 100; // watch face radius
let timer = null;
let currentDate = new Date();
let hourRadius = 60;
let minRadius = 80;
const centerPx = g.getWidth() / 2;
const seconds = (angle) => {
const a = angle * pRad;
const x = centerPx + Math.sin(a) * faceWidth;
const y = centerPx - Math.cos(a) * faceWidth;
// if 15 degrees, make hour marker larger
const radius = (angle % 15) ? 2 : 4;
g.fillCircle(x, y, radius);
};
const hourDot = (angle,radius) => {
const a = angle * pRad;
const x = centerPx + Math.sin(a) * hourRadius;
const y = centerPx - Math.cos(a) * hourRadius;
g.fillCircle(x, y, radius);
};
const minDot = (angle,radius) => {
const a = angle * pRad;
const x = centerPx + Math.sin(a) * minRadius;
const y = centerPx - Math.cos(a) * minRadius;
g.fillCircle(x, y, radius);
};
const drawAll = () => {
g.clear();
currentDate = new Date();
// draw hands first
onMinute();
// draw seconds
const currentSec = currentDate.getSeconds();
// draw all secs
for (let i = 0; i < 60; i++) {
if (i > currentSec) {
g.setColor(0, 0, 0.6);
} else {
g.setColor(0.3, 0.3, 1);
}
seconds((360 * i) / 60);
}
onSecond();
};
const resetSeconds = () => {
g.setColor(0, 0, 0.6);
for (let i = 0; i < 60; i++) {
seconds((360 * i) / 60);
}
};
const drawMin = () => {
g.setColor(0.5, 0.5, 0.5);
for (let i = 0; i < 60; i++) {
minDot((360 * i) / 60,1);
}
};
const drawHour = () => {
g.setColor(0.5, 0.5, 0.5);
for (let i = 0; i < 12; i++) {
hourDot((360 * 5 * i) / 60,1);
}
};
const onSecond = () => {
g.setColor(0.3, 0.3, 1);
seconds((360 * currentDate.getSeconds()) / 60);
if (currentDate.getSeconds() === 59) {
resetSeconds();
onMinute();
}
g.setColor(1, 0.7, 0.2);
currentDate = new Date();
seconds((360 * currentDate.getSeconds()) / 60);
g.setColor(1, 1, 1);
};
const drawDate = () => {
g.reset();
g.setColor(1, 1, 1);
g.setFont('6x8', 2);
const dayString = locale.dow(currentDate, true);
// pad left date
const dateString = ((currentDate.getDate() < 10) ? '0' : '') + currentDate.getDate().toString();
const dateDisplay = `${dayString} ${dateString}`;
// console.log(`${dayString}|${dateString}`);
// center date
const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2;
const t = centerPx - 6 ;
g.drawString(dateDisplay, l, t);
// console.log(l, t);
};
const onMinute = () => {
if (currentDate.getHours() === 0 && currentDate.getMinutes() === 0) {
g.clear();
resetSeconds();
}
// clear existing hands
g.setColor(0, 0, 0);
hourDot((360 * currentDate.getHours()) / 12,4);
minDot((360 * currentDate.getMinutes()) / 60,3);
// Hour
drawHour();
// Minute
drawMin();
// get new date, then draw new hands
currentDate = new Date();
g.setColor(1, 0, 0);
// Hour
hourDot((360 * currentDate.getHours()) / 12,4);
g.setColor(1, 0.9, 0.9);
// Minute
minDot((360 * currentDate.getMinutes()) / 60,3);
if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) {
Bangle.buzz();
}
drawDate();
};
const startTimers = () => {
timer = setInterval(onSecond, 1000);
};
Bangle.on('lcdPower', (on) => {
if (on) {
// g.clear();
drawAll();
startTimers();
Bangle.drawWidgets();
} else {
if (timer) {
clearInterval(timer);
}
}
});
g.clear();
resetSeconds();
startTimers();
drawAll();
Bangle.loadWidgets();
Bangle.drawWidgets();
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });

BIN
apps/dotclock/clock-dot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -2,3 +2,6 @@
0.02: Increase contrast (darker notification background, white text)
0.03: Gadgetbridge widget now shows connection state
0.04: Tweaks for variable size widget system
0.05: Show incoming call notification
Optimize animation, limit title length
0.06: Gadgetbridge App 'Connected' state is no longer toggleable

178
apps/gbridge/PROTOCOL.md Normal file
View File

@ -0,0 +1,178 @@
# Watch -> Phone
## show toast
```
{ "t": "info", "msg": "message" }
```
t can be one of "info", "warn", "error"
## report battery level
```
{ "t": "status", "bat": 30, "volt": 30 }
```
* bat is in range 0 to 100
* volt is optional and should be greater than 0
## find phone
```
{ "t": "findPhone", "n": true }
```
n is an boolean and toggles the find phone function
## control music player
```
{ "t": "music", "n": "play" }
```
n can be one of "play", "pause", "playpause", "next", "previous", "volumeup", "volumedown", "forward", "rewind"
## control phone call
```
{ "t": "call", "n": "accept"}
```
n can be one of "accept", "end", "incoming", "outcoming", "reject", "start", "ignore"
## react to notifications
Send a response to a notification from phone
```
{
"t": "notify",
"n": "dismiss",
"id": 2,
"tel": "+491234",
"msg": "message",
}
```
* n can be one of "dismiss", "dismiss all", "open", "mute", "reply"
* id, tel and message are optional
# Phone -> Watch
## show notification
```
{
"t": "notify",
"id": 2,
"src": "app",
"title": "titel",
"subject": "subject",
"body": "message body",
"sender": "sender",
"tel": "+491234"
}
```
## notification deleted
This event is send when the user skipped a notification
```
{ "t": "notify-", "id": 2 }
```
## set alarm
```
{
"t": "alarm",
"d": [
{ "h": 13, "m": 37 },
{ "h": 8, "m": 0 }
]
}
```
## call state changed
```
{
"t": "call",
"cmd": "accept",
"name": "name",
"number": "+491234"
}
```
cmd can be one of "", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end"
## music state changed
```
{
"t": "musicstate",
"state": "play",
"position": 40,
"shuffle": 0,
"repeat": 1
}
```
## set music info
```
{
"t": "musicinfo",
"artist": "artist",
"album": "album",
"track": "track",
"dur": 1,
"c": 2,
"n" 3
}
```
* dur is the duration of the track
* c is the track count
* n is the track number
## find device
```
{
"t": "find",
"n": true
}
```
n toggles find device functionality
## set constant vibration
```
{
"t": "vibrate",
"n": 2
}
```
n is the intensity
## send weather
```
{
"t": "weather",
"temp": 10,
"hum": 71,
"txt": "condition",
"wind": 13,
"loc": "location"
}
```
* hum is the humidity
* txt is the weather condition
* loc is the location

View File

@ -4,7 +4,7 @@ function gb(j) {
var mainmenu = {
"" : { "title" : "Gadgetbridge" },
"Connected" : { value : NRF.getSecurityStatus().connected },
"Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
"Find Phone" : function() { E.showMenu(findPhone); },
"Exit" : ()=> {load();},
};

View File

@ -1,125 +1,196 @@
(function() {
var musicState = "stop";
var musicInfo = {"artist":"","album":"","track":""};
var scrollPos = 0;
function gb(j) {
Bluetooth.println(JSON.stringify(j));
(() => {
const state = {
music: "stop",
musicInfo: {
artist: "",
album: "",
track: ""
},
scrollPos: 0
};
function gbSend(message) {
Bluetooth.println(JSON.stringify(message));
}
function show(size,render) {
function showNotification(size, render) {
var oldMode = Bangle.getLCDMode();
Bangle.setLCDMode("direct");
g.setClipRect(0,240,239,319);
g.setClipRect(0, 240, 239, 319);
g.setColor("#222222");
g.fillRect(1,241,238,318);
render(320-size);
g.fillRect(1, 241, 238, 318);
render(320 - size);
g.setColor("#ffffff");
g.fillRect(0,240,1,319);
g.fillRect(238,240,239,319);
g.fillRect(2,318,238,319);
g.fillRect(0, 240, 1, 319);
g.fillRect(238, 240, 239, 319);
g.fillRect(2, 318, 238, 319);
Bangle.setLCDPower(1); // light up
Bangle.setLCDMode(oldMode); // clears cliprect
function anim() {
scrollPos-=2;
if (scrollPos<-size) scrollPos=-size;
Bangle.setLCDOffset(scrollPos);
if (scrollPos>-size) setTimeout(anim,10);
}
anim();
}
function hide() {
function anim() {
scrollPos+=4;
if (scrollPos>0) scrollPos=0;
Bangle.setLCDOffset(scrollPos);
if (scrollPos<0) setTimeout(anim,10);
state.scrollPos -= 2;
if (state.scrollPos < -size) {
state.scrollPos = -size;
}
Bangle.setLCDOffset(state.scrollPos);
if (state.scrollPos > -size) setTimeout(anim, 15);
}
anim();
}
Bangle.on('touch',function() {
if (scrollPos) hide();
});
Bangle.on('swipe',function(dir) {
if (musicState=="play") {
gb({t:"music",n:dir>0?"next":"previous"});
function hideNotification() {
function anim() {
state.scrollPos += 4;
if (state.scrollPos > 0) state.scrollPos = 0;
Bangle.setLCDOffset(state.scrollPos);
if (state.scrollPos < 0) setTimeout(anim, 10);
}
});
gb({t:"status",bat:E.getBattery()});
anim();
}
global.GB = function(j) {
switch (j.t) {
function handleNotificationEvent(event) {
// split text up at word boundaries
var txt = event.body.split("\n");
var MAXCHARS = 38;
for (var i = 0; i < txt.length; i++) {
txt[i] = txt[i].trim();
var l = txt[i];
if (l.length > MAXCHARS) {
var p = MAXCHARS;
while (p > MAXCHARS - 8 && !" \t-_".includes(l[p]))
p--;
if (p == MAXCHARS - 8) p = MAXCHARS;
txt[i] = l.substr(0, p);
txt.splice(i + 1, 0, l.substr(p));
}
}
showNotification(80, (y) => {
// TODO: icon based on src?
var x = 120;
g.setFontAlign(0, 0);
g.setFont("6x8", 1);
g.setColor("#40d040");
g.drawString(event.src, x, y + 7);
g.setColor("#ffffff");
g.setFont("6x8", 2);
if (event.title)
g.drawString(event.title.slice(0,17), x, y + 25);
g.setFont("6x8", 1);
g.setColor("#ffffff");
g.setFontAlign(-1, -1);
g.drawString(txt.join("\n"), 10, y + 40);
});
Bangle.buzz();
}
function handleMusicStateUpdate(event) {
state.music = event.state
if (state.music == "play") {
showNotification(40, (y) => {
g.setColor("#ffffff");
g.drawImage(require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")), 8, y + 8);
g.setFontAlign(-1, -1);
var x = 40;
g.setFont("4x6", 2);
g.setColor("#ffffff");
g.drawString(state.musicInfo.artist, x, y + 8);
g.setFont("6x8", 1);
g.setColor("#ffffff");
g.drawString(state.musicInfo.track, x, y + 22);
});
}
if (state.music == "pause") {
hideNotification();
}
}
function handleCallEvent(event) {
if (event.cmd == "accept") {
showNotification(40, (y) => {
g.setColor("#ffffff");
g.drawImage(require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw=")), 8, y + 8);
g.setFontAlign(-1, -1);
var x = 40;
g.setFont("4x6", 2);
g.setColor("#ffffff");
g.drawString(event.name, x, y + 8);
g.setFont("6x8", 1);
g.setColor("#ffffff");
g.drawString(event.number, x, y + 22);
});
Bangle.buzz();
}
}
global.GB = (event) => {
switch (event.t) {
case "notify":
show(80,function(y) {
// TODO: icon based on src?
var x = 120;
g.setFontAlign(0,0);
g.setFont("6x8",1);
g.setColor("#40d040");
g.drawString(j.src,x,y+7);
g.setColor("#ffffff");
g.setFont("6x8",2);
g.drawString(j.title,x,y+25);
g.setFont("6x8",1);
g.setColor("#ffffff");
// split text up a word boundaries
var txt = j.body.split("\n");
var MAXCHARS = 38;
for (var i=0;i<txt.length;i++) {
txt[i] = txt[i].trim();
var l = txt[i];
if (l.length>MAXCHARS) {
var p = MAXCHARS;
while (p>MAXCHARS-8 && !" \t-_".includes(l[p]))
p--;
if (p==MAXCHARS-8) p=MAXCHARS;
txt[i] = l.substr(0,p);
txt.splice(i+1,0,l.substr(p));
}
}
g.setFontAlign(-1,-1);
g.drawString(txt.join("\n"),10,y+40);
Bangle.buzz();
});
break;
handleNotificationEvent(event);
break;
case "musicinfo":
musicInfo = j;
state.musicInfo = event;
break;
case "musicstate":
musicState = j.state;
if (musicState=="play")
show(40,function(y) {
g.setColor("#ffffff");
g.drawImage( require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")),8,y+8);
g.setFontAlign(-1,-1);
g.setFont("6x8",1);
var x = 40;
g.setFont("4x6",2);
g.setColor("#ffffff");
g.drawString(musicInfo.artist,x,y+8);
g.setFont("6x8",1);
g.setColor("#ffffff");
g.drawString(musicInfo.track,x,y+22);
});
if (musicState=="pause")
hide();
break;
handleMusicStateUpdate(event);
break;
case "call":
handleCallEvent(event);
break;
}
};
function draw() {
g.setColor(-1);
if (NRF.getSecurityStatus().connected)
g.drawImage(require("heatshrink").decompress(atob("i0WwgHExAABCIwJCBYwJEBYkIBQ2ACgvzCwoECx/z/AKDD4WD+YLBEIYKCx//+cvnAKCBwU/mc4/8/HYv//Ev+Y4EEAePn43DBQkzn4rCEIoABBIwKHO4cjmczK42I6mqlqEEBQeIBQaDED4IgDUhi6KaBbmIA==")),this.x+1,this.y+1);
else
g.drawImage(require("heatshrink").decompress(atob("i0WwQFC1WgAgYFDAgIFClQFCwEK1W/AoIPB1f+CAMq1f7/WqwQPB/fq1Gq1/+/4dC/2/CAIaB/YbBAAO///qAoX/B4QbBDQQ7BDQQrBAAWoIIIACIIIVC0ECB4cACAZiBAoRtCAoIDBA")),this.x+1,this.y+1);
}
function changed() {
WIDGETS["gbridgew"].draw();
g.flip();// turns screen on
}
NRF.on('connected',changed);
NRF.on('disconnected',changed);
// Touch control
Bangle.on("touch", () => {
if (state.scrollPos) {
hideNotification();
}
});
WIDGETS["gbridgew"]={area:"tl",width:24,draw:draw};
Bangle.on("swipe", (dir) => {
if (state.music == "play") {
const command = dir > 0 ? "next" : "previous"
gbSend({ t: "music", n: command });
}
});
function draw() {
g.setColor(-1);
if (NRF.getSecurityStatus().connected)
g.drawImage(require("heatshrink").decompress(atob("i0WwgHExAABCIwJCBYwJEBYkIBQ2ACgvzCwoECx/z/AKDD4WD+YLBEIYKCx//+cvnAKCBwU/mc4/8/HYv//Ev+Y4EEAePn43DBQkzn4rCEIoABBIwKHO4cjmczK42I6mqlqEEBQeIBQaDED4IgDUhi6KaBbmIA==")), this.x + 1, this.y + 1);
else
g.drawImage(require("heatshrink").decompress(atob("i0WwQFC1WgAgYFDAgIFClQFCwEK1W/AoIPB1f+CAMq1f7/WqwQPB/fq1Gq1/+/4dC/2/CAIaB/YbBAAO///qAoX/B4QbBDQQ7BDQQrBAAWoIIIACIIIVC0ECB4cACAZiBAoRtCAoIDBA")), this.x + 1, this.y + 1);
}
function changedConnectionState() {
WIDGETS["gbridgew"].draw();
g.flip(); // turns screen on
}
NRF.on("connected", changedConnectionState);
NRF.on("disconnected", changedConnectionState);
WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw };
gbSend({ t: "status", bat: E.getBattery() });
})();

View File

@ -5,32 +5,9 @@
<body>
<div id="tracks"></div>
<div class="modal active" id="status-modal">
<div class="modal-overlay"></div>
<div class="modal-container">
<div class="modal-header">
<div class="modal-title h5">Please wait</div>
</div>
<div class="modal-body">
<div class="content">
Loading...
</div>
</div>
</div>
</div>
<script src="../../lib/interface.js"></script>
<script>
var domTracks = document.getElementById("tracks");
var domModal = document.getElementById("status-modal");
function showModal(title) {
domModal.querySelector(".content").innerHTML = title;
domModal.classList.add("active");
}
function hideModal(title) {
domModal.classList.remove("active");
}
function saveKML(track,title) {
var kml = `<?xml version="1.0" encoding="UTF-8"?>
@ -107,19 +84,15 @@ function trackLineToObject(l, hasTrackNumber) {
}
function downloadTrack(trackid, callback) {
showModal("Downloading Track...");
Puck.write(`\x10(function() {
var f = require("Storage").open(".gpsrc${trackid.toString(36)}","r");
var l = f.readLine();
while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); }
})()\n`,tracklist=>{
hideModal();
var track = tracklist.trim().split("\n").map(l=>trackLineToObject(l,false));
Util.showModal("Downloading Track...");
Util.readStorageFile(`.gpsrc${trackid.toString(36)}`,data=>{
Util.hideModal();
var track = data.trim().split("\n").map(l=>trackLineToObject(l,false));
callback(track);
});
}
function getTrackList() {
showModal("Loading Tracks...");
Util.showModal("Loading Tracks...");
domTracks.innerHTML = "";
Puck.write(`\x10(function() {
for (var n=0;n<36;n++) {
@ -171,7 +144,7 @@ function getTrackList() {
</div>
</div>`;
domTracks.innerHTML = html;
hideModal();
Util.hideModal();
var buttons = domTracks.querySelectorAll("button");
for (var i=0;i<buttons.length;i++) {
buttons[i].addEventListener("click",event => {
@ -179,9 +152,9 @@ function getTrackList() {
var trackid = parseInt(button.getAttribute("trackid"));
var task = button.getAttribute("task");
if (task=="delete") {
showModal("Deleting Track...");
Puck.write(`\x10require("Storage").open(".gpsrc${trackid.toString(36)}","r").erase()\n`,()=>{
hideModal();
Util.showModal("Deleting Track...");
Util.eraseStorageFile(`.gpsrc${trackid.toString(36)}`,()=>{
Util.hideModal();
getTrackList();
});
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AFEEolAC6lN7vdDCcECwPd6guVGCYuDC4cCBQMikQXQJAMjkECmcyIx4XDmUjmYvLC4XUDARHBIoIWLgATCGQdA7tEonQC5ouDDYg0BOxgSEAggwKRwgUCC6ZIDSwoXNogWDDgNCAgIWIkUEoUk6kiCgMkokipsiBIQXIki2CAgNCAoYADC5Eic4Mic4ICCAIIJCC5MzAAcykYGEAAIXOABAXTmUzGoIXVAIIXLB4SICDIovjO76PZbYR3PDI4XiI6530MIh3SC6R33C/oAOC48CCxsgC44A/ADY="))

163
apps/grocery/grocery.html Normal file
View File

@ -0,0 +1,163 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<h4>List of products</h4>
<table class="table">
<thead>
<tr>
<th>name</th>
<th>quantity</th>
<th>actions</th>
</tr>
</thead>
<tbody id="products">
</tbody>
</table>
<br><br>
<h4>Add a new product</h4>
<form id="add_product_form">
<div class="columns">
<div class="column col-4 col-xs-12">
<input class="form-input input-sm" type="text" id="add_product_name" placeholder="Name">
</div>
<div class="column col-4 col-xs-12">
<input class="form-input input-sm" value="1" type="number" id="add_product_quantity" placeholder="Quantity">
</div>
<div class="column col-4 col-xs-12">
<button id="add_product_button" class="btn btn-primary btn-sm">Add</button>
</div>
</div>
</form>
<br><br>
<button id="reset" class="btn btn-error">Reset</button> <button id="upload" class="btn btn-primary">Upload</button>
<script src="../../lib/customize.js"></script>
<script>
var products = []
try{
var stored = localStorage.getItem('grocery-product-list')
if(stored) products = JSON.parse(stored);
}catch(e){}
var $name = document.getElementById('add_product_name')
var $form = document.getElementById('add_product_form')
var $button = document.getElementById('add_product_button')
var $quantity = document.getElementById('add_product_quantity')
var $list = document.getElementById('products')
var $reset = document.getElementById('reset')
renderProducts()
$reset.addEventListener('click', reset)
$form.addEventListener('submit', event => {
event.preventDefault()
var name = $name.value.trim()
if(!name) return;
var quantity = parseInt($quantity.value)
products.push({
name, quantity,
ok: false
})
renderProducts()
$name.value = ''
$quantity.value = 1
save()
})
function save(){
localStorage.setItem('grocery-product-list',JSON.stringify(products));
}
function reset(){
products = []
save()
renderProducts()
}
function removeProduct(index){
products = products.filter((p,i) => i!==index)
save()
renderProducts()
}
function renderProducts(){
$list.innerHTML = ''
products.forEach((product,index) => {
var $product = document.createElement('tr')
$product.innerHTML = `<td>${product.name}</td><td>${product.quantity}</td><td><button class="btn btn-error" onclick="removeProduct(${index})">remove</button></td>`
$list.appendChild($product)
})
$name.focus()
}
document.getElementById("upload").addEventListener("click", function() {
var app = `
var newTime = ${Date.now()}
var products = ${JSON.stringify(products)}
var newTime = newTime;
var filename = 'grocery';
var settings = require("Storage").readJSON(filename,1)|| null;
function getSettings(){
return {
products : products,
date: newTime
};
}
if(!settings || !settings.date || settings.date < newTime){
settings = getSettings();
Bangle.buzz(500);
}
function updateSettings() {
require("Storage").writeJSON(filename, settings);
Bangle.buzz();
}
function twoChat(n){
if(n<10) return '0'+n;
return ''+n;
}
const mainMenu = settings.products.reduce(function(m, p, i){
const name = twoChat(p.quantity)+' '+p.name;
m[name] = {
value: p.ok,
format: v => v?'[x]':'[ ]',
onchange: v => {
settings.products[i].ok = v;
updateSettings();
}
};
return m;
}, {
'': { 'title': 'Grocery list' }
});
mainMenu['< Back'] = ()=>{load();};
E.showMenu(mainMenu);
`;
var icon = `require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AQ0QACF1nGAAIxpFoYwqFwwwnRggwGB4eFAggACLzwHCMAeF1WGAgOGw2x2IGCLzYGEF4YpBwotCFwJfWFwo1GSAYtBAIIABRq4vFMhAwBzoAFdzIuKAAOc4IAGGC4qEMZOiF44wXFxovleBYvIGCwmB0WjE4V/AgfG1IvCzujFQOjwoECF6WFwovBDYOFEwN/AgIwCAgOFBwYrBBAQEBzodCF6AAHww1CBpIODAAYvRDAWG2IEBAYYJFBxICCF6Ox1WxAAQfBAYQlCAAIOJAQIvUADQvn1WGR4RfbP4gAFBwgFCF7a5EdwQADF46/cL9wAQF94AGF85bB1TvmF47vdJ4bvFF8qPRFgLv/L7jPCaQq/fYYrvgJgoAGd/7v/F/4v/F5oAdF54weFyAA/AH4A3A="))`;
sendCustomizedApp({
storage:[
{name:"grocery.app.js", content:app},
{name:"grocery.img", content:icon, evaluate:true},
{name:"grocery"}
]
});
});
</script>
</body>
</html>

BIN
apps/grocery/grocery.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -5,46 +5,13 @@
<body>
<div id="records"></div>
<div class="modal active" id="status-modal">
<div class="modal-overlay"></div>
<div class="modal-container">
<div class="modal-header">
<div class="modal-name h5">Please wait</div>
</div>
<div class="modal-body">
<div class="content">
Loading...
</div>
</div>
</div>
</div>
<script src="../../lib/interface.js"></script>
<script>
var domRecords = document.getElementById("records");
var domModal = document.getElementById("status-modal");
function showModal(name) {
domModal.querySelector(".content").innerHTML = name;
domModal.classList.add("active");
}
function hideModal(name) {
domModal.classList.remove("active");
}
function saveRecord(record,name) {
var csv = `${record.map(rec=>[rec.time, rec.bpm, rec.confidence].join(",")).join("\n")}`;
var a = document.createElement("a"),
file = new Blob([csv], {type: "Comma-separated value file"});
var url = URL.createObjectURL(file);
a.href = url;
a.download = name+".csv";
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
Util.saveCSV(name, csv);
}
@ -62,20 +29,16 @@ function recordLineToObject(l, hasRecordNbr) {
}
function downloadRecord(recordNbr, callback) {
showModal("Downloading heart rate record...");
Puck.write(`\x10(function() {
var f = require("Storage").open(".heart${recordNbr.toString(36)}","r");
var l = f.readLine();
while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); }
})()\n`,recordList=>{
hideModal();
var record = recordList.trim().split("\n").map(l=>recordLineToObject(l,false));
Util.showModal("Downloading heart rate record...");
Util.readStorageFile(`.heart${recordNbr.toString(36)}`,data=>{
Util.hideModal();
var record = data.trim().split("\n").map(l=>recordLineToObject(l,false));
callback(record);
});
}
function getRecordList() {
showModal("Loading heart rate records...");
Util.showModal("Loading heart rate records...");
domRecords.innerHTML = "";
Puck.write(`\x10(function() {
for (var n=0;n<36;n++) {
@ -118,7 +81,7 @@ function getRecordList() {
</div>
</div>`;
domRecords.innerHTML = html;
hideModal();
Util.hideModal();
var buttons = domRecords.querySelectorAll("button");
for (var i=0;i<buttons.length;i++) {
buttons[i].addEventListener("click",event => {
@ -126,9 +89,9 @@ function getRecordList() {
var recordNbr = parseInt(button.getAttribute("recordNbr"));
var task = button.getAttribute("task");
if (task=="delete") {
showModal("Deleting record...");
Puck.write(`\x10require("Storage").open(".heart${recordNbr.toString(36)}","r").erase()\n`,()=>{
hideModal();
Util.showModal("Deleting record...");
Util.eraseStorageFile(`.heart${recordNbr.toString(36)}`,()=>{
Util.hideModal();
getRecordList();
});
}

View File

@ -2,3 +2,5 @@
0.02: Fix locale.currencySym
0.03: Fix global 'locale' variable
0.04: Add function meridian
0.05: Inline locale details - faster, less memory overhead
Add correct scaling for speed/distance/temperature

View File

@ -15,9 +15,55 @@
<script src="locales.js"></script>
<script>
/*
eg. the built-in en_GB is:
exports = { name : "en_GB", currencySym:"£",
translate : str=>str, // as-is
date : (d,short) => short?("0"+d.getDate()).substr(-2)+"/"+("0"+(d.getMonth()+1)).substr(-2)+"/"+d.getFullYear():d.toString().substr(4,11), // Date to "Feb 28 2020" or "28/02/2020"(short)
time : (d,short) => { // Date to "4:15.28 pm" or "15:42"(short)
if (short)
return d.toString().substr(16,5);
else {
var h = d.getHours(), m = d.getMinutes(), r = "am";
if (h==0) { h=12; }
else if (h>=12) {
if (h>12) h-=12;
r = "pm";
}
return (" "+h).substr(-2)+":"+("0"+m).substr(-2)+"."+("0"+d.getSeconds()).substr(-2)+" "+r;
}
},
dow : (d,short) => short?d.toString().substr(0,3):"Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(",")[d.getDay()], // Date to "Monday" or "Mon"(short)
month : (d,short) => short?d.toString().substr(4,3):"January,February,March,April,May,June,July,August,September,October,November,December".split(",")[d.getMonth()], // Date to "February" or "Feb"(short)
number : n => n.toString(), // more fancy?
currency : n => "£"+n.toFixed(2), // number to "£1.00"
distance : m => (m<1000)?Math.round(m)+"m":Math.round(m/160.934)/10+"mi", // meters to "123m" or "1.2mi" depending on size
speed : s => Math.round(s*0.621371)+"mph",// kph to "123mph"
temp : t => Math.round(t)+"'C" // degrees C to degrees C
meridian: d => (d.getHours() <= 12) ? "am":"pm",
};
*/
// do some sanity checks
Object.keys(locales).forEach(function(localeName) {
var locale = locales[localeName];
if (distanceUnits[locale.distance[0]]===undefined) console.error(localeName+": Unknown distance unit "+locale.distance[0]);
if (distanceUnits[locale.distance[1]]===undefined) console.error(localeName+": Unknown distance unit "+locale.distance[1]);
if (speedUnits[locale.speed]===undefined) console.error(localeName+": Unknown speed unit "+locale.speed);
});
var languageSelector = document.getElementById("languages");
languageSelector.innerHTML = Object.keys(locales).map(l=>`<option value="${l}">${l}</option>`).join("\n");
languageSelector.innerHTML = Object.keys(locales).map(l=>{
var localeParts = l.split("_"); // en_GB -> ["en","GB"]
var icon = "";
// If we have a 2 char ISO country code, use it to get the unicode flag
if (localeParts[1] && localeParts[1].length==2)
icon = localeParts[1].toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0)+127397) )+" ";
return `<option value="${l}">${icon}${l}</option>`
}).join("\n");
document.getElementById("upload").addEventListener("click", function() {
@ -30,22 +76,25 @@
return;
}
function js(x) { return JSON.stringify(x); }
var replaceList = {
"%Y": "${d.getFullYear()}",
"%y": "${(d.getFullYear().toString()).substr(-2)}",
"%m": "${('0'+(d.getMonth()+1).toString()).substr(-2)}",
"%-m": "${d.getMonth()+1}",
"%d": "${('0'+d.getDate()).substr(-2)}",
"%-d": "${d.getDate()}",
"%HH": "${('0'+d.getHours()).substr(-2)}",
"%MM": "${('0'+d.getMinutes()).substr(-2)}",
"%SS": "${('0'+d.getSeconds()).substr(-2)}",
"%A": "${locale.day.split(',')[d.getDay()]}",
"%a": "${locale.abday.split(',')[d.getDay()]}",
"%B": "${locale.month.split(',')[d.getMonth()]}",
"%b": "${locale.abmonth.split(',')[d.getMonth()]}",
"%p": "${(d.getHours()<12)?locale.ampm[0].toUpperCase():locale.ampm[1].toUpperCase()}",
"%P": "${(d.getHours()<12)?locale.ampm[0].toLowerCase():locale.ampm[1].toLowerCase()}"
"%Y": "d.getFullYear()",
"%y": "(d.getFullYear().toString()).substr(-2)",
"%m": "('0'+(d.getMonth()+1).toString()).substr(-2)",
"%-m": "d.getMonth()+1",
"%d": "('0'+d.getDate()).substr(-2)",
"%-d": "d.getDate()",
"%HH": "('0'+d.getHours()).substr(-2)",
"%MM": "('0'+d.getMinutes()).substr(-2)",
"%SS": "('0'+d.getSeconds()).substr(-2)",
"%A": "l.day.split(',')[d.getDay()]",
"%a": "l.abday.split(',')[d.getDay()]",
"%B": "l.month.split(',')[d.getMonth()]",
"%b": "l.abmonth.split(',')[d.getMonth()]",
"%p": `(d.getHours()<12)?${js(locale.ampm[0].toUpperCase())}:${js(locale.ampm[1].toUpperCase())}`,
"%P": `(d.getHours()<12)?${js(locale.ampm[0].toLowerCase())}:${js(locale.ampm[1].toLowerCase())}`
};
var timeN = locales[lang].timePattern[0];
@ -53,33 +102,51 @@
var dateN = locales[lang].datePattern[0];
var dateS = locales[lang].datePattern[1];
Object.keys(replaceList).forEach(e => {
timeN = timeN.replace(e,replaceList[e]);
timeS = timeS.replace(e,replaceList[e]);
dateN = dateN.replace(e,replaceList[e]);
dateS = dateS.replace(e,replaceList[e]);
timeN = timeN.replace(e,"${"+replaceList[e]+"}");
timeS = timeS.replace(e,"${"+replaceList[e]+"}");
dateN = dateN.replace(e,"${"+replaceList[e]+"}");
dateS = dateS.replace(e,"${"+replaceList[e]+"}");
});
var currency = locale.currency_first ?
`${js(locale.currency_symbol)} + n.toFixed(2)`:
`n.toFixed(2) + ${js(locale.currency_symbol)}`;
var temperature;
if (locale.temperature=='°C') temperature="t";
else if (locale.temperature=='°F') temperature="(t*9/5)+32";
else throw new Error("Unknown temperature unit "+locale.temperature);
var app = `var locale = ${JSON.stringify(locales[lang])};
var localeModule = `var l = ${JSON.stringify({
abday : locale.abday,
day : locale.day,
abmonth : locale.abmonth,
month : locale.month,
})};
exports = {
lang: locale.lang,
currencySym: locale.currency_symbol,
dow: (d,short) => {day = d.getDay();return (short) ? locale.abday.split(",")[day] : locale.day.split(",")[day];},
month: (d,short) => { month = d.getMonth(); return (short) ? locale.abmonth.split(",")[month] : locale.month.split(",")[month];},
number: n => n.toString().replace(locale.thousands_sep, locale.decimal_point),
currency: n => n.toFixed(2).replace(locale.thousands_sep, locale.decimal_point) + locale.currency_symbol,
distance: n => (n < 1000) ? Math.round(n) + locale.distance[0] : Math.round(n/1000) + locale.distance[1],
speed: s => Math.round(s) +locale.speed,
temp: t => Math.round(t) + locale.temperature,
translate: s => {s=""+s;return locale.trans[s]||locale.trans[s.toLowerCase()]||s;},
name: ${js(locale.lang)},
currencySym: ${js(locale.currency_symbol)},
dow: (d,short) => {day = d.getDay();return (short) ? l.abday.split(",")[day] : l.day.split(",")[day];},
month: (d,short) => { month = d.getMonth(); return (short) ? l.abmonth.split(",")[month] : l.month.split(",")[month];},
number: n => n.toString(),
currency: n => ${currency},
distance: n => (n < ${distanceUnits[locale.distance[1]]}) ? Math.round(n/${distanceUnits[locale.distance[0]]}) + ${js(locale.distance[0])} : Math.round(n/${distanceUnits[locale.distance[1]]}) + ${js(locale.distance[1])},
speed: s => Math.round(s/${speedUnits[locale.speed]}) + ${js(locale.speed)},
temp: t => Math.round(${temperature}) + ${js(locale.temperature)},
translate: s => {var t=${js(locale.trans)};s=""+s;return t[s]||t[s.toLowerCase()]||s;},
date: (d,short) => (short) ? \`${dateS}\`: \`${dateN}\`,
time: (d,short) => (short) ? \`${timeS}\`: \`${timeN}\`,
meridian: d => (d.getHours() <= 12) ? locale.ampm[0]:locale.ampm[1],
meridian: d => (d.getHours() <= 12) ? ${js(locale.ampm[0])}:${js(locale.ampm[1])},
};`;
console.log("Locale Module is:",localeModule);
/*
FIXME:
* Number/Currency need to add thousands separators: .replace(${js(locale.thousands_sep)}, ${js(locale.decimal_point)}) won't cut it as toString doesn't add separators itself
* distance (and speed) should probably use 1 decimal point for numbers less than 10
*/
sendCustomizedApp({
storage:[
{name:"locale", content:app}
{name:"locale", content:localeModule}
]
});
});

View File

@ -1,4 +1,20 @@
/* jshint esversion: 6 */
const distanceUnits = { // how many meters per X?
"m" : 1,
"yd" : 0.9144,
"mi" : 1609.34,
"km" : 1000,
"kmi" : 1000
};
const speedUnits = { // how many kph per X?
"kmh" : 1,
"kph" : 1,
"mph" : 1.60934
};
/*
timePattern / datePattern:
%Y year four digits
%y last two digits of year (00..99)
%m month (01..12)
@ -21,10 +37,10 @@ var locales = {
lang: "en_GB",
decimal_point: ".",
thousands_sep: ",",
currency_symbol: "£",
currency_symbol: "£", currency_first:true,
int_curr_symbol: "GBP",
speed: 'mph',
distance: { "0": "mi", "1": "kmi" },
distance: { "0": "yd", "1": "mi" },
temperature: '°C',
ampm: {0:"am",1:"pm"},
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
@ -55,10 +71,10 @@ var locales = {
lang: "en_US",
decimal_point: ".",
thousands_sep: ",",
currency_symbol: "$",
currency_symbol: "$", currency_first:true,
int_curr_symbol: "USD",
speed: "mph",
distance: { 0: "mi", 1: "kmi" },
distance: { 0: "yd", 1: "mi" },
temperature: "°F",
ampm: {0:"am",1:"pm"},
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
@ -348,10 +364,44 @@ var locales = {
temperature: '°C',
ampm: {0:"de",1:"du"},
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "%Y %d %b", 1: "%Y.%m.%d" }, // 2020 Feb 28" // "2020.03.01."(short)
datePattern: { 0: "%Y %b %d, %A", 1: "%Y.%m.%d" }, // 2020 Feb 28, Péntek" // "2020.03.01."(short)
abmonth: "Jan,Feb,Már,Ápr,Máj,Jún,Júl,Aug,Szep,Okt,Nov,Dec",
month: "Január,Február,Március,Április,Május,Június,Július,Augusztus,Szeptember,Október,November,December",
abday: "Vas,Hét,Ke,Szer,Csüt,Pén,Szom",
day: "Vasárnap,Hétfő,Kedd,Szerda,Csütörtök,Péntek,Szombat",
trans: { yes: "igen", Yes: "Igen", no: "nem", No: "Nem", ok: "ok", on: "be", off: "ki" }},
trans: { yes: "igen", Yes: "Igen", no: "nem", No: "Nem", ok: "ok", on: "be", off: "ki" }},
"pt_BR": {
lang: "pt_BR",
decimal_point: ",",
thousands_sep: ".",
currency_symbol: "R$", currency_first:true,
int_curr_symbol: "BRL",
speed: "kmh",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: {0:"am",1:"pm"},
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "", 1: "%d/%m/%y" },
abmonth: "Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez",
month: "Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro",
abday: "Dom,Seg,Ter,Qua,Qui,Sex,Sab",
day: "Domingo,Segunda-feira,Terça-feira,Quarta-feira,Quinta-feira,Sexta-feira,Sábado",
trans: { yes: "sim", Yes: "Sim", no: "não", No: "Não", ok: "certo", on: "ligado", off: "desligado" }},
"cs_CZ": {
lang: "cs_CZ",
decimal_point: ",",
thousands_sep: " ",
currency_symbol: "Kč",
int_curr_symbol: " CZK",
speed: 'kmh',
distance: { "0": "m", "1": "km" },
temperature: '°C',
ampm: {0:"dop",1:"odp"},
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", 1: "%d.%m.%Y" }, // "30. led 2020" // "30.01.2020"(short)
abmonth: "led,úno,bře,dub,kvě,čvn,čvc,srp,zář,říj,lis,pro",
month: "leden,únor,březen,duben,květen,červen,červenec,srpen,září,říjen,listopad,prosinec",
abday: "ne,po,út,st,čt,pá,so",
day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota",
trans: { yes: "tak", Yes: "Tak", no: "nie", No: "Nie", ok: "ok", on: "na", off: "poza" }}
};

View File

@ -1,2 +1,5 @@
0.01: Create mario app
0.02: Fix day of the week and add padding
0.03: use short date format from locale, take timeout from settings
0.04: modify date to display to be more at the original idea but still localized
0.05: use 12/24 hour clock from settings

View File

@ -1,11 +1,15 @@
/**********************************
BangleJS MARIO CLOCK V0.1.0
BangleJS MARIO CLOCK
+ Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock
+ Converting images to 1bit BMP: Image > Mode > Indexed and tick the "Use black and white (1-bit) palette", Then export as BMP.
+ Online Image convertor: https://www.espruino.com/Image+Converter
**********************************/
var locale = require("locale");
const locale = require("locale");
const storage = require('Storage');
const settings = (storage.readJSON('setting.json', 1) || {});
const timeout = settings.timeout || 10;
const is12Hour = settings["12hour"] || false;
// Screen dimensions
let W, H;
@ -270,7 +274,8 @@ function drawTime() {
drawBrick(42, 25);
const t = new Date();
const hours = ("0" + t.getHours()).substr(-2);
const h = t.getHours();
const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2);
const mins = ("0" + t.getMinutes()).substr(-2);
g.setFont("6x8");
@ -280,14 +285,13 @@ function drawTime() {
}
function drawDate() {
const date = new Date();
const day = locale.dow(date).substr(0, 3);
const dayNum = ("0" + date.getDate()).substr(-2);
const month = locale.month(date).substr(0, 3);
g.setFont("6x8");
g.setColor(LIGHTEST);
g.drawString(`${day} ${dayNum} ${month}`, 10, 0, true);
let d = new Date();
let dateStr = locale.date(d, true);
dateStr = dateStr.replace(d.getFullYear(), "").trim().replace(/\/$/i,"");
dateStr = locale.dow(d, true) + " " + dateStr;
g.drawString(dateStr, (W - g.stringWidth(dateStr))/2, 0, true);
}
function redraw() {
@ -322,7 +326,7 @@ function resetDisplayTimeout() {
displayTimeoutRef = setInterval(() => {
if (Bangle.isLCDOn()) Bangle.setLCDPower(false);
clearTimers();
}, ONE_SECOND * 10);
}, ONE_SECOND * timeout);
}
function startTimers(){
@ -372,8 +376,9 @@ function init() {
Bangle.setLCDPower(true);
}
});
startTimers();
}
// Initialise!
init();
startTimers();

View File

@ -1,2 +1,3 @@
0.02: Modified for use with new bootloader and firmware
0.03: Localization
0.04: move jshint to the top

View File

@ -1,5 +1,5 @@
var locale = require("locale");
/* jshint esversion: 6 */
var locale = require("locale");
const Radius = { "center": 8, "hour": 78, "min": 95, "dots": 102 };
const Center = { "x": 120, "y": 132 };
@ -17,7 +17,7 @@ function drawMixedClock() {
var date = new Date();
var dateArray = date.toString().split(" ");
var isEn = locale.lang.startsWith("en");
var isEn = locale.name.startsWith("en");
var point = [];
var minute = date.getMinutes();
var hour = date.getHours();

2
apps/moonphase/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Added GPS to obtain coordinates, added buttons

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIifh/wAod//wECgP//+AAoMHAoPgCwQFBDAUfw///AFBjkD//8AoMYgPnEgUQgPB/4qCgeB/YFDwHxGAeA+AFEvHAAocdAoQCBh4CBgJFBh4CBAoNwg4FBhHA+AIBgkcgJSBAoMIg5SBAoIpB/E58EGAoP8n4FD/8f8EDAoQvBgfANYOfAYPwAoP/4AtBAoWAgP4SARfBAoZYB/0/Aod/AgKJCBQSVCj4FBUIStFXIrFFaIrdGADYA=="))

353
apps/moonphase/app.js Normal file
View File

@ -0,0 +1,353 @@
//Icons from https://icons8.com
//Sun and Moon calculations from https://github.com/mourner/suncalc and https://gist.github.com/endel/dfe6bb2fbe679781948c
//varibales
const storage = require('Storage');
let coords;
var timer;
var fix;
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,
dayMs = 1000 * 60 * 60 * 24,
J1970 = 2440588,
J2000 = 2451545;
var SunCalc = {};
//pictures
function getImg(i) {
var data = {
"NewMoon": "AD8AAH/4AHwPgDwA8BwADg4AAcMAADHAAA5gAAGYAABsAAAPAAADwAAA8AAAPAAADwAAA2AAAZgAAGcAADjAAAw4AAcHAAOA8APAHwPgAf/gAA/AAA==",
"WaxingCrescentNorth" : "AD8AAH/4AHw/gDwH8BwA/g4AH8MAB/HAAf5gAD+YAA/sAAP/AAD/wAA/8AAP/AAD/wAA/2AAP5gAD+cAB/jAAfw4AH8HAD+A8B/AHw/gAf/gAA/AAA==",
"WaningCrescentSouth" : "AD8AAH/4AHw/gDwH8BwA/g4AH8MAB/HAAf5gAD+YAA/sAAP/AAD/wAA/8AAP/AAD/wAA/2AAP5gAD+cAB/jAAfw4AH8HAD+A8B/AHw/gAf/gAA/AAA==",
"FirstQuarterNorth" : "AD8AAH/4AHx/gDwf8BwH/g4B/8MAf/HAH/5gB/+YAf/sAH//AB//wAf/8AH//AB//wAf/2AH/5gB/+cAf/jAH/w4B/8HAf+A8H/AHx/gAf/gAA/AAA==",
"FirstQuarterSouth" : "AD8AAH/4AH+PgD/g8B/4Dg/+AcP/gDH/4A5/+AGf/gBv/4AP/+AD//gA//4AP/+AD//gA3/4AZ/+AGf/gDj/4Aw/+AcH/gOA/4PAH+PgAf/gAA/AAA==",
"WaxingGibbousNorth" : "AD8AAH/4AH3/gDz/8Bw//g4f/8MH//HB//5g//+YP//sD///A///wP//8D///A///wP//2D//5g//+cH//jB//w4f/8HD/+A8//AH3/gAf/gAA/AAA==",
"WaxingGibbousSouth" : "AD8AAH/4AH/vgD/88B//Dg//4cP/+DH//g5//8Gf//Bv//wP//8D///A///wP//8D///A3//wZ//8Gf/+Dj//gw//4cH/8OA//PAH/vgAf/gAA/AAA==",
"FullMoon" : "AD8AAH/4AH//gD//8B///g///8P///H///5///+f///v/////////////////////////3///5///+f///j///w///8H//+A///AH//gAf/gAA/AAA==",
"WaningGibbousNorth" : "AD8AAH/4AH/vgD/88B//Dg//4cP/+DH//g5//8Gf//Bv//wP//8D///A///wP//8D///A3//wZ//8Gf/+Dj//gw//4cH/8OA//PAH/vgAf/gAA/AAA==",
"WaningGibbousSouth" : "AD8AAH/4AH3/gDz/8Bw//g4f/8MH//HB//5g//+YP//sD///A///wP//8D///A///wP//2D//5g//+cH//jB//w4f/8HD/+A8//AH3/gAf/gAA/AAA==",
"LastQuarterNorth" : "AD8AAH/4AH+PgD/g8B/4Dg/+AcP/gDH/4A5/+AGf/gBv/4AP/+AD//gA//4AP/+AD//gA3/4AZ/+AGf/gDj/4Aw/+AcH/gOA/4PAH+PgAf/gAA/AAA==",
"LastQuarterSouth" : "AD8AAH/4AHx/gDwf8BwH/g4B/8MAf/HAH/5gB/+YAf/sAH//AB//wAf/8AH//AB//wAf/2AH/5gB/+cAf/jAH/w4B/8HAf+A8H/AHx/gAf/gAA/AAA==",
"WaningCrescentNorth" : "AD8AAH/4AH8PgD+A8B/ADg/gAcP4ADH+AA5/AAGfwABv8AAP/AAD/wAA/8AAP/AAD/wAA38AAZ/AAGf4ADj+AAw/gAcH8AOA/gPAH8PgAf/gAA/AAA==",
"WaxingCrescentSouth" : "AD8AAH/4AH8PgD+A8B/ADg/gAcP4ADH+AA5/AAGfwABv8AAP/AAD/wAA/8AAP/AAD/wAA38AAZ/AAGf4ADj+AAw/gAcH8AOA/gPAH8PgAf/gAA/AAA=="
};
return {
width : 26, height : 26, bpp : 1,
transparent : 0,
buffer : E.toArrayBuffer(atob(data[i]))
};
}
// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
// date/time constants and conversions
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
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)
};
}
// adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// 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
};
}
SunCalc.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.
SunCalc.getMoonIllumination = function (date) {
var year = date.getFullYear();
var month = date.getMonth();
var day = date.getDate();
var Moon = {
phases: ['new', 'waxing-crescent', 'first-quarter', 'waxing-gibbous', 'full', 'waning-gibbous', 'last-quarter', 'waning-crescent'],
phase: function (year, month, day) {
let c = 0;
let e = 0;
let jd = 0;
let b = 0;
if (month < 3) {
year--;
month += 12;
}
++month;
c = 365.25 * year;
e = 30.6 * month;
jd = c + e + day - 694039.09; // jd is total days elapsed
jd /= 29.5305882; // divide by the moon cycle
b = parseInt(jd); // int(jd) -> b, take integer part of jd
jd -= b; // subtract integer part to leave fractional part of original jd
b = Math.round(jd * 8); // scale fraction from 0-8 and round
if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0
return {phase: b, name: Moon.phases[b]};
}
};
return (Moon.phase(year, month, day));
};
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
SunCalc.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;
};
function getMPhaseComp (offset) {
var date = new Date();
date.setDate(date.getDate() + offset);
var dd = String(date.getDate());
if(dd<10){dd='0'+dd;}
var mm = String(date.getMonth() + 1);
if(mm<10){mm='0'+mm;}
var yyyy = date.getFullYear();
var phase = SunCalc.getMoonIllumination(date);
return dd + "." + mm + "." + yyyy + ": "+ phase.name;
}
function getMPhaseSim (offset) {
var date = new Date();
date.setDate(date.getDate() + offset);
var dd = String(date.getDate());
if(dd<10){dd='0'+dd;}
var mm = String(date.getMonth() + 1);
if(mm<10){mm='0'+mm;}
var yyyy = date.getFullYear();
var phase = SunCalc.getMoonIllumination(date);
return phase.name;
}
function drawMoonPhase(offset, x, y){
if (coords.lat >= 0 && coords.lat <= 90){ //Northern hemisphere
if (getMPhaseSim(offset) == "new") {g.drawImage(getImg("NewMoon"), x, y);}
if (getMPhaseSim(offset) == "waxing-crescent") {g.drawImage(getImg("WaxingCrescentNorth"), x, y);}
if (getMPhaseSim(offset) == "first-quarter") {g.drawImage(getImg("FirstQuarterNorth"), x, y);}
if (getMPhaseSim(offset) == "waxing-gibbous") {g.drawImage(getImg("WaxingGibbousNorth"), x, y);}
if (getMPhaseSim(offset) == "full") {g.drawImage(getImg("FullMoon"), x, y);}
if (getMPhaseSim(offset) == "waning-gibbous") {g.drawImage(getImg("WaningGibbousNorth"), x, y);}
if (getMPhaseSim(offset) == "last-quarter") {g.drawImage(getImg("LastQuarterNorth"), x, y);}
if (getMPhaseSim(offset) == "waning-crescent") {g.drawImage(getImg("WaningCrescentNorth"), x, y);}
}
else { //Southern hemisphere
if (getMPhaseSim(offset) == "new") {g.drawImage(getImg("NewMoon"), x, y);}
if (getMPhaseSim(offset) == "waxing-crescent") {g.drawImage(getImg("WaxingCrescentSouth"), x, y);}
if (getMPhaseSim(offset) == "first-quarter") {g.drawImage(getImg("FirstQuarterSouth"), x, y);}
if (getMPhaseSim(offset) == "waxing-gibbous") {g.drawImage(getImg("WaxingGibbousSouth"), x, y);}
if (getMPhaseSim(offset) == "full") {g.drawImage(getImg("FullMoon"), x, y);}
if (getMPhaseSim(offset) == "waning-gibbous") {g.drawImage(getImg("WaningGibbousSouth"), x, y);}
if (getMPhaseSim(offset) == "last-quarter") {g.drawImage(getImg("LastQuarterSouth"), x, y);}
if (getMPhaseSim(offset) == "waning-crescent") {g.drawImage(getImg("WaningCrescentSouth"), x, y);}
}
}
function drawMoon(offset, x, y) {
g.setFont("6x8");
g.clear();
g.drawString("Key1: day+, Key2:today, Key3:day-",x,y-30);
g.drawString("Last known coordinates: " + coords.lat.toFixed(4) + " " + coords.lon.toFixed(4), x, y-20);
g.drawString("Press BTN4 to update",x, y-10);
g.drawString(getMPhaseComp(offset),x,y+30);
drawMoonPhase(offset, x+35, y+40);
g.drawString(getMPhaseComp(offset+2),x,y+70);
drawMoonPhase(offset+2, x+35, y+80);
g.drawString(getMPhaseComp(offset+4),x,y+110);
drawMoonPhase(offset+4, x+35, y+120);
g.drawString(getMPhaseComp(offset+6),x,y+150);
drawMoonPhase(offset+6, x+35, y+160);
}
//Write coordinates to file
function updateCoords() {
storage.write('coords.json', coords);
}
//set coordinates to default (city where I live)
function resetCoords() {
coords = {
lat : 52.96236,
lon : 7.62571,
};
updateCoords();
}
function getGpsFix() {
Bangle.on('GPS', function(fix) {
g.clear();
if (fix.fix == 1) {
var gpsString = "lat: " + fix.lat.toFixed(4) + " lon: " + fix.lon.toFixed(4);
coords.lat = fix.lat;
coords.lon = fix.lon;
updateCoords();
g.drawString("Got GPS fix and wrote coords to file",10,20);
g.drawString(gpsString,10,30);
g.drawString("Press BTN5 to return to app",10,40);
clearInterval(timer);
timer = undefined;
}
else {
g.drawString("Searching satellites...",10,20);
g.drawString("Press BTN5 to stop GPS",10, 30);
}
});
}
function start() {
var x = 10;
var y = 50;
var offsetMoon = 0;
coords = storage.readJSON('coords.json',1); //read coordinates from file
if (!coords) resetCoords(); //if coordinates could not be read, reset them
drawMoon(offsetMoon, x, y); //offset, x, y
//define button functions
setWatch(function() { //BTN1
offsetMoon++; //jump to next day
drawMoon(offsetMoon, x, y); //offset, x, y
}, BTN1, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN2
offsetMoon = 0; //jump to today
drawMoon(offsetMoon, x, y); //offset, x, y
}, BTN2, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN3
offsetMoon--; //jump to next day
drawMoon(offsetMoon, x, y); //offset, x, y
}, BTN3, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN4
g.drawString("--- Getting GPS signal ---",x, y);
Bangle.setGPSPower(1);
timer = setInterval(getGpsFix, 10000);
}, BTN4, {edge:"rising", debounce:50, repeat:true});
setWatch(function() { //BTN5
if (timer) clearInterval(timer);
timer = undefined;
Bangle.setGPSPower(0);
drawMoon(offsetMoon, x, y); //offset, x, y
}, BTN5, {edge:"rising", debounce:50, repeat:true});
}
start();

BIN
apps/moonphase/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

2
apps/pipboy/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New Watch!
0.02: Changed colors for better readability and added current date

View File

@ -29,17 +29,23 @@ function topLine() {
function bottomLine() {
g.setColor(darkGreen);
g.fillRect(5, 175, 60, 185);
g.fillRect(67, 175, 140, 185);
//first line
g.setColor(darkerGreen);
g.fillRect(5, 175, 100, 185); //DATE
g.fillRect(105, 175, 160, 185);//STIM
g.fillRect(166, 175, 239, 185); // RADAWAY
g.setColor(green);
g.setFont("6x8", tinyFont);
g.drawString("DATE", 20, 177);
g.drawString("STIM (3)", 135, 177);
g.drawString("RADAWAY (8)", 205, 177);
//second line
g.setColor(darkerGreen);
g.fillRect(5, 190, 70, 200);
g.fillRect(75, 190, 239, 200);
g.setFont("6x8", tinyFont);
g.drawString("STIM (0)", 32, 177);
g.drawString("RADAWAY (0)", 105, 177);
g.setColor(green);
g.drawString("HP 115/115", 38, 192);
g.drawString("LEVEL 6", 100, 192);
@ -55,7 +61,15 @@ function drawClock() {
var t = new Date();
var h = t.getHours();
var m = t.getMinutes();
var dd = t.getDate();
var mm = t.getMonth()+1; //month is zero-based
var yy = t.getFullYear();
var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2);
//create date string
if (dd.toString().length < 2) dd = '0' + dd;
if (mm.toString().length < 2) mm = '0' + mm;
var date = dd + "." + mm + "." + yy;
g.setFont("6x8",bigFont);
g.setColor(green);
@ -63,6 +77,10 @@ function drawClock() {
g.clearRect(0, 110, 150, 140);
g.drawString(time, 70, 110);
//draw date
g.setFont("6x8", tinyFont);
g.drawString(date, 67, 177);
}
function drawAll() {
@ -83,4 +101,3 @@ setInterval(drawClock, 1E4);
drawAll();
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});

1
apps/rpgdice/ChangeLog Executable file
View File

@ -0,0 +1 @@
0.01: First release

1
apps/rpgdice/app-icon.js Executable file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgMJgMQgMZzOREaERiERzIACiIVOAIIUCz///ORgIXNIQIAC/4ABJJYsBCogYEAYMQiAWGLAoAJJI8JLAYoCAAgJBJIIwGBohxBJI4YBJIwOFC4w5EC4hdOzIgCyLFDC45hHAAZJDgJAKMQwyBSYSOBxIXGPRTdChOfxChHbpRhBC4P5GAgAOgEZFAKjIBAz1EC5YYJxAvBJ4IXQzGIxEQB4RbPCoOIwEAOKAsCC4QvCFiAXDdwwsMC5eebogVGAALWBC42f/AWLC4zwCUgIEBCxK+DE4bsFC5+f/IrBC4RzHXwkZzATEDgP/RZAXFz5ECf4oXMCYKICC6hABMAQXOgAXBLgLrHRxZfCC6sBCo4XLLwIXBbAgXRMIQAGRxgwChIXVgEQIYimOGAZ6CSgOJC6CrCC4TZBC6IwCC4QWQPQYXKOggAFPQOfC5AWKPQgXGCpR6FOwoWOPQQXDIZYwHC4QVRAAQXBBxgA="))

86
apps/rpgdice/app.js Executable file
View File

@ -0,0 +1,86 @@
const dice = [4, 6, 8, 10, 12, 20, 100];
const nFlips = 20;
const delay = 500;
let dieIndex = 1;
let face = 0;
let rolling = false;
let bgColor;
let fgColor;
function getDie() {
return dice[dieIndex];
}
function setColors(lastBounce) {
if (lastBounce) {
bgColor = 0xFFFF;
fgColor = 0x0000;
} else {
bgColor = 0x0000
fgColor = 0xFFFF;
}
}
function flipFace() {
while(true) {
let newFace = Math.floor(Math.random() * getDie()) + 1;
if (newFace !== face) {
face = newFace;
break;
}
}
}
function draw() {
g.setColor(bgColor);
g.fillRect(0, 0, g.getWidth(), g.getHeight());
g.setColor(fgColor);
g.setFontAlign(0, 0);
g.setFontVector(40);
g.drawString('d' + getDie(), 180, 30);
g.setFontVector(100);
g.drawString(face, 120, 120);
}
function roll(bounces) {
flipFace();
setColors(bounces === 0);
draw();
if (bounces > 0) {
setTimeout(() => roll(bounces - 1), delay / bounces);
} else {
rolling = false;
}
}
function startRolling() {
if (rolling) return;
rolling = true;
roll(nFlips);
}
function changeDie() {
if (rolling) return;
dieIndex = (dieIndex + 1) % dice.length;
draw();
}
Bangle.on('lcdPower',function(on) {
if (on) {
startRolling();
}
});
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
startRolling();
// Top button rolls the die, bottom button changes it
setWatch(startRolling, BTN1, {repeat:true});
setWatch(changeDie, BTN3, {repeat:true});
// Show launcher when middle button pressed
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});

BIN
apps/rpgdice/rpgdice.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -4,3 +4,4 @@
0.05: Fix Settings json
0.06: Remove distance setting as there's a separate app for Locale now
0.07: Added vibrate as beep workaround
0.08: Add support for app/widget settings

View File

@ -114,6 +114,7 @@ function showMainMenu() {
}
},
'Set Time': showSetTimeMenu,
'App/widget settings': showAppSettingsMenu,
'Reset Settings': showResetMenu,
'Turn Off': Bangle.off,
'< Back': ()=> {load();}
@ -295,4 +296,48 @@ function showSetTimeMenu() {
return E.showMenu(timemenu);
}
function showAppSettingsMenu(){
let appmenu = {
'': {'title': 'App Settings'},
'< Back': showMainMenu,
}
const apps = storage.list(/\.info$/)
.map(app => storage.readJSON(app, 1))
.filter(app => app && app.settings)
.sort((a, b) => a.sortorder - b.sortorder)
if (apps.length === 0) {
appmenu['No app has settings'] = () => {};
}
apps.forEach(function (app) {
appmenu[app.name] = () => {showAppSettings(app)};
})
E.showMenu(appmenu)
}
function showAppSettings(app) {
const showError = msg => {
E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`);
setWatch(showAppSettingsMenu, BTN1, { repeat: false });
}
let appSettings = storage.read(app.settings);
if (!appSettings) {
return showError('Missing settings');
}
try {
appSettings = eval(appSettings);
} catch (e) {
console.log(`${app.name} settings error:`, e)
return showError('Error in settings');
}
if (typeof appSettings !== "function") {
return showError('Invalid settings');
}
try {
// pass showAppSettingsMenu as "back" argument
appSettings(showAppSettingsMenu);
} catch (e) {
console.log(`${app.name} settings error:`, e)
return showError('Error in settings');
}
}
showMainMenu();

7
apps/swatch/ChangeLog Normal file
View File

@ -0,0 +1,7 @@
0.01: Original App
0.02: Lap log now counts up from 1
Lap log now scrolls into 2nd column after 18th entry, able to display 36 entries before going off screen
0.03: Added ability to save Lap log as a date named JSON file into memory
Fixed bug from 0.01 where BN1 (reset) could clear the lap log when timer is running
0.04: Changed save file filename, add interface.html to allow laps to be loaded
0.05: Added widgets

View File

@ -0,0 +1,90 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="records"></div>
<script src="../../lib/interface.js"></script>
<script>
var domRecords = document.getElementById("records");
function getLapTimes() {
Util.showModal("Loading Lap Times...");
domRecords.innerHTML = "";
Puck.eval('require("Storage").list(/^swatch.*\.json/).map(fn=>({n:fn,d:require("Storage").readJSON(fn,1)}))',lapData=>{
var html = `<div class="container">
<div class="columns">\n`;
lapData.forEach((lap,lapIndex) => {
lap.date = lap.n.substr(7,16).replace("_"," ");
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">${lap.date}</div>
<div class="card-subtitle text-gray">${lap.d.length} Laps</div>
</div>
<div class="card-body">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>lap</th>
<th>time</th>
</tr>
</thead>
<tbody>
${ lap.d.map((d,n)=>`<tr><td>${n+1}</td><td>${d}</td></tr>`).join("\n") }
</tbody>
</table>
</div>
<div class="card-footer">
<button class="btn btn-primary" idx="${lapIndex}" task="download">Download</button>
<button class="btn btn-default" idx="${lapIndex}" task="delete">Delete</button>
</div>
</div>
`;
});
if (lapData.length==0) {
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">No record</div>
<div class="card-subtitle text-gray">No laps recorded</div>
</div>
</div>
`;
}
html += `
</div>
</div>`;
domRecords.innerHTML = html;
Util.hideModal();
var buttons = domRecords.querySelectorAll("button");
for (var i=0;i<buttons.length;i++) {
buttons[i].addEventListener("click",event => {
var button = event.currentTarget;
var lapIndex = parseInt(button.getAttribute("idx"));
var lap = lapData[lapIndex];
if (!lap) throw new Error("Invalid index!");
var task = button.getAttribute("task");
if (task=="delete") {
Util.showModal("Deleting lap time...");
Util.eraseStorage(lap.n,()=>{
Util.hideModal();
getLapTimes();
});
}
if (task=="download") {
Util.saveCSV(lap.n.slice(0,-5)+".csv", lap.d.map((d,n)=>[n+1,d].join(",")).join("\n"));
}
});
}
})
}
function onInit() {
getLapTimes();
}
</script>
</body>
</html>

View File

@ -13,21 +13,26 @@ function timeToText(t) {
return mins+":"+("0"+secs).substr(-2)+"."+("0"+hs).substr(-2);
}
function updateLabels() {
g.clear();
g.reset(1);
g.clearRect(0,23,g.getWidth()-1,g.getHeight()-24);
g.setFont("6x8",2);
g.setFontAlign(0,0,3);
g.drawString(started?"STOP":"GO",230,120);
if (!started) g.drawString("RESET",230,50);
g.drawString("LAP",230,190);
if (!started) g.drawString("RESET",230,180);
g.drawString(started?"LAP":"SAVE",230,50);
g.setFont("6x8",1);
g.setFontAlign(-1,-1);
for (var i in lapTimes) {
g.drawString(i+": "+timeToText(lapTimes[i]),10,timeY + 30 + i*8);
if (i<16)
{g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),35,timeY + 30 + i*8);}
else if (i<32)
{g.drawString(lapTimes.length-i+": "+timeToText(lapTimes[i]),125,timeY + 30 + (i-16)*8);}
}
drawsecs();
}
function drawsecs() {
var t = tCurrent-tStart;
g.reset(1);
g.setFont("Vector",48);
g.setFontAlign(0,0);
var secs = Math.floor(t/1000)%60;
@ -47,6 +52,9 @@ function drawms() {
g.clearRect(hsXPos,timeY,220,timeY+20);
g.drawString("."+("0"+hs).substr(-2),hsXPos,timeY+10);
}
function getLapTimesArray() {
return lapTimes.map(timeToText).reverse();
}
setWatch(function() { // Start/stop
started = !started;
@ -69,20 +77,33 @@ setWatch(function() { // Start/stop
drawms();
}, 20);
}, BTN2, {repeat:true});
setWatch(function() { // Reset
Bangle.beep();
if (!started) {
tStart = tCurrent = Date.now();
}
lapTimes = [];
updateLabels();
}, BTN1, {repeat:true});
setWatch(function() { // Lap
Bangle.beep();
if (started) tCurrent = Date.now();
lapTimes.unshift(tCurrent-tStart);
if (started) {
tCurrent = Date.now();
lapTimes.unshift(tCurrent-tStart);
}
tStart = tCurrent;
if (!started) { // save
var timenow= Date();
var filename = "swatch-"+(new Date()).toISOString().substr(0,16).replace("T","_")+".json";
// this maxes out the 28 char maximum
require("Storage").writeJSON(filename, getLapTimesArray());
E.showMessage("Laps Saved","Stopwatch");
setTimeout(updateLabels, 1000);
} else {
updateLabels();
}
}, BTN1, {repeat:true});
setWatch(function() { // Reset
if (!started) {
Bangle.beep();
tStart = tCurrent = Date.now();
lapTimes = [];
}
updateLabels();
}, BTN3, {repeat:true});
updateLabels();
Bangle.loadWidgets();
Bangle.drawWidgets();

2
apps/toucher/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Add swipe support and doucle tap to run application

130
apps/toucher/app.js Normal file
View File

@ -0,0 +1,130 @@
g.clear();
const Storage = require("Storage");
function getApps(){
return Storage.list(/\.info$/).filter(app => app.endsWith('.info')).map(app => Storage.readJSON(app,1) || { name: "DEAD: "+app.substr(1) })
.filter(app=>app.type=="app" || app.type=="clock" || !app.type)
.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
if (a.name<b.name) return -1;
if (a.name>b.name) return 1;
return 0;
});
}
const selected = 0;
const apps = getApps();
function prev(){
if (selected>=0) {
selected--;
}
drawMenu();
}
function next() {
if (selected+1<apps.length) {
selected++;
}
drawMenu();
}
function run() {
if(selected < 0) return load();
if (!apps[selected].src) return;
if (Storage.read(apps[selected].src)===undefined) {
E.showMessage("App Source\nNot found");
setTimeout(drawMenu, 2000);
} else {
E.showMessage("Loading...");
load(apps[selected].src);
}
}
function getCurrentApp(){
return apps[selected];
}
function getNextApp(){
return apps[selected+1];
}
function drawFallbackIcon(){
g.setColor(1,1,1);
g.fillRect(72, 40, 168, 136);
g.setColor(0,0,0);
g.setFont('6x8', 8);
g.drawString('?', 124, 88);
}
function drawArrow(x, y, size, dir){
size = size || 10;
dir = dir || 1;
g.moveTo(x, y).lineTo(x+(size*dir), y-size).lineTo(x+(size*dir),y+size).lineTo(x, y);
}
function drawMenu(){
if(selected < 0){
g.clear();
g.setFontAlign(0,0);
g.setFont('6x8', 2);
g.drawString('Back', 120, 120);
drawArrow(220, 120, 10, -1);
return;
}
const app = getCurrentApp();
g.clear();
g.setFontAlign(0,0);
g.setFont('6x8', 2);
if(!app) return g.drawString('???', 120, 120);
g.drawString(app.name, 120, 160);
if (app.icon) icon = Storage.read(app.icon);
if (icon) try {g.drawImage(icon, 120-48, 40, { scale: 2 });} catch(e){ drawFallbackIcon(); }
else drawFallbackIcon();
g.setFont('6x8', 1);
const type = app.type ? app.type : 'App';
const version = app.version ? app.version : '0.00';
const info = type+' v'+version;
g.setFontAlign(-1,1);
g.drawString(info, 20, 220);
const count = (selected+1)+'/'+apps.length;
g.setFontAlign(1,1);
g.drawString(count, 220, 220);
drawArrow(20, 120, 10, 1);
if(getNextApp()) drawArrow(220, 120, 10, -1);
}
drawMenu();
// Physical buttons
setWatch(prev, BTN1, {repeat:true});
setWatch(next, BTN3, {repeat:true});
setWatch(run, BTN2, {repeat:true,edge:"falling"});
// Screen event
Bangle.on('touch', function(button){
switch(button){
case 1:
prev();
break;
case 2:
next();
break;
case 3:
run();
break;
}
});
Bangle.on('swipe', dir => {
if(dir == 1) prev();
else next();
});

BIN
apps/toucher/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

5
apps/widbatpc/ChangeLog Normal file
View File

@ -0,0 +1,5 @@
0.02: Now refresh battery monitor every minute if LCD on
0.03: Tweaks for variable size widget system
0.04: Ensure redrawing works with variable size widget system
0.05: Change color depending on battery level, cloned from widbat
0.06: Show battery percentage as text

59
apps/widbatpc/widget.js Normal file
View File

@ -0,0 +1,59 @@
(function(){
const levelColor = (l) => {
if (Bangle.isCharging()) return 0x07E0; // "Green"
if (l >= 50) return 0x05E0; // slightly darker green
if (l >= 15) return 0xFD20; // "Orange"
return 0xF800; // "Red"
}
function setWidth() {
WIDGETS["bat"].width = 40 + (Bangle.isCharging()?16:0);
}
function draw() {
var s = 39;
var x = this.x, y = this.y;
const l = E.getBattery(), c = levelColor(l);
if (Bangle.isCharging()) {
g.setColor(c).drawImage(atob(
"DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
x+=16;
}
g.setColor(-1);
g.fillRect(x,y+2,x+s-4,y+21);
g.clearRect(x+2,y+4,x+s-6,y+19);
g.fillRect(x+s-3,y+10,x+s,y+14);
g.setColor(c).fillRect(x+4,y+6,x+4+l*(s-12)/100,y+17);
g.setColor(-1);
g.setFontAlign(-1,-1);
if (l >= 100) {
g.setFont('4x6', 2);
g.drawString(l, x + 6, y + 7);
} else {
if (l < 10) x+=6;
g.setFont('6x8', 2);
g.drawString(l, x + 6, y + 4);
}
}
Bangle.on('charging',function(charging) {
if(charging) Bangle.buzz();
setWidth();
Bangle.drawWidgets(); // relayout widgets
g.flip();
});
var batteryInterval;
Bangle.on('lcdPower', function(on) {
if (on) {
WIDGETS["bat"].draw();
// refresh once a minute if LCD on
if (!batteryInterval)
batteryInterval = setInterval(draw, 60000);
} else {
if (batteryInterval) {
clearInterval(batteryInterval);
batteryInterval = undefined;
}
}
});
WIDGETS["bat"]={area:"tr",width:40,draw:draw};
setWidth();
})()

BIN
apps/widbatpc/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

1
apps/widhwt/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New Widget!

23
apps/widhwt/widget.js Normal file
View File

@ -0,0 +1,23 @@
/* jshint esversion: 6 */
(() => {
var icon = require("heatshrink").decompress(atob("jEYwIKHgwCBhwCBh4CEggPCkACBmAXDBwVZ+EB+F4gEsjl8EgMP+EChk/gEMh+ehkA+YIBxwxBnF/4HggH/wEAj0AA=="));
var color = 0x4A69;
function draw() {
g.reset().setColor(color).drawImage(icon, this.x + 1, 0);
}
WIDGETS["widhwt"] = { area: "tr", width: 26, draw: draw };
Bangle.on('swipe', function() {
color = 0x41f;
Bangle.buzz();
Bangle.drawWidgets();
setTimeout(() => {
color = 0x4A69;
Bangle.buzz(1E3, 1);
Bangle.drawWidgets();
}, 35E3);
});
})();

BIN
apps/widhwt/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

1
apps/widmp/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

33
apps/widmp/widget.js Normal file
View File

@ -0,0 +1,33 @@
/* jshint esversion: 6 */
(() => {
const BLACK = 0, MOON = 0x41f, MC = 29.5305882, NM = 694039.09;
var r = 12, mx = 0, my = 0;
var moon = {
0: () => { g.reset().setColor(BLACK).fillRect(mx - r, my - r, mx + r, my + r);},
1: () => { moon[0](); g.setColor(MOON).drawCircle(mx, my, r);},
2: () => { moon[3](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
3: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx - r, my - r, mx, my + r);},
4: () => { moon[3](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
5: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r);},
6: () => { moon[7](); g.setColor(MOON).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);},
7: () => { moon[0](); g.setColor(MOON).fillCircle(mx, my, r).setColor(BLACK).fillRect(mx, my - r, mx + r + r, my + r);},
8: () => { moon[7](); g.setColor(BLACK).fillEllipse(mx - r / 2, my - r, mx + r / 2, my + r);}
};
function moonPhase(d) {
var tmp, month = d.getMonth(), year = d.getFullYear(), day = d.getDate();
if (month < 3) {year--; month += 12;}
tmp = ((365.25 * year + 30.6 * ++month + day - NM) / MC);
return Math.round(((tmp - (tmp | 0)) * 7)+1);
}
function draw() {
mx = this.x; my = this.y + 12;
moon[moonPhase(Date())]();
}
WIDGETS["widmoon"] = { area: "tr", width: 24, draw: draw };
})();

BIN
apps/widmp/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

1
apps/widtbat/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New Widget!

18
apps/widtbat/widget.js Normal file
View File

@ -0,0 +1,18 @@
/* jshint esversion: 6 */
(() => {
const CBS = 0x41f, CBC = 0x07E0;
var batS = require("heatshrink").decompress(atob("j0TwIHEv///kD////EfAYPwuEAgPB4EAg/HCgMfzgDBvwOC/IOC84ONDoUcFgc/AYOAHYRDE"));
var xo = 6, xl = 22, yo = 9, h = 17;
function draw() {
g.reset().setColor(CBS).drawImage(batS, this.x + 1, this.y + 4);
g.setColor(0).fillRect(this.x + xo, this.y + yo, this.x + xl, this.y + h);
var cbc = (Bangle.isCharging()) ? CBC : CBS;
g.setColor(cbc).fillRect(this.x + xo, this.y + yo, this.x + (xl - xo) / 100 * E.getBattery() + xo, this.y + h);
}
Bangle.on('charging', function(charging) {
if (charging) Bangle.buzz();
Bangle.drawWidgets();
});
WIDGETS["widtbat"] = { area:"tr", width:32, draw: draw };
})();

BIN
apps/widtbat/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

1
apps/widver/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New Widget

11
apps/widver/widget.js Normal file
View File

@ -0,0 +1,11 @@
/* jshint esversion: 6 */
(() => {
var width = 28,
ver = process.env.VERSION.split('.');
function draw() {
g.reset().setColor(0, 0.5, 1).setFont("6x8", 1);
g.drawString(ver[0], this.x + 2, this.y + 4, true);
g.setFontAlign(0, -1, 0).drawString(ver[1], this.x + width / 2, this.y + 14, true);
}
WIDGETS["version"] = { area: "tr", width: width, draw: draw };
})();

BIN
apps/widver/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Some files were not shown because too many files have changed in this diff Show More