Merge branch 'master' of github.com:espruino/BangleApps

pull/1776/head
Gordon Williams 2022-04-29 08:48:54 +01:00
commit ed7319e74b
32 changed files with 550 additions and 93 deletions

View File

@ -1 +1 @@
theme: jekyll-theme-minimal
theme: jekyll-theme-slate

View File

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

View File

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

View File

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

View File

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

115
apps/bradbury/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/bradbury/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Fix true wind computation, add swipe gesture to pause GPS

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{ "id": "openwind",
"name": "OpenWind",
"shortName":"OpenWind",
"version":"0.01",
"version":"0.02",
"description": "OpenWind",
"icon": "openwind.png",
"readme": "README.md",

View File

@ -1 +1,2 @@
0.01: Initial version
0.02: Moved settings from launcher to settings->apps menu

View File

@ -1 +0,0 @@
require("heatshrink").decompress(atob("kMigILIgPAAYMD/ADBwcGhkAwM5wcA/+2//Av/Rn/giFoyFggkUrFggEKlAkCiApCx+AAYNGoADBkU4AYMQj4DBvEICANkAoIPBgE2B4MAiMAH4MAwECAYNALYUgBIISCHYMYAoQWBAIMEgAYBAIMBwEDDQNgDwUf/4eBg4DCAA4"))

View File

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

View File

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

1
apps/scicalc/ChangeLog Normal file
View File

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

18
apps/scicalc/README.md Normal file
View File

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

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AJioAaF1wwSFzowRCQUZo4AWjIvVFy4ABF/4vXyGQAYov/R+sZFy8ZF6oAcF/4vvi4AeF/4SCjseAAMdAx8MAAYvVEAQABAx4v/R/TvvF96PUg8cAAMHd9QuCAAIv/R+rvvF96Pvd94vvR97vvF96Pvd94vvR97vsGDwuQGDouSAH4A/AGwA=="))

113
apps/scicalc/app.js Normal file
View File

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

View File

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

BIN
apps/scicalc/scicalc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

View File

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

View File

@ -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:
![](screenshot.jpg)

View File

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

View File

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

View File

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