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 ) || { } ) ;
2023-01-27 16:59:07 +00:00
const active = alarms . filter ( alarm => require ( "sched" ) . getTimeToAlarm ( alarm ) ) ;
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 = [ ] ;
2023-02-03 17:03:44 +00:00
let drawTimeTimeout ;
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
2022-11-30 08:34:17 +00:00
const winwidth = 13 ; // Actually 12.5 Hz, rounded
const nomothresh = 0.023 ; // Original implementation: 6, resolution 11 bit, scale +-4G = 6/(2^(11-1))*4 = 0.023438 in G
2020-05-27 05:50:48 +00:00
const sleepthresh = 600 ;
var ess _values = [ ] ;
var slsnds = 0 ;
2023-02-03 17:03:44 +00:00
function calc _ess ( acc _magn ) { "ram"
2022-04-09 09:22:51 +00:00
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-11-30 08:34:17 +00:00
const LABEL _ETA = /*LANG*/ "ETA" ;
const LABEL _WAKEUP _TIME = /*LANG*/ "Alarm at" ;
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-11-30 08:34:17 +00:00
layout . alarm _date . label = ` ${ LABEL _WAKEUP _TIME } : ${ alarmHour } : ${ alarmMinute } ` ;
2022-04-27 19:22:33 +00:00
layout . render ( ) ;
2020-05-27 05:50:48 +00:00
2023-02-03 17:03:44 +00:00
function drawTime ( ) { "ram"
const drawSeconds = ! Bangle . isLocked ( ) ;
2020-05-28 13:38:46 +00:00
if ( Bangle . isLCDOn ( ) ) {
const now = new Date ( ) ;
2023-02-03 17:03:44 +00:00
layout . date . label = locale . time ( now , ! drawSeconds ) ; // 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-11-30 08:34:17 +00:00
layout . eta . label = ` ${ LABEL _ETA } : ${ diffHour } : ${ diffMinutes . padStart ( 2 , '0' ) } ` ;
2022-04-27 19:22:33 +00:00
layout . render ( ) ;
2020-05-28 13:38:46 +00:00
}
2022-06-12 08:39:48 +00:00
2023-02-03 17:03:44 +00:00
const period = drawSeconds ? 1000 : 60000 ;
2024-03-20 09:09:25 +00:00
if ( drawTimeTimeout !== undefined ) {
clearTimeout ( drawTimeTimeout ) ;
2023-02-03 17:03:44 +00:00
}
drawTimeTimeout = setTimeout ( ( ) => {
drawTimeTimeout = undefined ;
2022-06-12 08:39:48 +00:00
drawTime ( ) ;
2023-02-03 17:03:44 +00:00
} , period - ( Date . now ( ) % period ) ) ;
2020-05-28 13:38:46 +00:00
}
2020-05-27 05:50:48 +00:00
2023-02-03 17:03:44 +00:00
Bangle . on ( 'lock' , function ( on ) {
if ( on === false ) {
drawTime ( ) ;
}
} ) ;
2022-04-27 19:22:33 +00:00
drawTime ( ) ;
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 ) {
2024-03-18 18:35:07 +00:00
logs . push ( { time : time . toISOString ( ) , 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 ) {
2023-02-04 08:53:05 +00:00
const logday = BANGLEJS2 ? nextAlarmDate . getDate ( ) : 0 ;
config . logs [ logday ] = [ ] ; // overwrite log on each day of month
logs = config . logs [ logday ] ;
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 ) ) ;
2024-03-20 09:09:25 +00:00
let run = ( ) => {
2022-11-30 08:34:17 +00:00
layout . state . label = /*LANG*/ "Start" ;
2022-06-11 07:00:47 +00:00
layout . render ( ) ;
2022-06-14 18:29:10 +00:00
Bangle . setOptions ( { powerSave : false } ) ; // do not dynamically change accelerometer poll interval
Bangle . setPollInterval ( 80 ) ; // 12.5Hz
2023-02-03 17:03:44 +00:00
Bangle . on ( 'accel' , ( accelData ) => { "ram"
2022-06-11 07:00:47 +00:00
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 ( ) ) {
2022-11-30 08:34:17 +00:00
layout . state . label = swest ? /*LANG*/ "Sleep" : /*LANG*/ "Awake" ;
2022-06-11 07:00:47 +00:00
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 ) ;
2022-11-01 05:56:09 +00:00
} else if ( measure && now >= minAlarm && swest === false ) {
2022-06-11 07:00:47 +00:00
addLog ( now , "alarm" ) ;
2022-12-06 10:45:50 +00:00
measure = false ;
2022-12-05 17:06:17 +00:00
if ( nextAlarmConfig . js ) {
eval ( nextAlarmConfig . js ) ; // run nextAlarmConfig.js if set
2022-12-06 10:45:50 +00:00
} else {
buzz ( ) ;
if ( config . settings . disableAlarm ) {
// disable alarm for scheduler
nextAlarmConfig . last = now . getDate ( ) ;
require ( 'Storage' ) . writeJSON ( 'sched.json' , alarms ) ;
}
2022-12-06 10:48:02 +00:00
}
2022-06-11 07:00:47 +00:00
}
} ) ;
} ;
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" } ) ;