mirror of https://github.com/espruino/BangleApps
Merge branch 'master' of github.com:espruino/BangleApps
commit
ed7319e74b
|
@ -1 +1 @@
|
|||
theme: jekyll-theme-minimal
|
||||
theme: jekyll-theme-slate
|
|
@ -24,3 +24,4 @@
|
|||
0.23: Fix regression with Days of Week (#1735)
|
||||
0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow
|
||||
Add "Enable All", "Disable All" and "Remove All" actions
|
||||
0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu
|
||||
|
|
|
@ -47,8 +47,7 @@ function showMainMenu() {
|
|||
menu[txt] = {
|
||||
value : "\0"+atob(alarm.on?"EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g":"EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g"),
|
||||
onchange : function() {
|
||||
if (alarm.timer) editTimer(idx, alarm);
|
||||
else editAlarm(idx, alarm);
|
||||
setTimeout(alarm.timer ? editTimer : editAlarm, 10, idx, alarm);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -116,7 +115,7 @@ function editAlarm(alarmIndex, alarm) {
|
|||
},
|
||||
/*LANG*/'Days': {
|
||||
value: "SMTWTFS".split("").map((d,n)=>a.dow&(1<<n)?d:".").join(""),
|
||||
onchange: () => editDOW(a.dow, d => {
|
||||
onchange: () => setTimeout(editDOW, 100, a.dow, d => {
|
||||
a.dow = d;
|
||||
a.t = require("sched").encodeTime(t);
|
||||
editAlarm(alarmIndex, a);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "alarm",
|
||||
"name": "Alarms & Timers",
|
||||
"shortName": "Alarms",
|
||||
"version": "0.24",
|
||||
"version": "0.25",
|
||||
"description": "Set alarms and timers on your Bangle",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,alarm,widget",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcCkGSpEgwQCChICFkgCBgkQoMEyFJAoICByVBkgLBkkSpIaDEwWShEkFgcIBAIdCEYQCBAoQdBAoYsBC4Q7BpICBEYQCDF4Q7CEYYCCEYUSKYYUDyRlCJQQIBNYYvBMoQCBkgjBFgxxCL4REDFgaPEHYgmCIgosCNYZEEDoZ0CNwY7CIIYgDEYtB9+e/dg/4AB2EJkYEB/mC/fn33Ivvz598v4MB/0BgoRCyVHvmW7Mg2EA8uD/EAh/IkGP/8AgVLtkA5El+FJvoRBgmf4Mkh0HkEQo9kyEfkeQofsgf4kmPCIP+h/gwULkkCncEu/ZsmRI4cEv0H8ESpdgEwMjwXI9kTCIOANYkSEYOCncF+UAjuR/ED+FBg/3/f8RgNgiVPkYdBtkT/Egv0Il+AoMfI4PgyX7vkW799F4Nl//4//woH/+0Ztvx7Fs335sk//5EB/IRBhACB77CBpEkgEIgGQoDRBgEggVBgDdBgGAgPv317ku+5cj334t+OSoI+B8gCBtlx7dkuFfgvx4N8yPbvgOB8ACBR4MA9mf4Egz3IgeChEDwDOBx/AjuCoN8y/JgkX4ME2FBjuQn65BgMtwELkGOEYOO4Mh2EJh+Sh/jOIMd+3fskRcwMTEwOWo98gCSBwFJkm2pfgx3II4PBk++/aABhEfwEInpZBvkX7MkJQMl2FHfANBjgCBlmQhHsgwjB33IkeyBAOChMcEwM9+/ZsBHBboMJtv2hd9+FHZANBVoM7kGC/fv2FJ9+GEYOAh//+UIaIMBkkQpEAHwIIBoMgiFJBANJEAMIkGShEkwQIChIIBhIIBhIaCkmQpIFCgmSEwYpDEYwCCpAICBwUEiQdFEwIICyAIDHwQ7CEYYpCEYWSpA7FDocSEwojBCgIaDIgYCBNwR0BNYYjFEwZTDLgQjGOgYvBEYQ7ENYlJFgQCCDohuGTYpBFkhoCSoQICEYIA="))
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,14 @@
|
|||
{ "id": "bradbury",
|
||||
"name": "Bradbury Watch",
|
||||
"shortName":"Bradbury",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version":"0.01",
|
||||
"description": "A watch face based on the classic Seiko model worn by one of my favorite authors. I didn't follow the original lcd layout exactly, opting for larger font for more easily readable time, and adding date, battery level, and step count; read from the device. Tapping the screen toggles visibility of widgets.",
|
||||
"type": "clock",
|
||||
"supports":["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"bradbury.app.js","url":"app.js"},
|
||||
{"name":"bradbury.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
|
@ -6,3 +6,4 @@
|
|||
0.24: Added previews to the customizer.
|
||||
0.25: Fixed a bug that would let widgets change the color of the clock.
|
||||
0.26: Time formatted to locale
|
||||
0.27: Fixed the timing code, which sometimes did not update for one minute
|
||||
|
|
|
@ -7,6 +7,13 @@ if (settings.fontIndex==undefined) {
|
|||
require('Storage').writeJSON("myapp.json", settings);
|
||||
}
|
||||
|
||||
function queueDraw() {
|
||||
setTimeout(function() {
|
||||
draw();
|
||||
queueDraw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
function draw() {
|
||||
var date = new Date();
|
||||
// Draw day of the week
|
||||
|
@ -24,7 +31,5 @@ Bangle.setUI("clock");
|
|||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
queueDraw();
|
||||
draw();
|
||||
setTimeout(function() {
|
||||
setInterval(draw,60000);
|
||||
}, 60000 - Date.now() % 60000);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "contourclock",
|
||||
"name": "Contour Clock",
|
||||
"shortName" : "Contour Clock",
|
||||
"version":"0.26",
|
||||
"version":"0.27",
|
||||
"icon": "app.png",
|
||||
"description": "A Minimalist clockface with large Digits. Now with more fonts!",
|
||||
"screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}],
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Fix true wind computation, add swipe gesture to pause GPS
|
||||
|
|
|
@ -14,7 +14,9 @@ additionally displayed in red. In this mode, the speed over ground in knots is
|
|||
|
||||
## Controls
|
||||
|
||||
There are no controls in the main app, but there are two settings in the settings app that can be changed:
|
||||
In the main app, when true wind mode is enabled (see below), swiping left on the screen will temporarily disable GPS (to preserve battery); a small
|
||||
red satellite symbol will appear on the bottom right. Swiping right will turn GPS back on.
|
||||
The settings app provides the following two settings:
|
||||
|
||||
* True wind: enables or disables true wind calculations; enabling this will turn on GPS inside the app
|
||||
* Mounting angle: mounting relative to the boat of the wind instrument (in degrees)
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
OW_CHAR_UUID = '0000cc91-0000-1000-8000-00805f9b34fb';
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
gatt = {};
|
||||
cx = g.getWidth()/2;
|
||||
cy = 24+(g.getHeight()-24)/2;
|
||||
w = (g.getWidth()-24)/2;
|
||||
|
||||
gps_course = { spd: 0 };
|
||||
var gatt = {};
|
||||
var cx = g.getWidth()/2;
|
||||
var cy = 24+(g.getHeight()-24)/2;
|
||||
var w = (g.getWidth()-24)/2;
|
||||
var y1 = 24;
|
||||
var y2 = g.getHeight()-1;
|
||||
var gps_course = { spd: 0 };
|
||||
var course_marker_len = g.getWidth()/4;
|
||||
|
||||
var settings = require("Storage").readJSON('openwindsettings.json', 1) || {};
|
||||
|
||||
i = 0;
|
||||
hullpoly = [];
|
||||
var pause_gps = false;
|
||||
|
||||
var i = 0;
|
||||
var hullpoly = [];
|
||||
for (y=-1; y<=1; y+=0.1) {
|
||||
hullpoly[i++] = cx - (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3;
|
||||
hullpoly[i++] = cy - y*w*0.7;
|
||||
|
@ -22,21 +26,22 @@ for (y=1; y>=-1; y-=0.1) {
|
|||
|
||||
function wind_updated(ev) {
|
||||
if (ev.target.uuid == "0xcc91") {
|
||||
awa = settings.mount_angle-ev.target.value.getInt16(1, true)*0.1;
|
||||
awa = settings.mount_angle+ev.target.value.getInt16(1, true)*0.1;
|
||||
if (awa<0) awa += 360;
|
||||
aws = ev.target.value.getInt16(3, true)*0.01;
|
||||
// console.log(awa, aws);
|
||||
//console.log(awa, aws);
|
||||
if (gps_course.spd > 0) {
|
||||
wv = { // wind vector (in fixed reference frame)
|
||||
lon: Math.sin(Math.PI*(gps_course.course+awa)/180)*aws,
|
||||
lat: Math.cos(Math.PI*(gps_course.course+awa)/180)*aws
|
||||
wv = { // wind vector (in "earth" reference frame)
|
||||
vlon: Math.sin(Math.PI*(gps_course.course+(awa+180))/180)*aws,
|
||||
vlat: Math.cos(Math.PI*(gps_course.course+(awa+180))/180)*aws
|
||||
};
|
||||
twv = { lon: wv.lon+gps_course.lon, lat: wv.lat+gps_course.lat };
|
||||
tws = Math.sqrt(Math.pow(twv.lon,2)+Math.pow(twv.lat, 2));
|
||||
twa = Math.atan2(twv.lat, twv.lon)*180/Math.PI-gps_course.course;
|
||||
twv = { vlon: wv.vlon+gps_course.vlon, vlat: wv.vlat+gps_course.vlat };
|
||||
tws = Math.sqrt(Math.pow(twv.vlon,2)+Math.pow(twv.vlat, 2));
|
||||
twa = 180+Math.atan2(twv.vlon, twv.vlat)*180/Math.PI-gps_course.course;
|
||||
if (twa<0) twa += 360;
|
||||
if (twa>360) twa -=360;
|
||||
}
|
||||
else {
|
||||
else {
|
||||
tws = -1;
|
||||
twa = 0;
|
||||
}
|
||||
|
@ -57,34 +62,37 @@ function draw_compass(awa, aws, twa, tws) {
|
|||
a = i*Math.PI/2+Math.PI/4;
|
||||
g.drawLineAA(cx+Math.cos(a)*w*0.85, cy+Math.sin(a)*w*0.85, cx+Math.cos(a)*w*0.99, cy+Math.sin(a)*w*0.99);
|
||||
}
|
||||
g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy+Math.cos(Math.PI*awa/180)*w*0.9, w*0.1);
|
||||
g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy-Math.cos(Math.PI*awa/180)*w*0.9, w*0.1);
|
||||
if (tws>0) g.setColor(1, 0, 0).fillCircle(cx+Math.sin(Math.PI*twa/180)*w*0.9, cy+Math.cos(Math.PI*twa/180)*w*0.9, w*0.1);
|
||||
g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06);
|
||||
g.setFontAlign(0, 0, 0).drawString(aws.toFixed(1), cx, cy-0.32*w);
|
||||
if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w);
|
||||
if (settings.truewind && typeof gps_course.spd!=='undefined') {
|
||||
spd = gps_course.spd/1.852;
|
||||
g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1);
|
||||
if (!pause_gps) {
|
||||
if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w);
|
||||
if (settings.truewind && gps_course.spd!=-1) {
|
||||
spd = gps_course.spd/1.852;
|
||||
g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1);
|
||||
}
|
||||
}
|
||||
if (pause_gps) g.setColor("#f00").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),g.getWidth()-15, g.getHeight()-15);
|
||||
}
|
||||
|
||||
function parseDevice(d) {
|
||||
device = d;
|
||||
console.log("Found device");
|
||||
device.gatt.connect().then(function(ga) {
|
||||
console.log("Connected");
|
||||
gatt = ga;
|
||||
return ga.getPrimaryService("cc90");
|
||||
}).then(function(s) {
|
||||
return s.getCharacteristic("cc91");
|
||||
}).then(function(c) {
|
||||
c.on('characteristicvaluechanged', (event)=>wind_updated(event));
|
||||
return c.startNotifications();
|
||||
}).then(function() {
|
||||
console.log("Done!");
|
||||
}).catch(function(e) {
|
||||
console.log("ERROR"+e);
|
||||
});}
|
||||
device.gatt.connect().then(function(ga) {
|
||||
console.log("Connected");
|
||||
gatt = ga;
|
||||
return ga.getPrimaryService("cc90");
|
||||
}).then(function(s) {
|
||||
return s.getCharacteristic("cc91");
|
||||
}).then(function(c) {
|
||||
c.on('characteristicvaluechanged', (event)=>wind_updated(event));
|
||||
return c.startNotifications();
|
||||
}).then(function() {
|
||||
console.log("Done!");
|
||||
}).catch(function(e) {
|
||||
console.log("ERROR"+e);
|
||||
});}
|
||||
|
||||
function connection_setup() {
|
||||
NRF.setScan();
|
||||
|
@ -96,8 +104,10 @@ if (settings.truewind) {
|
|||
Bangle.on('GPS',function(fix) {
|
||||
if (fix.fix && fix.satellites>3 && fix.speed>2) { // only uses fixes w/ more than 3 sats and speed > 2kph
|
||||
gps_course =
|
||||
{ lon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852,
|
||||
lat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852,
|
||||
{ vlon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852,
|
||||
vlat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852,
|
||||
lat: fix.lat,
|
||||
lon: fix.lon,
|
||||
spd: fix.speed,
|
||||
course: fix.course
|
||||
};
|
||||
|
@ -107,6 +117,20 @@ if (settings.truewind) {
|
|||
Bangle.setGPSPower(1, "app");
|
||||
}
|
||||
|
||||
if (settings.truewind) {
|
||||
Bangle.on("swipe", (d)=>{
|
||||
if (d==-1 && !pause_gps) {
|
||||
pause_gps = true;
|
||||
Bangle.setGPSPower(0);
|
||||
draw_compass(0, 0, 0, 0);
|
||||
}
|
||||
else if (d==1 && pause_gps) {
|
||||
pause_gps = false;
|
||||
Bangle.setGPSPower(1, "app");
|
||||
draw_compass(0, 0, 0, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
draw_compass(0, 0, 0, 0);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "openwind",
|
||||
"name": "OpenWind",
|
||||
"shortName":"OpenWind",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "OpenWind",
|
||||
"icon": "openwind.png",
|
||||
"readme": "README.md",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: Initial version
|
||||
0.02: Moved settings from launcher to settings->apps menu
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
require("heatshrink").decompress(atob("kMigILIgPAAYMD/ADBwcGhkAwM5wcA/+2//Av/Rn/giFoyFggkUrFggEKlAkCiApCx+AAYNGoADBkU4AYMQj4DBvEICANkAoIPBgE2B4MAiMAH4MAwECAYNALYUgBIISCHYMYAoQWBAIMEgAYBAIMBwEDDQNgDwUf/4eBg4DCAA4"))
|
|
@ -1,14 +1,15 @@
|
|||
{ "id": "quicklaunch",
|
||||
{
|
||||
"id": "quicklaunch",
|
||||
"name": "Quick Launch",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice.",
|
||||
"version":"0.02",
|
||||
"description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.",
|
||||
"type": "bootloader",
|
||||
"tags": "tools, system",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"quicklaunch.app.js","url":"app.js"},
|
||||
{"name":"quicklaunch.boot.js","url":"boot.js"},
|
||||
{"name":"quicklaunch.img","url":"app-icon.js","evaluate":true}
|
||||
{"name":"quicklaunch.settings.js","url":"settings.js"},
|
||||
{"name":"quicklaunch.boot.js","url":"boot.js"}
|
||||
],
|
||||
"data": [{"name":"quicklaunch.json"}]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
(function(back) {
|
||||
var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
|
||||
|
||||
var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type));
|
||||
|
@ -118,3 +119,4 @@ apps.forEach((a)=>{
|
|||
});
|
||||
|
||||
showMainMenu();
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,18 @@
|
|||
# SciCalc
|
||||
|
||||
Simple scientific calculator. I needed one, so I wrote a basic one, no design frills. Input expressions are slightly post processed and then evaluated
|
||||
by the JS interpreter.
|
||||
|
||||
## Usage
|
||||
|
||||
Buttons are arranged on 3 separate screens, swiping left or right switches between them. Swiping down has the same effect as hitting the "=" button.
|
||||
|
||||
## Features
|
||||
|
||||
The calculator supports the following operations:
|
||||
|
||||
* basic arithmetic: +, -, *, /, ^ (raise to a power), +/- (invert sign), 1/x (inverse), use of parentheses
|
||||
* trigonometric fucntions: sin, cos, tan, asin, acos, atan
|
||||
* exponential exp, natural logarithm log, pow function (this one takes 2 comma separated arguments)
|
||||
* Pi is provided as a constant
|
||||
* a memory button "M" stores or recalls the last result (after hitting the "=" button or swiping down)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AJioAaF1wwSFzowRCQUZo4AWjIvVFy4ABF/4vXyGQAYov/R+sZFy8ZF6oAcF/4vvi4AeF/4SCjseAAMdAx8MAAYvVEAQABAx4v/R/TvvF96PUg8cAAMHd9QuCAAIv/R+rvvF96Pvd94vvR97vvF96Pvd94vvR97vsGDwuQGDouSAH4A/AGwA=="))
|
|
@ -0,0 +1,113 @@
|
|||
const W = g.getWidth();
|
||||
const H = g.getHeight();
|
||||
|
||||
const dispH = H/5;
|
||||
const butH = H-dispH;
|
||||
|
||||
const buttons = [[['7', '8', '9'],
|
||||
['4', '5', '6'],
|
||||
['1', '2', '3'],
|
||||
['E', '0', '.']],
|
||||
[['<', 'M', 'C'],
|
||||
['+', '-', '*'],
|
||||
['/', '(', ')'],
|
||||
['^', ',', '=']],
|
||||
[['Sin', 'Cos', 'Tan'],
|
||||
['Asi', 'Aco', 'Ata'],
|
||||
['Pi', '1/x', '+/-'],
|
||||
['Log', 'Exp', 'Pow']
|
||||
]];
|
||||
|
||||
var curPage = 0;
|
||||
var inputStr = '';
|
||||
var memory = '';
|
||||
var qResult = false;
|
||||
|
||||
function drawPage (p) {
|
||||
g.clearRect(0, dispH, W-1, H-1);
|
||||
g.setFont('Vector', butH/5).setFontAlign(0, 0, 0).setColor(g.theme.fg);
|
||||
for (x=0; x<3; ++x)
|
||||
for (y=0; y<4; ++y)
|
||||
g.drawString(buttons[p][y][x], (x+0.5)*W/3, dispH+(y+0.7)*butH/4);
|
||||
g.setColor(0.5, 0.5, 0.5);
|
||||
for (x=1; x<3; ++x) g.drawLine(x*W/3, dispH+0.2*butH/4-2, x*W/3, H-1);
|
||||
for (y=1; y<4; ++y) g.drawLine(0, dispH+(y+0.2)*butH/4, W-1, dispH+(y+0.2)*butH/4);
|
||||
g.setColor(g.theme.fg).drawLine(0, dispH+0.2*butH/4-2, W-1, dispH+0.2*butH/4-2);
|
||||
}
|
||||
|
||||
function updateDisp(s, len) {
|
||||
var fh = butH/5;
|
||||
if (s.toString().length>len) s = s.toString().substr(0,len);
|
||||
g.setFont("Vector", butH/5).setColor(g.theme.fg).setFontAlign(1, 0, 0);
|
||||
while (g.stringWidth(s) > W-1) {
|
||||
fh /= 1.05;
|
||||
g.setFont("Vector", fh);
|
||||
}
|
||||
g.clearRect(0, 0, W-1, dispH-1).drawString(s, W-2, dispH/2);
|
||||
g.setColor(g.theme.fg).drawLine(0, dispH+0.2*butH/4-2, W-1, dispH+0.2*butH/4-2);
|
||||
}
|
||||
|
||||
function processInp (s) {
|
||||
var idx = s.indexOf("^");
|
||||
if (idx > 0) s = "Math.pow(" + s.slice(0,idx) + "," + s.slice(idx+1, s.length) + ")";
|
||||
['Sin', 'Cos', 'Tan', 'Asin', 'Acos', 'Atan', 'Log', 'Exp', 'Pow'].forEach((x) => {
|
||||
var i = s.indexOf(x);
|
||||
while (i>-1) {
|
||||
s = s.slice(0,i)+"Math."+s.slice(i,i+1).toLowerCase()+s.slice(i+1, s.length);
|
||||
i = s.indexOf(x, i+6);
|
||||
}
|
||||
});
|
||||
idx = s.indexOf('Pi');
|
||||
if (idx>-1) s = s.slice(0,idx) + "Math.PI" + s.slice(idx+2, s.length);
|
||||
idx = 0;
|
||||
s.split('').forEach((x)=>{ if (x=='(') idx++; if (x==')') idx-- });
|
||||
s += ')'.repeat(idx);
|
||||
return s;
|
||||
}
|
||||
|
||||
function compute() {
|
||||
var res;
|
||||
console.log(processInp(inputStr));
|
||||
try { res = eval(processInp(inputStr)); }
|
||||
catch(e) { res = "error"; }
|
||||
inputStr = res;
|
||||
qResult = true;
|
||||
updateDisp(inputStr, 19);
|
||||
}
|
||||
|
||||
function touchHandler(e, d) {
|
||||
var x = Math.floor(d.x/(W/3));
|
||||
var y = Math.floor((d.y-dispH-0.2*butH/4)/(butH/4));
|
||||
var c = buttons[curPage][y][x];
|
||||
if (c=="=") { // do the computation
|
||||
compute();
|
||||
return;
|
||||
}
|
||||
else if (c=="<" && inputStr.length>0) inputStr = inputStr.slice(0, -1); // delete last character
|
||||
else if (c=='M' && qResult) memory = inputStr;
|
||||
else if (c=='M') inputStr += memory;
|
||||
else if (c=="C") inputStr = ''; // clear
|
||||
else {
|
||||
if ("Sin Cos Tan Log Exp Pow".indexOf(c)>-1 && c!='E') c += "(";
|
||||
if ("Asi Aco Ata".indexOf(c)>-1) c += "n(";
|
||||
if (c=='1/x') { inputStr = "1/("+inputStr+")"; compute(); return; }
|
||||
if (c=='+/-') { inputStr = "-("+inputStr+")"; compute(); return; }
|
||||
if (qResult && "+-*/^".indexOf(c)==-1) inputStr = c + inputStr + ")";
|
||||
else inputStr += c;
|
||||
}
|
||||
qResult = false;
|
||||
updateDisp(inputStr, 32);
|
||||
}
|
||||
|
||||
function swipeHandler(e,d) {
|
||||
curPage -= e;
|
||||
if (curPage>buttons.length-1) curPage = 0;
|
||||
if (curPage<0) curPage = buttons.length-1;
|
||||
drawPage(curPage);
|
||||
if (d==1) compute();
|
||||
}
|
||||
|
||||
Bangle.on("touch", touchHandler);
|
||||
Bangle.on("swipe", swipeHandler);
|
||||
g.clear();
|
||||
drawPage(curPage);
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "scicalc",
|
||||
"name": "Scientific Calculator",
|
||||
"shortName":"SciCalc",
|
||||
"version":"0.01",
|
||||
"description": "Scientific calculator",
|
||||
"icon": "scicalc.png",
|
||||
"readme": "README.md",
|
||||
"tags": "app,tool",
|
||||
"allow_emulator": true,
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"scicalc.app.js","url":"app.js"},
|
||||
{"name":"scicalc.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 562 B |
|
@ -3,3 +3,5 @@
|
|||
0.03: Add compatibility for Bangle.js 2 and new firmware, added "Alarm at " for the alarm time
|
||||
0.04: Read alarms from new scheduling library, account for higher acceleration sensor noise on Bangle.js 2
|
||||
0.05: Refactor decodeTime() to scheduling library
|
||||
0.06: Add logging
|
||||
use Layout library and display ETA
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# Sleep Phase Alarm
|
||||
|
||||
The display shows:
|
||||
|
||||
- the current time
|
||||
- time of the next alarm or timer
|
||||
- time difference between current time and alarm time (ETA)
|
||||
- current state of the ESS algorithm, "Sleep" or "Awake", useful for debugging
|
||||
|
||||
## Logging
|
||||
|
||||
For each day of month (1..31) the ESS states are logged. An entry will be overwritten in the next month, e.g. an entry on the 4th May will overwrite an entry on the 4th April.
|
||||
The logs can be viewed with the download button:
|
||||
|
||||

|
|
@ -1,6 +1,10 @@
|
|||
const BANGLEJS2 = process.env.HWVERSION == 2; //# check for bangle 2
|
||||
const alarms = require("Storage").readJSON("sched.json",1)||[];
|
||||
const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
|
||||
const Layout = require("Layout");
|
||||
const locale = require('locale');
|
||||
const alarms = require("Storage").readJSON("sched.json",1) || [];
|
||||
const config = require("Storage").readJSON("sleepphasealarm.json",1) || {logs: []};
|
||||
const active = alarms.filter(a=>a.on);
|
||||
let logs = [];
|
||||
|
||||
// Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS):
|
||||
// Marko Borazio, Eugen Berlin, Nagihan Kücükyildiz, Philipp M. Scholl and Kristof Van Laerhoven, "Towards a Benchmark for Wearable Sleep Analysis with Inertial Wrist-worn Sensing Units", ICHI 2014, Verona, Italy, IEEE Press, 2014.
|
||||
|
@ -52,53 +56,45 @@ active.forEach(alarm => {
|
|||
}
|
||||
});
|
||||
|
||||
function drawString(s, y) { //# replaced x: always centered
|
||||
g.reset(); //# moved up to prevent blue background
|
||||
g.clearRect(0, y - 12, 239, y + 8); //# minimized upper+lower clearing
|
||||
g.setFont("Vector", 20);
|
||||
g.setFontAlign(0, 0); // align centered
|
||||
g.drawString(s, g.getWidth() / 2, y); //# set x to center
|
||||
}
|
||||
var layout = new Layout({
|
||||
type:"v", c: [
|
||||
{type:"txt", font:"10%", label:"Sleep Phase Alarm", bgCol:g.theme.bgH, fillx: true, height:Bangle.appRect.h/6},
|
||||
{type:"txt", font:"16%", label: ' '.repeat(20), id:"date", height:Bangle.appRect.h/6},
|
||||
{type:"txt", font:"12%", label: "", id:"alarm_date", height:Bangle.appRect.h/6},
|
||||
{type:"txt", font:"10%", label: ' '.repeat(20), id:"eta", height:Bangle.appRect.h/6},
|
||||
{type:"txt", font:"12%", label: ' '.repeat(20), id:"state", height:Bangle.appRect.h/6},
|
||||
]
|
||||
}, {lazy:true});
|
||||
|
||||
function drawApp() {
|
||||
g.clearRect(0,24,239,215); //# no problem
|
||||
var alarmHour = nextAlarm.getHours();
|
||||
var alarmMinute = nextAlarm.getMinutes();
|
||||
if (alarmHour < 10) alarmHour = "0" + alarmHour;
|
||||
if (alarmMinute < 10) alarmMinute = "0" + alarmMinute;
|
||||
const s = "Alarm at " + alarmHour + ":" + alarmMinute + "\n\n"; //# make distinct to time
|
||||
E.showMessage(s, "Sleep Phase Alarm");
|
||||
layout.alarm_date.label = "Alarm at " + alarmHour + ":" + alarmMinute;
|
||||
layout.render();
|
||||
|
||||
function drawTime() {
|
||||
if (Bangle.isLCDOn()) {
|
||||
const now = new Date();
|
||||
var nowHour = now.getHours();
|
||||
var nowMinute = now.getMinutes();
|
||||
var nowSecond = now.getSeconds();
|
||||
if (nowHour < 10) nowHour = "0" + nowHour;
|
||||
if (nowMinute < 10) nowMinute = "0" + nowMinute;
|
||||
if (nowSecond < 10) nowSecond = "0" + nowSecond;
|
||||
const time = nowHour + ":" + nowMinute + (BANGLEJS2 ? "" : ":" + nowSecond); //# hide seconds on bangle 2
|
||||
drawString(time, BANGLEJS2 ? 85 : 105); //# remove x, adjust height for bangle 2 an newer firmware
|
||||
layout.date.label = locale.time(now, BANGLEJS2 && Bangle.isLocked() ? 1 : 0); // hide seconds on bangle 2
|
||||
const diff = nextAlarm - now;
|
||||
const diffHour = Math.floor((diff % 86400000) / 3600000).toString();
|
||||
const diffMinutes = Math.round(((diff % 86400000) % 3600000) / 60000).toString();
|
||||
layout.eta.label = "ETA: -"+ diffHour + ":" + diffMinutes.padStart(2, '0');
|
||||
layout.render();
|
||||
}
|
||||
}
|
||||
|
||||
if (BANGLEJS2) {
|
||||
drawTime();
|
||||
setTimeout(_ => {
|
||||
drawTime();
|
||||
setInterval(drawTime, 60000);
|
||||
}, 60000 - Date.now() % 60000); //# every new minute on bangle 2
|
||||
} else {
|
||||
setInterval(drawTime, 500); // 2Hz
|
||||
}
|
||||
drawTime();
|
||||
setInterval(drawTime, 500); // 2Hz
|
||||
}
|
||||
|
||||
var buzzCount = 19;
|
||||
function buzz() {
|
||||
if ((require('Storage').readJSON('setting.json',1)||{}).quiet>1) return; // total silence
|
||||
Bangle.setLCDPower(1);
|
||||
Bangle.buzz().then(()=>{
|
||||
Bangle.setLCDPower(1);
|
||||
Bangle.buzz().then(()=>{
|
||||
if (buzzCount--) {
|
||||
setTimeout(buzz, 500);
|
||||
} else {
|
||||
|
@ -108,12 +104,21 @@ function buzz() {
|
|||
});
|
||||
}
|
||||
|
||||
function addLog(time, type) {
|
||||
logs.push({time: time, type: type});
|
||||
require("Storage").writeJSON("sleepphasealarm.json", config);
|
||||
}
|
||||
|
||||
// run
|
||||
var minAlarm = new Date();
|
||||
var measure = true;
|
||||
if (nextAlarm !== undefined) {
|
||||
Bangle.loadWidgets(); //# correct widget load draw order
|
||||
config.logs[nextAlarm.getDate()] = []; // overwrite log on each day of month
|
||||
logs = config.logs[nextAlarm.getDate()];
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
let swest_last;
|
||||
|
||||
// minimum alert 30 minutes early
|
||||
minAlarm.setTime(nextAlarm.getTime() - (30*60*1000));
|
||||
|
@ -124,14 +129,26 @@ if (nextAlarm !== undefined) {
|
|||
|
||||
if (swest !== undefined) {
|
||||
if (Bangle.isLCDOn()) {
|
||||
drawString(swest ? "Sleep" : "Awake", BANGLEJS2 ? 150 : 180); //# remove x, adjust height
|
||||
layout.state.label = swest ? "Sleep" : "Awake";
|
||||
layout.render();
|
||||
}
|
||||
// log
|
||||
if (swest_last != swest) {
|
||||
if (swest) {
|
||||
addLog(new Date(now - sleepthresh*13/12.5*1000), "sleep"); // calculate begin of no motion phase, 13 values/second at 12.5Hz
|
||||
} else {
|
||||
addLog(now, "awake");
|
||||
}
|
||||
swest_last = swest;
|
||||
}
|
||||
}
|
||||
|
||||
if (now >= nextAlarm) {
|
||||
// The alarm widget should handle this one
|
||||
addLog(now, "alarm");
|
||||
setTimeout(load, 1000);
|
||||
} else if (measure && now >= minAlarm && swest === false) {
|
||||
addLog(now, "alarm");
|
||||
buzz();
|
||||
measure = false;
|
||||
}
|
||||
|
@ -141,6 +158,4 @@ if (nextAlarm !== undefined) {
|
|||
E.showMessage('No Alarm');
|
||||
setTimeout(load, 1000);
|
||||
}
|
||||
// BTN2 to menu, BTN3 to main # on bangle 2 only BTN to main
|
||||
if (!BANGLEJS2) setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
|
||||
setWatch(() => load(), BANGLEJS2 ? BTN : BTN3, { repeat: false, edge: "falling" });
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chart.js@2.8.0/dist/Chart.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<p>Please select a wakeup day:</p>
|
||||
<div class="form-group">
|
||||
<select id="day" disabled class="form-select">
|
||||
<option selected disabled>No day</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="sleepChart"></canvas>
|
||||
</div>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@2.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<script>
|
||||
function getData() {
|
||||
const select = document.getElementById("day");
|
||||
const ctx = document.getElementById('sleepChart').getContext('2d');
|
||||
const yTicks = ["sleep", "awake", "alarm"];
|
||||
|
||||
// show loading window
|
||||
Util.showModal("Loading...");
|
||||
// get the data
|
||||
Util.readStorage('sleepphasealarm.json',data=>{
|
||||
let logs = JSON.parse(data || "{}")?.logs || [];
|
||||
// remove window
|
||||
Util.hideModal();
|
||||
|
||||
logs = logs.filter(log => log != null);
|
||||
logs.sort(function(a, b) {return new Date(b?.filter(entry => entry.type === "alarm")[0]?.time) - new Date(a?.filter(entry => entry.type === "alarm")[0]?.time)}); // sort by alarm date desc
|
||||
logs.forEach((log, i) => {
|
||||
const timeStr = log.filter(entry => entry.type === "alarm")[0]?.time;
|
||||
if (timeStr) {
|
||||
const date = new Date(timeStr);
|
||||
let option = document.createElement("option");
|
||||
option.text = date.toLocaleDateString();
|
||||
option.value = i;
|
||||
select.add(option);
|
||||
select.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
labels: [],
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: "No date selected",
|
||||
data: [],
|
||||
fill: false,
|
||||
stepped: true,
|
||||
borderColor: '#ff0000',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
tooltipFormat: 'HH:mm',
|
||||
displayFormats: {
|
||||
millisecond: 'HH:mm:ss.SSS',
|
||||
second: 'HH:mm:ss',
|
||||
minute: 'HH:mm',
|
||||
hour: 'HH',
|
||||
day: 'D MMM.',
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {ticks: {callback: (value, index, values) => yTicks[value]}},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return yTicks[context.raw];
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
select.onchange = () => {
|
||||
const log = logs[select.value];
|
||||
chart.data.labels = log.map(entry => new Date(entry.time));
|
||||
chart.data.datasets[0].data = log.map(entry => yTicks.indexOf(entry.type));
|
||||
const timeStr = log.filter(entry => entry.type === "alarm")[0]?.time;
|
||||
chart.data.datasets[0].label = new Date(timeStr).toLocaleDateString();
|
||||
chart.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Called when app starts
|
||||
function onInit() {
|
||||
getData();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -2,14 +2,17 @@
|
|||
"id": "sleepphasealarm",
|
||||
"name": "SleepPhaseAlarm",
|
||||
"shortName": "SleepPhaseAlarm",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.",
|
||||
"icon": "app.png",
|
||||
"tags": "alarm",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"dependencies": {"scheduler":"type"},
|
||||
"storage": [
|
||||
{"name":"sleepphasealarm.app.js","url":"app.js"},
|
||||
{"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
],
|
||||
"data": [{"name":"sleepphasealarm.json","storageFile":true}],
|
||||
"interface": "interface.html"
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
Loading…
Reference in New Issue