diff --git a/apps/HRV/README.md b/apps/HRV/README.md index 252225fb1..a59eb63f4 100644 --- a/apps/HRV/README.md +++ b/apps/HRV/README.md @@ -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. diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog new file mode 100644 index 000000000..1fa4881b5 --- /dev/null +++ b/apps/dtlaunch/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial version +0.02: Multiple pages diff --git a/apps/dtlaunch/README.md b/apps/dtlaunch/README.md new file mode 100644 index 000000000..70f7ff931 --- /dev/null +++ b/apps/dtlaunch/README.md @@ -0,0 +1,16 @@ +# Desktop style App Launcher + +![](screenshot.jpg) + +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 \ No newline at end of file diff --git a/apps/dtlaunch/app-icon.js b/apps/dtlaunch/app-icon.js new file mode 100644 index 000000000..a49bb0af4 --- /dev/null +++ b/apps/dtlaunch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4ATxAAQC+2N7vd7AX/C/6/7a/4X/a/4X/C/4X/C/4Xfl3iC6vu9wXtI653WAH4A/ABg")) \ No newline at end of file diff --git a/apps/dtlaunch/app.js b/apps/dtlaunch/app.js new file mode 100644 index 000000000..b9ad8525a --- /dev/null +++ b/apps/dtlaunch/app.js @@ -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.nameb.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); diff --git a/apps/dtlaunch/icon.png b/apps/dtlaunch/icon.png new file mode 100644 index 000000000..daa22371b Binary files /dev/null and b/apps/dtlaunch/icon.png differ diff --git a/apps/dtlaunch/screenshot.jpg b/apps/dtlaunch/screenshot.jpg new file mode 100644 index 000000000..18ac25d96 Binary files /dev/null and b/apps/dtlaunch/screenshot.jpg differ diff --git a/apps/hardalarm/ChangeLog b/apps/hardalarm/ChangeLog new file mode 100644 index 000000000..b8b4561b8 --- /dev/null +++ b/apps/hardalarm/ChangeLog @@ -0,0 +1 @@ +0.01: Add a number to match to turn off alarm diff --git a/apps/hardalarm/app-icon.js b/apps/hardalarm/app-icon.js new file mode 100644 index 000000000..6def7b58f --- /dev/null +++ b/apps/hardalarm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkE5gA/AF9cBZXFQYIOGBIUMC5PATgQJFqAIBgovMBwISDAYQ5HGBAAGFxQ/FgMzmcgJ5BIKgAXFIxYuBgMxCIMjmcQgECiBHLFwITBFYIvBiBLBmQwLqECCYMziICBmIeBD4IwKFwQAIGAJGJRQUhgIbDAocQJBHAgYlCOQIrDAoUwhhGIiZZBX4gFEAgIXICIMwX4gFFC45eDF5RgI4pZHAo0gC43AXoaPJC4J4GRwQAMSA4XfmKuBC6kQRYQXMO4YACXYYADO46nDIwcCiBIFU47XDBwcTkBQFa4/MH4sAkYxBBAoWGC4I/DmQ1BdwJPEC5CQEmAECGQKOKMA0gCYRgELxBICCYUyFQZgCJgIWI5lQYIxjCGYMFC5PFLAiKDFwRGJGATCFLoYuKGAYYGiAuMDAkiCoMiCx6SCAAq7IF48F4tQCoMFqAXP4AQF4B1MSAZXFMwIXPA41VC5wA/ADAA==")) diff --git a/apps/hardalarm/app.js b/apps/hardalarm/app.js new file mode 100644 index 000000000..61467b421 --- /dev/null +++ b/apps/hardalarm/app.js @@ -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(); diff --git a/apps/hardalarm/app.png b/apps/hardalarm/app.png new file mode 100644 index 000000000..ed830c5eb Binary files /dev/null and b/apps/hardalarm/app.png differ diff --git a/apps/hardalarm/boot.js b/apps/hardalarm/boot.js new file mode 100644 index 000000000..e327317e3 --- /dev/null +++ b/apps/hardalarm/boot.js @@ -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); + } + } +})(); diff --git a/apps/hardalarm/hardalarm.js b/apps/hardalarm/hardalarm.js new file mode 100644 index 000000000..c3623a193 --- /dev/null +++ b/apps/hardalarm/hardalarm.js @@ -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.hra.hr-b.hr); + showAlarm(active[0]); +} else { + // otherwise just go back to default app + setTimeout(load, 100); +} diff --git a/apps/hardalarm/widget.js b/apps/hardalarm/widget.js new file mode 100644 index 000000000..677266195 --- /dev/null +++ b/apps/hardalarm/widget.js @@ -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); + }}; +})()