2022-04-28 08:21:54 +00:00
const BANGLEJS2 = process . env . HWVERSION == 2 ; // check for bangle 2
2022-06-11 07:00:47 +00:00
const CONFIGFILE = "sleepphasealarm.json" ;
2022-04-27 19:22:33 +00:00
const Layout = require ( "Layout" ) ;
const locale = require ( 'locale' ) ;
2022-04-22 11:06:42 +00:00
const alarms = require ( "Storage" ) . readJSON ( "sched.json" , 1 ) || [ ] ;
2022-06-11 07:00:47 +00:00
const config = Object . assign ( {
logs : [ ] , // array of length 31 with one entry for each day of month
settings : {
startBeforeAlarm : 0 , // 0 = start immediately, 1..23 = start 1h..23h before alarm time
disableAlarm : false ,
}
} , require ( "Storage" ) . readJSON ( CONFIGFILE , 1 ) || { } ) ;
2020-05-27 05:50:48 +00:00
const active = alarms . filter ( a => a . on ) ;
2022-06-11 07:00:47 +00:00
const schedSettings = require ( "sched" ) . getSettings ( ) ;
let buzzCount = schedSettings . buzzCount ;
2022-04-22 11:06:42 +00:00
let logs = [ ] ;
2020-05-27 05:50:48 +00:00
// Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS):
2020-05-29 15:18:12 +00:00
// 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.
2020-05-27 05:50:48 +00:00
// 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 ;
2022-04-09 09:22:51 +00:00
const nomothresh = 0.03 ; // 0.006 was working on Bangle1, but Bangle2 has higher noise.
2020-05-27 05:50:48 +00:00
const sleepthresh = 600 ;
var ess _values = [ ] ;
var slsnds = 0 ;
2022-04-09 09:22:51 +00:00
function calc _ess ( acc _magn ) {
ess _values . push ( acc _magn ) ;
2020-05-27 05:50:48 +00:00
2020-05-28 13:38:46 +00:00
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 = [ ] ;
2020-05-27 05:50:48 +00:00
2020-05-28 13:38:46 +00:00
// check for non-movement according to the threshold
const nonmot = stddev < nomothresh ;
2020-05-27 05:50:48 +00:00
2020-05-28 13:38:46 +00:00
// amount of seconds within non-movement sections
if ( nonmot ) {
slsnds += 1 ;
if ( slsnds >= sleepthresh ) {
2022-03-21 10:03:32 +00:00
return true ; // sleep
2020-05-28 13:38:46 +00:00
}
} else {
slsnds = 0 ;
2022-03-21 10:03:32 +00:00
return false ; // awake
2020-05-28 13:38:46 +00:00
}
}
2020-05-27 05:50:48 +00:00
}
// locate next alarm
2022-06-11 07:00:47 +00:00
var nextAlarmDate ;
var nextAlarmConfig ;
2020-05-27 05:50:48 +00:00
active . forEach ( alarm => {
2020-05-28 13:38:46 +00:00
const now = new Date ( ) ;
2022-05-12 20:51:41 +00:00
const time = require ( "time_utils" ) . decodeTime ( alarm . t ) ;
var dateAlarm = new Date ( now . getFullYear ( ) , now . getMonth ( ) , now . getDate ( ) , time . h , time . m ) ;
2020-05-28 13:38:46 +00:00
if ( dateAlarm < now ) { // dateAlarm in the past, add 24h
dateAlarm . setTime ( dateAlarm . getTime ( ) + ( 24 * 60 * 60 * 1000 ) ) ;
}
2022-05-01 20:24:29 +00:00
if ( ( alarm . dow >> dateAlarm . getDay ( ) ) & 1 ) { // check valid day of week
2022-06-11 07:00:47 +00:00
if ( nextAlarmDate === undefined || dateAlarm < nextAlarmDate ) {
nextAlarmDate = dateAlarm ;
nextAlarmConfig = alarm ;
2022-05-01 20:24:29 +00:00
}
2020-05-28 13:38:46 +00:00
}
2020-05-27 05:50:48 +00:00
} ) ;
2022-04-27 19:22:33 +00:00
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 } ) ;
2020-05-27 05:50:48 +00:00
function drawApp ( ) {
2022-06-11 07:00:47 +00:00
var alarmHour = nextAlarmDate . getHours ( ) ;
var alarmMinute = nextAlarmDate . getMinutes ( ) ;
2020-05-28 13:38:46 +00:00
if ( alarmHour < 10 ) alarmHour = "0" + alarmHour ;
if ( alarmMinute < 10 ) alarmMinute = "0" + alarmMinute ;
2022-04-27 19:22:33 +00:00
layout . alarm _date . label = "Alarm at " + alarmHour + ":" + alarmMinute ;
layout . render ( ) ;
2020-05-27 05:50:48 +00:00
2020-05-28 13:38:46 +00:00
function drawTime ( ) {
if ( Bangle . isLCDOn ( ) ) {
const now = new Date ( ) ;
2022-04-27 19:22:33 +00:00
layout . date . label = locale . time ( now , BANGLEJS2 && Bangle . isLocked ( ) ? 1 : 0 ) ; // hide seconds on bangle 2
2022-06-11 07:00:47 +00:00
const diff = nextAlarmDate - now ;
2022-04-27 19:22:33 +00:00
const diffHour = Math . floor ( ( diff % 86400000 ) / 3600000 ) . toString ( ) ;
2022-05-01 20:24:29 +00:00
const diffMinutes = Math . floor ( ( ( diff % 86400000 ) % 3600000 ) / 60000 ) . toString ( ) ;
2022-04-27 19:22:33 +00:00
layout . eta . label = "ETA: -" + diffHour + ":" + diffMinutes . padStart ( 2 , '0' ) ;
layout . render ( ) ;
2020-05-28 13:38:46 +00:00
}
}
2020-05-27 05:50:48 +00:00
2022-04-27 19:22:33 +00:00
drawTime ( ) ;
setInterval ( drawTime , 500 ) ; // 2Hz
2020-05-27 05:50:48 +00:00
}
function buzz ( ) {
2021-03-23 18:56:34 +00:00
if ( ( require ( 'Storage' ) . readJSON ( 'setting.json' , 1 ) || { } ) . quiet > 1 ) return ; // total silence
2022-06-11 07:00:47 +00:00
Bangle . setLCDPower ( 1 ) ;
require ( "buzz" ) . pattern ( nextAlarmConfig . vibrate || ";" ) ;
if ( buzzCount -- ) {
setTimeout ( buzz , schedSettings . buzzIntervalMillis ) ;
} else {
// back to main after finish
setTimeout ( load , 1000 ) ;
}
2020-05-27 05:50:48 +00:00
}
2022-04-22 11:06:42 +00:00
function addLog ( time , type ) {
logs . push ( { time : time , type : type } ) ;
2022-06-11 07:00:47 +00:00
if ( logs . length > 1 ) { // Do not write if there is only one state
require ( "Storage" ) . writeJSON ( CONFIGFILE , config ) ;
}
2022-04-22 11:06:42 +00:00
}
2020-05-27 05:50:48 +00:00
// run
var minAlarm = new Date ( ) ;
var measure = true ;
2022-06-11 07:00:47 +00:00
if ( nextAlarmDate !== undefined ) {
config . logs [ nextAlarmDate . getDate ( ) ] = [ ] ; // overwrite log on each day of month
logs = config . logs [ nextAlarmDate . getDate ( ) ] ;
2022-04-27 19:22:33 +00:00
g . clear ( ) ;
2022-04-22 11:06:42 +00:00
Bangle . loadWidgets ( ) ;
2020-05-28 13:38:46 +00:00
Bangle . drawWidgets ( ) ;
2022-04-22 11:06:42 +00:00
let swest _last ;
2020-05-27 05:50:48 +00:00
2020-05-28 13:38:46 +00:00
// minimum alert 30 minutes early
2022-06-11 07:00:47 +00:00
minAlarm . setTime ( nextAlarmDate . getTime ( ) - ( 30 * 60 * 1000 ) ) ;
run = ( ) => {
layout . state . label = "Start" ;
layout . render ( ) ;
Bangle . on ( 'accel' , ( accelData ) => { // 12.5Hz
const now = new Date ( ) ;
const acc = accelData . mag ;
const swest = calc _ess ( acc ) ;
2020-05-27 05:50:48 +00:00
2022-06-11 07:00:47 +00:00
if ( swest !== undefined ) {
if ( Bangle . isLCDOn ( ) ) {
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 ;
2022-04-22 11:06:42 +00:00
}
2020-05-28 13:38:46 +00:00
}
2020-05-27 05:50:48 +00:00
2022-06-11 07:00:47 +00:00
if ( now >= nextAlarmDate ) {
// The alarm widget should handle this one
addLog ( now , "alarm" ) ;
setTimeout ( load , 1000 ) ;
} else if ( measure && now >= minAlarm && swest _last === false ) {
addLog ( now , "alarm" ) ;
buzz ( ) ;
measure = false ;
if ( config . settings . disableAlarm ) {
// disable alarm for scheduler
nextAlarmConfig . last = now . getDate ( ) ;
require ( "Storage" ) . writeJSON ( "sched.json" , alarms ) ;
}
}
} ) ;
} ;
2020-05-28 13:38:46 +00:00
drawApp ( ) ;
2022-06-11 07:00:47 +00:00
if ( config . settings . startBeforeAlarm === 0 ) {
// Start immediately
run ( ) ;
} else {
// defer start
layout . state . label = "Deferred" ;
layout . render ( ) ;
const diff = nextAlarmDate - Date . now ( ) ;
let timeout = diff - config . settings . startBeforeAlarm * 60 * 60 * 1000 ;
if ( timeout < 0 ) timeout = 0 ;
setTimeout ( run , timeout ) ;
}
2020-05-27 05:50:48 +00:00
} else {
2020-05-28 13:38:46 +00:00
E . showMessage ( 'No Alarm' ) ;
setTimeout ( load , 1000 ) ;
2020-05-27 05:50:48 +00:00
}
2022-01-24 23:17:43 +00:00
setWatch ( ( ) => load ( ) , BANGLEJS2 ? BTN : BTN3 , { repeat : false , edge : "falling" } ) ;