mirror of https://github.com/espruino/BangleApps
commit
f7ca693ff4
|
@ -1,9 +1,14 @@
|
|||
Monitor Heart Rate Variability using the Bangle.JS
|
||||
===================================================
|
||||
|
||||
One-time mode:
|
||||
-------------
|
||||
|
||||
This will take a HRV measurement over a single approx 30 second period. It will also provide you with a HR reading based on the post-processing of the signal.
|
||||
|
||||
Continuous mode:
|
||||
----------------
|
||||
|
||||
This will continually take measurements over 30 second periods every 3 and half minutes and log them to a CSV file on the Bangle until the watch is reset; this file can then be reviewed in Excel or other apps. The log file is reset each time you restart and select this mode to save on storage. The log file is just 1 line per each 3 minute cycle showing: timestamp, HR, HRV and sample count.
|
||||
|
||||
Note that in both modes, if the watch seems unresponsive, just wait for it to finish its cycle of processing the data, which shouldn’t take longer than a couple of minutes.
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial version
|
||||
0.02: Multiple pages
|
|
@ -0,0 +1,16 @@
|
|||
# Desktop style App Launcher
|
||||
|
||||

|
||||
|
||||
In the picture above, the Settings app is selected.
|
||||
## Controls
|
||||
|
||||
**BTN1** - move backward through app icons on a page
|
||||
|
||||
**BTN2** - run the selected app
|
||||
|
||||
**BTN3** - move forward through app icons
|
||||
|
||||
**Swipe Left** - move to next page of app icons
|
||||
|
||||
**Swipe Right** - move to previous page of app icons
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4ATxAAQC+2N7vd7AX/C/6/7a/4X/a/4X/C/4X/C/4Xfl3iC6vu9wXtI653WAH4A/ABg"))
|
|
@ -0,0 +1,73 @@
|
|||
/* Desktop launcher
|
||||
*
|
||||
*/
|
||||
|
||||
var s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="clock" || !app.type));
|
||||
apps.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;
|
||||
});
|
||||
|
||||
var Napps = apps.length;
|
||||
var Npages = Math.ceil(Napps/6);
|
||||
var maxPage = Npages-1;
|
||||
var selected = -1;
|
||||
var oldselected = -1;
|
||||
var page = 0;
|
||||
|
||||
function draw_icon(p,n,selected) {
|
||||
var x = (n%3)*80;
|
||||
var y = n>2?130:40;
|
||||
(selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89);
|
||||
g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25});
|
||||
g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1);
|
||||
var txt = apps[p*6+n].name.split(" ");
|
||||
for (var i = 0; i < txt.length; i++) {
|
||||
txt[i] = txt[i].trim();
|
||||
g.drawString(txt[i],x+40,y+70+i*8);
|
||||
}
|
||||
}
|
||||
|
||||
function drawPage(p){
|
||||
g.setColor(0,0,0).fillRect(0,0,239,239);
|
||||
g.setFont("6x8",2).setFontAlign(0,-1,0).setColor(1,1,1).drawString("Bangle ("+(p+1)+"/"+Npages+")",120,12);
|
||||
for (var i=0;i<6;i++) {
|
||||
if (!apps[p*6+i]) return i;
|
||||
draw_icon(p,i,selected==i);
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.on("swipe",(dir)=>{
|
||||
selected = 0;
|
||||
oldselected=-1;
|
||||
if (dir<0){
|
||||
++page; if (page>maxPage) page=maxPage;
|
||||
drawPage(page);
|
||||
} else {
|
||||
--page; if (page<0) page=0;
|
||||
drawPage(page);
|
||||
}
|
||||
});
|
||||
|
||||
function nextapp(d){
|
||||
oldselected = selected;
|
||||
selected+=d;
|
||||
selected = selected<0?5:selected>5?0:selected;
|
||||
selected = (page*6+selected)>=Napps?0:selected;
|
||||
draw_icon(page,selected,true);
|
||||
if (oldselected>=0) draw_icon(page,oldselected,false);
|
||||
}
|
||||
|
||||
function doselect(){
|
||||
load(apps[page*6+selected].src);
|
||||
}
|
||||
|
||||
setWatch(nextapp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
|
||||
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
|
||||
setWatch(nextapp.bind(null,1), BTN3, {repeat:true,edge:"falling"});
|
||||
|
||||
drawPage(0);
|
Binary file not shown.
After Width: | Height: | Size: 305 B |
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: Add a number to match to turn off alarm
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkE5gA/AF9cBZXFQYIOGBIUMC5PATgQJFqAIBgovMBwISDAYQ5HGBAAGFxQ/FgMzmcgJ5BIKgAXFIxYuBgMxCIMjmcQgECiBHLFwITBFYIvBiBLBmQwLqECCYMziICBmIeBD4IwKFwQAIGAJGJRQUhgIbDAocQJBHAgYlCOQIrDAoUwhhGIiZZBX4gFEAgIXICIMwX4gFFC45eDF5RgI4pZHAo0gC43AXoaPJC4J4GRwQAMSA4XfmKuBC6kQRYQXMO4YACXYYADO46nDIwcCiBIFU47XDBwcTkBQFa4/MH4sAkYxBBAoWGC4I/DmQ1BdwJPEC5CQEmAECGQKOKMA0gCYRgELxBICCYUyFQZgCJgIWI5lQYIxjCGYMFC5PFLAiKDFwRGJGATCFLoYuKGAYYGiAuMDAkiCoMiCx6SCAAq7IF48F4tQCoMFqAXP4AQF4B1MSAZXFMwIXPA41VC5wA/ADAA=="))
|
|
@ -0,0 +1,112 @@
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
var alarms = require("Storage").readJSON("hardalarm.json",1)||[];
|
||||
/*alarms = [
|
||||
{ on : true,
|
||||
hr : 6.5, // hours + minutes/60
|
||||
msg : "Eat chocolate",
|
||||
last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
|
||||
rp : true, // repeat
|
||||
as : false, // auto snooze
|
||||
}
|
||||
];*/
|
||||
|
||||
function formatTime(t) {
|
||||
var hrs = 0|t;
|
||||
var mins = Math.round((t-hrs)*60);
|
||||
return hrs+":"+("0"+mins).substr(-2);
|
||||
}
|
||||
|
||||
function getCurrentHr() {
|
||||
var time = new Date();
|
||||
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
const menu = {
|
||||
'': { 'title': 'Alarms' },
|
||||
'New Alarm': ()=>editAlarm(-1)
|
||||
};
|
||||
alarms.forEach((alarm,idx)=>{
|
||||
txt = (alarm.on?"on ":"off ")+formatTime(alarm.hr);
|
||||
if (alarm.rp) txt += " (repeat)";
|
||||
menu[txt] = function() {
|
||||
editAlarm(idx);
|
||||
};
|
||||
});
|
||||
menu['< Back'] = ()=>{load();};
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editAlarm(alarmIndex) {
|
||||
var newAlarm = alarmIndex<0;
|
||||
var hrs = 12;
|
||||
var mins = 0;
|
||||
var en = true;
|
||||
var repeat = true;
|
||||
var as = false;
|
||||
if (!newAlarm) {
|
||||
var a = alarms[alarmIndex];
|
||||
hrs = 0|a.hr;
|
||||
mins = Math.round((a.hr-hrs)*60);
|
||||
en = a.on;
|
||||
repeat = a.rp;
|
||||
as = a.as;
|
||||
}
|
||||
const menu = {
|
||||
'': { 'title': 'Alarms' },
|
||||
'Hours': {
|
||||
value: hrs,
|
||||
onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
},
|
||||
'Minutes': {
|
||||
value: mins,
|
||||
onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
|
||||
},
|
||||
'Enabled': {
|
||||
value: en,
|
||||
format: v=>v?"On":"Off",
|
||||
onchange: v=>en=v
|
||||
},
|
||||
'Repeat': {
|
||||
value: en,
|
||||
format: v=>v?"Yes":"No",
|
||||
onchange: v=>repeat=v
|
||||
},
|
||||
'Auto snooze': {
|
||||
value: as,
|
||||
format: v=>v?"Yes":"No",
|
||||
onchange: v=>as=v
|
||||
}
|
||||
};
|
||||
function getAlarm() {
|
||||
var hr = hrs+(mins/60);
|
||||
var day = 0;
|
||||
// If alarm is for tomorrow not today (eg, in the past), set day
|
||||
if (hr < getCurrentHr())
|
||||
day = (new Date()).getDate();
|
||||
// Save alarm
|
||||
return {
|
||||
on : en, hr : hr,
|
||||
last : day, rp : repeat, as: as
|
||||
};
|
||||
}
|
||||
menu["> Save"] = function() {
|
||||
if (newAlarm) alarms.push(getAlarm());
|
||||
else alarms[alarmIndex] = getAlarm();
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
showMainMenu();
|
||||
};
|
||||
if (!newAlarm) {
|
||||
menu["> Delete"] = function() {
|
||||
alarms.splice(alarmIndex,1);
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
menu['< Back'] = showMainMenu;
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
showMainMenu();
|
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,25 @@
|
|||
// check for alarms
|
||||
(function() {
|
||||
var alarms = require('Storage').readJSON('hardalarm.json',1)||[];
|
||||
var time = new Date();
|
||||
var active = alarms.filter(a=>a.on);
|
||||
if (active.length) {
|
||||
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
||||
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
if (!require('Storage').read("hardalarm.js")) {
|
||||
console.log("No alarm app!");
|
||||
require('Storage').write('hardalarm.json',"[]");
|
||||
} else {
|
||||
var t = 3600000*(active[0].hr-hr);
|
||||
if (active[0].last == time.getDate() || t < 0) t += 86400000;
|
||||
if (t<1000) t=1000;
|
||||
/* execute alarm at the correct time. We avoid execing immediately
|
||||
since this code will get called AGAIN when hardalarm.js is loaded. alarm.js
|
||||
will then clearInterval() to get rid of this call so it can proceed
|
||||
normally. */
|
||||
setTimeout(function() {
|
||||
load("hardalarm.js");
|
||||
},t);
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,127 @@
|
|||
// Chances are boot0.js got run already and scheduled *another*
|
||||
// 'load(hardalarm.js)' - so let's remove it first!
|
||||
clearInterval();
|
||||
|
||||
function formatTime(t) {
|
||||
var hrs = 0|t;
|
||||
var mins = Math.round((t-hrs)*60);
|
||||
return hrs+":"+("0"+mins).substr(-2);
|
||||
}
|
||||
|
||||
function getCurrentHr() {
|
||||
var time = new Date();
|
||||
return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
}
|
||||
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
|
||||
function getRandomFromRange(lowerRangeMin, lowerRangeMax, higherRangeMin, higherRangeMax) {
|
||||
var lowerRange = lowerRangeMax - lowerRangeMin;
|
||||
var higherRange = higherRangeMax - higherRangeMin;
|
||||
var fullRange = lowerRange + higherRange;
|
||||
var randomNum = getRandomInt(fullRange);
|
||||
if(randomNum <= (lowerRangeMax - lowerRangeMin)) {
|
||||
return randomNum + lowerRangeMin;
|
||||
} else {
|
||||
return randomNum + (higherRangeMin - lowerRangeMax);
|
||||
}
|
||||
}
|
||||
|
||||
function showNumberPicker(currentGuess, randomNum) {
|
||||
if(currentGuess == randomNum) {
|
||||
E.showMessage("" + currentGuess + "\n PRESS ENTER", "Get to " + randomNum);
|
||||
} else {
|
||||
E.showMessage("" + currentGuess, "Get to " + randomNum);
|
||||
}
|
||||
}
|
||||
|
||||
function showPrompt(msg, buzzCount, alarm) {
|
||||
E.showPrompt(msg,{
|
||||
title:"STAY AWAKE!",
|
||||
buttons : {"Sleep":0,"Stop":1} // default is sleep so it'll come back in 10 mins
|
||||
}).then(function(choice) {
|
||||
buzzCount = 0;
|
||||
if (choice==0) {
|
||||
if(alarm.ohr===undefined) alarm.ohr = alarm.hr;
|
||||
alarm.hr += 10/60; // 10 minutes
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
load();
|
||||
} else if(choice==1) {
|
||||
alarm.last = (new Date()).getDate();
|
||||
if (alarm.ohr!==undefined) {
|
||||
alarm.hr = alarm.ohr;
|
||||
delete alarm.ohr;
|
||||
}
|
||||
if (!alarm.rp) alarm.on = false;
|
||||
require("Storage").write("hardalarm.json",JSON.stringify(alarms));
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showAlarm(alarm) {
|
||||
var msg = formatTime(alarm.hr);
|
||||
var buzzCount = 20;
|
||||
if (alarm.msg)
|
||||
msg += "\n"+alarm.msg;
|
||||
var okClicked = false;
|
||||
var currentGuess = 10;
|
||||
var randomNum = getRandomFromRange(0, 7, 13, 20);
|
||||
showNumberPicker(currentGuess, randomNum)
|
||||
setWatch(o => {
|
||||
if(!okClicked && currentGuess < 20) {
|
||||
currentGuess = currentGuess + 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}, BTN1, {repeat: true, edge: 'rising'});
|
||||
|
||||
setWatch(o => {
|
||||
if(currentGuess == randomNum) {
|
||||
okClicked = true;
|
||||
showPrompt(msg, buzzCount, alarm);
|
||||
}
|
||||
}, BTN2, {repeat: true, edge: 'rising'});
|
||||
|
||||
setWatch(o => {
|
||||
if(!okClicked && currentGuess > 0) {
|
||||
currentGuess = currentGuess - 1;
|
||||
showNumberPicker(currentGuess, randomNum);
|
||||
}
|
||||
}, BTN3, {repeat: true, edge: 'rising'});
|
||||
|
||||
function buzz() {
|
||||
Bangle.buzz(500).then(()=>{
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(500).then(function() {
|
||||
setTimeout(()=>{
|
||||
Bangle.buzz(2000).then(function() {
|
||||
if (buzzCount--)
|
||||
setTimeout(buzz, 2000);
|
||||
else if(alarm.as) { // auto-snooze
|
||||
buzzCount = 20;
|
||||
setTimeout(buzz, 600000); // 10 minutes
|
||||
}
|
||||
});
|
||||
},100);
|
||||
});
|
||||
},100);
|
||||
});
|
||||
}
|
||||
buzz();
|
||||
}
|
||||
|
||||
// Check for alarms
|
||||
var day = (new Date()).getDate();
|
||||
var hr = getCurrentHr()+10000; // get current time - 10s in future to ensure we alarm if we've started the app a tad early
|
||||
var alarms = require("Storage").readJSON("hardalarm.json",1)||[];
|
||||
var active = alarms.filter(a=>a.on&&(a.hr<hr)&&(a.last!=day));
|
||||
if (active.length) {
|
||||
// if there's an alarm, show it
|
||||
active = active.sort((a,b)=>a.hr-b.hr);
|
||||
showAlarm(active[0]);
|
||||
} else {
|
||||
// otherwise just go back to default app
|
||||
setTimeout(load, 100);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
(() => {
|
||||
var alarms = require('Storage').readJSON('hardalarm.json',1)||[];
|
||||
alarms = alarms.filter(alarm=>alarm.on);
|
||||
if (!alarms.length) return; // no alarms, no widget!
|
||||
delete alarms;
|
||||
// add the widget
|
||||
WIDGETS["alarm"]={area:"tl",width:24,draw:function() {
|
||||
g.setColor(-1);
|
||||
g.drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
|
||||
}};
|
||||
})()
|
Loading…
Reference in New Issue