diff --git a/apps.json b/apps.json index 6093d82df..5eede9c05 100644 --- a/apps.json +++ b/apps.json @@ -1817,5 +1817,32 @@ {"name":"animclk.pal","url":"animclk.pal"}, {"name":"animclk.img","url":"app-icon.js","evaluate":true} ] + }, + { + "id": "verticalface", + "name": "Vertical watch face", + "shortName":"Vertical Face", + "icon": "app.png", + "version":"0.4.1", + "description": "A simple vertical watch face with the date.", + "tags": "clock", + "type":"clock", + "allow_emulator":true, + "storage": [ + {"name":"verticalface.app.js","url":"app.js"}, + {"name":"verticalface.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "sleepphasealarm", + "name": "SleepPhaseAlarm", + "shortName":"SleepPhaseAlarm", + "icon": "app.png", + "version":"0.01", + "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.", + "tags": "alarm", + "storage": [ + {"name":"sleepphasealarm.app.js","url":"app.js"}, + {"name":"sleepphasealarm.img","url":"app-icon.js","evaluate":true} + ] } ] diff --git a/apps/sleepphasealarm/app-icon.js b/apps/sleepphasealarm/app-icon.js new file mode 100644 index 000000000..3fbcc29af --- /dev/null +++ b/apps/sleepphasealarm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4AfhGIxGAC9YABxBIWF05ZCCYRfRC65CCLSoqBOKwutO4oAK7vd6AXbACAXz93uC6kOC4PgC6YWBAAMCkQAJkAXWkQX2O4YXTU4YXS6czAAUyC/4XBACIX/C8rXBABkNC/4XNAH4A/ABoA=")) diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js new file mode 100644 index 000000000..dbb91c23f --- /dev/null +++ b/apps/sleepphasealarm/app.js @@ -0,0 +1,137 @@ +const alarms = require("Storage").readJSON("alarm.json",1)||[]; +const active = alarms.filter(a=>a.on); + +// 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. +// https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en +// +// Function needs to be called for every measurement but returns a value at maximum once a second (see winwidth) +// start of sleep marker is delayed by sleepthresh due to continous data reading +const winwidth=13; +const nomothresh=0.006; +const sleepthresh=600; +var ess_values = []; +var slsnds = 0; +function calc_ess(val) { + ess_values.push(val); + + if (ess_values.length == winwidth) { + // calculate standard deviation over ~1s + const mean = ess_values.reduce((prev,cur) => cur+prev) / ess_values.length; + const stddev = Math.sqrt(ess_values.map(val => Math.pow(val-mean,2)).reduce((prev,cur) => prev+cur)/ess_values.length); + ess_values = []; + + // check for non-movement according to the threshold + const nonmot = stddev < nomothresh; + + // amount of seconds within non-movement sections + if (nonmot) { + slsnds+=1; + if (slsnds >= sleepthresh) { + return true; // awake + } + } else { + slsnds=0; + return false; // sleep + } + } +} + +// locate next alarm +var nextAlarm; +active.forEach(alarm => { + const now = new Date(); + const alarmHour = alarm.hr/1; + const alarmMinute = Math.round((alarm.hr%1)*60); + var dateAlarm = new Date(now.getFullYear(), now.getMonth(), now.getDate(), alarmHour, alarmMinute); + if (dateAlarm < now) { // dateAlarm in the past, add 24h + dateAlarm.setTime(dateAlarm.getTime() + (24*60*60*1000)); + } + if (nextAlarm === undefined || dateAlarm < nextAlarm) { + nextAlarm = dateAlarm; + } +}); + +function drawString(s, x, y) { + g.clearRect(0,y-15,239,y+15); + g.reset(); + g.setFont("Vector",20); + g.setFontAlign(0,0); // align right bottom + g.drawString(s, x, y); +} + +function drawApp() { + g.clearRect(0,24,239,215); + var alarmHour = nextAlarm.getHours(); + var alarmMinute = nextAlarm.getMinutes(); + if (alarmHour < 10) alarmHour = "0" + alarmHour; + if (alarmMinute < 10) alarmMinute = "0" + alarmMinute; + const s = alarmHour + ":" + alarmMinute + "\n\n"; + E.showMessage(s, "Sleep Phase Alarm"); + + 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 + ":" + nowSecond; + drawString(time, 120, 140); + } + } + + setInterval(drawTime, 500); // 2Hz +} + +var buzzCount = 19; +function buzz() { + Bangle.setLCDPower(1); + Bangle.buzz().then(()=>{ + if (buzzCount--) { + setTimeout(buzz, 500); + } else { + // back to main after finish + setTimeout(load, 1000); + } + }); +} + +// run +var minAlarm = new Date(); +var measure = true; +if (nextAlarm !== undefined) { + Bangle.drawWidgets(); + Bangle.loadWidgets(); + + // minimum alert 30 minutes early + minAlarm.setTime(nextAlarm.getTime() - (30*60*1000)); + setInterval(function() { + const now = new Date(); + const acc = Bangle.getAccel().mag; + const swest = calc_ess(acc); + + if (swest !== undefined) { + if (Bangle.isLCDOn()) { + drawString(swest ? "Sleep" : "Awake", 120, 180); + } + } + + if (now >= nextAlarm) { + // The alarm widget should handle this one + setTimeout(load, 1000); + } else if (measure && now >= minAlarm && swest === false) { + buzz(); + measure = false; + } + }, 80); // 12.5Hz + drawApp(); +} else { + E.showMessage('No Alarm'); + setTimeout(load, 1000); +} +// BTN2 to menu, BTN3 to main +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); +setWatch(() => load(), BTN3, { repeat: false, edge: "falling" }); diff --git a/apps/sleepphasealarm/app.png b/apps/sleepphasealarm/app.png new file mode 100644 index 000000000..0a19bd463 Binary files /dev/null and b/apps/sleepphasealarm/app.png differ diff --git a/apps/verticalface/app-icon.js b/apps/verticalface/app-icon.js new file mode 100644 index 000000000..a3b7a6dec --- /dev/null +++ b/apps/verticalface/app-icon.js @@ -0,0 +1 @@ +E.toArrayBuffer(atob("MDAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADT4+Pj4+DQAAAAANPj4+Pj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANDTg4ODg4DQ0ABg0xODg4ODgNDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAAAAAOD4ADT44AAAAAAA+OAAAAABWVjJWVlZWVlZWVjIAAAAAAAAAAAAAAAA+OAAAAAAAOD4ADT44AAAAAAA+OAAAAABWMgBWK1YrVlZWVisAAAAAAAAAAAAAAAA+OAAAAAAAOD4ADT44AAAAAAA+OAAAAAArVgBWKysAVgArAFYAAAAAAAAAAAAAAAANDQAAAAAAOD4ADT44AAAANzg+OAAAAABWVgAyK1ZWMgArVisAAAAAAAAAAAAAAAAAAAAAAAAAOD4ADT44AAAAOD4+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOD4ADT44ADc4DQ0+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOD4ADT44ADg+DQA+OAAAAACBgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADg+DQAADT4+Pg0AAAA+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABg0NDTg4DQAADT44OA0AAAA+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADT4+Pg0AAAAADT44AAAAAAA+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANMTg4OA0AAAAADT44AAAAAAA+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAAAAAAAAADT44AAAAAAA+OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+ODg4ODg4ODgABw03ODg4ODgNDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+Pj4+Pj4+Pj4AAAANPj4+Pj4AAAAAAABWVlZWVlYrVgAAAAAAAAAAAAAAAAAAAAANDQ0NDQ0NDQ0AAAAHDQ0NDQ0AAAAAAABWVlZWVjIyVgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWVlYAKysAVgAAAAAAAAAAAAAAAAAAAAAADT4+Pj4+DQAAAAAAADg+DQAAAAAAAABWVjIAKysAMgAAAAAAAAAAAAAAAAAAAAANDTg4ODg4DQ0AAAAHDTg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAAAAAOD4AAAANPj4+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAAAAAOD4ABw03ODg+DQAAAAAAAACBgQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAAAAAOD4ADT44ADg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAAA04OD4ABw0NADg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAAAADg+Pj4AAAAAADg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAANODENOD4AAAAAADg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+OAANPg0AOD4AAAAAADg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+Pj44AAAAOD4AAAAAADg+DQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+ODg3AAAAOD4AAAAAADg+DQAAAAAAAABWVlYAVlZWVlZWVjJWVgAAAAAAAAAAAAA+OAAAAAAAOD4AAAAAADg+DQAAAAAAAABWAFZWVlZWVlZWKzJWMgAAAAAAAAAAAAA+OAAAAAAAOD4AAAAAADg+DQAAAAAAAABWK1YAVitWVjJWAFZWAAAAAAAAAAAAAAA+OAAAAAAAOD4AAAAAADg+DQAAAAAAAABWVjIAVisyMisrVitWVgAAAAAAAAAAAAANMTg4ODg4MQ0ABzg4ODg+ODgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADT4+Pj4+DQAADT4+Pj4+Pj4AAAAAAACBgV0yVgAAAAAAAAAAAAAAAAAAAAAAAAAABg0NDQ0NBgAABg0NDQ0NDQ0AAAAAAACBgTJdKwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBgVZWKwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWVlZWVgAAAAAAAAAAAAAAAAAAAAAAAA0NBwAHBgcADQANBg0NAA0ADQcNDQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0NNw04DQ0ADQ0NDQ04AA0NNzg4OAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0NDQA4DQ0ADQ04DQ0HAA0NOA0NDQ0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYGBwAHDQcADQcNBwcAAA0ADQcHDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) diff --git a/apps/verticalface/app.js b/apps/verticalface/app.js new file mode 100644 index 000000000..3e4650d18 --- /dev/null +++ b/apps/verticalface/app.js @@ -0,0 +1,154 @@ +require("Font8x12").add(Graphics); +let HRMstate = false; +let currentHRM = "CALC"; + + +function drawTimeDate() { + var d = new Date(); + var h = d.getHours(), m = d.getMinutes(), day = d.getDate(), month = d.getMonth(), weekDay = d.getDay(); + + var daysOfWeek = ["SUN", "MON", "TUE","WED","THU","FRI","SAT"]; + var hours = h; + var mins= ("0"+m).substr(-2); + var date = `${daysOfWeek[weekDay]}|${day}|${("0"+(month+1)).substr(-2)}`; + + + // Reset the state of the graphics library + g.reset(); + // Set color + g.setColor('#2ecc71'); + // draw the current time (4x size 7 segment) + g.setFont("8x12",9); + g.setFontAlign(-1,0); // align right bottom + g.drawString(hours, 25, 65, true /*clear background*/); + g.drawString(mins, 25, 155, true /*clear background*/); + + // draw the date (2x size 7 segment) + g.setFont("6x8",2); + g.setFontAlign(-1,0); // align right bottom + g.drawString(date, 20, 215, true /*clear background*/); +} + + +//We will create custom "Widgets" for our face. + +function drawSteps() { + //Reset to defaults. + g.reset(); + // draw the date (2x size 7 segment) + g.setColor('#7f8c8d'); + g.setFont("8x12",2); + g.setFontAlign(-1,0); // align right bottom + g.drawString("STEPS", 145, 40, true /*clear background*/); + g.setColor('#bdc3c7'); + g.drawString("-", 145, 65, true /*clear background*/); +} + +function drawBPM(on) { + //Reset to defaults. + g.reset(); + g.setColor('#7f8c8d'); + g.setFont("8x12",2); + g.setFontAlign(-1,0); + var heartRate = 0; + + if(on){ + g.drawString("BPM", 145, 105, true); + g.setColor('#e74c3c'); + g.drawString("*", 190, 105, false); + g.setColor('#bdc3c7'); + //Showing current heartrate reading. + heartRate = currentHRM.toString() + " "; + return g.drawString(heartRate, 145, 130, true /*clear background*/); + } else { + g.drawString("BPM ", 145, 105, true /*clear background*/); + g.setColor('#bdc3c7'); + return g.drawString("- ", 145, 130, true); //Padding + } +} + +function drawBattery() { + let charge = E.getBattery(); + //Reset to defaults. + g.reset(); + // draw the date (2x size 7 segment) + g.setColor('#7f8c8d'); + g.setFont("8x12",2); + g.setFontAlign(-1,0); // align right bottom + g.drawString("CHARGE", 145, 170, true /*clear background*/); + g.setColor('#bdc3c7'); + g.drawString(`${charge}%`, 145, 195, true /*clear background*/); +} + + +// Clear the screen once, at startup +g.clear(); + +// draw immediately at first +drawTimeDate(); +drawSteps(); +drawBPM(); +drawBattery(); + +var secondInterval = setInterval(()=>{ + drawTimeDate(); +}, 15000); + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + secondInterval = setInterval(()=>{ + drawTimeDate(); +}, 15000); + //Screen on + drawBPM(HRMstate); + drawTimeDate(); + drawBattery(); + } else { + //Screen off + clearInterval(secondInterval); + } +}); + +// Show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +Bangle.on('touch', function(button) { + if(button == 1 || button == 2){ + Bangle.showLauncher(); + } +}); + +//HRM Controller. +setWatch(function(){ + if(!HRMstate){ + console.log("Toggled HRM"); + //Turn on. + Bangle.buzz(); + Bangle.setHRMPower(1); + currentHRM = "CALC"; + HRMstate = true; + } else if(HRMstate){ + console.log("Toggled HRM"); + //Turn off. + Bangle.buzz(); + Bangle.setHRMPower(0); + HRMstate = false; + currentHRM = []; + } + drawBPM(HRMstate); +}, BTN1, { repeat: true, edge: "falling" }); + +Bangle.on('HRM', function(hrm) { + if(hrm.confidence > 90){ + /*Do more research to determine effect algorithm for heartrate average.*/ + console.log(hrm.bpm); + currentHRM = hrm.bpm; + drawBPM(HRMstate); + } +}); + + +//Bangle.on('step', function(up) { +// console.log("Step"); +//}); \ No newline at end of file diff --git a/apps/verticalface/app.png b/apps/verticalface/app.png new file mode 100644 index 000000000..f7e1cba57 Binary files /dev/null and b/apps/verticalface/app.png differ