forked from FOSS/BangleApps
138 lines
5.3 KiB
JavaScript
138 lines
5.3 KiB
JavaScript
|
// 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
|
||
|
|
||
|
// sleeplog.status values: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
||
|
|
||
|
// load settings into global object
|
||
|
global.sleeplog = Object.assign({
|
||
|
enabled: true, // en-/disable completely
|
||
|
logfile: "sleeplog.log", // logfile
|
||
|
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
|
||
|
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
|
||
|
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
|
||
|
tempthresh: 27, // every temperature above ist registered as worn
|
||
|
}, require("Storage").readJSON("sleeplog.json", true) || {});
|
||
|
|
||
|
// delete app settings
|
||
|
["breaktod", "maxawake", "minconsec"].forEach(property => delete global.sleeplog[property]);
|
||
|
|
||
|
// check if service enabled
|
||
|
if (global.sleeplog.enabled) {
|
||
|
|
||
|
// add cached values and functions to global object
|
||
|
global.sleeplog = Object.assign(global.sleeplog, {
|
||
|
// set cached values
|
||
|
ess_values: [],
|
||
|
nomocount: 0,
|
||
|
firstnomodate: undefined,
|
||
|
resting: undefined,
|
||
|
status: 0,
|
||
|
|
||
|
// define acceleration listener function
|
||
|
accel: function(xyz) {
|
||
|
// save acceleration magnitude and start calculation on enough saved data
|
||
|
if (global.sleeplog.ess_values.push(xyz.mag) >= global.sleeplog.winwidth) global.sleeplog.calc();
|
||
|
},
|
||
|
|
||
|
// define calculator function
|
||
|
calc: function() {
|
||
|
// calculate standard deviation over
|
||
|
var mean = this.ess_values.reduce((prev, cur) => cur + prev) / this.winwidth;
|
||
|
var stddev = Math.sqrt(this.ess_values.map(val => Math.pow(val - mean, 2)).reduce((prev, cur) => prev + cur) / this.winwidth);
|
||
|
// reset saved acceleration data
|
||
|
this.ess_values = [];
|
||
|
|
||
|
// check for non-movement according to the threshold
|
||
|
if (stddev < this.nomothresh) {
|
||
|
// increment non-movement sections count, set date of first non-movement
|
||
|
if (++this.nomocount == 1) this.firstnomodate = Math.floor(Date.now());
|
||
|
// check resting state and non-movement count against threshold
|
||
|
if (this.resting !== true && this.nomocount >= this.sleepthresh) {
|
||
|
// change resting state, status and write to log
|
||
|
this.resting = true;
|
||
|
// check if the watch is worn
|
||
|
if (E.getTemperature() > this.tempthresh) {
|
||
|
// set status and write to log as sleping
|
||
|
this.status = 3;
|
||
|
this.log(this.firstnomodate, 3, E.getTemperature());
|
||
|
} else {
|
||
|
// set status and write to log as not worn
|
||
|
this.status = 1;
|
||
|
this.log(this.firstnomodate, 1, E.getTemperature());
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// reset non-movement sections count
|
||
|
this.nomocount = 0;
|
||
|
// check resting state
|
||
|
if (this.resting !== false) {
|
||
|
// change resting state
|
||
|
this.resting = false;
|
||
|
// set status and write to log as awake
|
||
|
this.status = 2;
|
||
|
this.log(Math.floor(Date.now()), 2);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// define logging function
|
||
|
log: function(date, status, temperature, info) {
|
||
|
// skip logging if logfile is undefined or does not end with ".log"
|
||
|
if (!this.logfile || !this.logfile.endsWith(".log")) return;
|
||
|
// prevent logging on implausible date
|
||
|
if (date < 9E11 || Date() < 9E11) return;
|
||
|
|
||
|
// set default value for status
|
||
|
status = status || 0;
|
||
|
|
||
|
// define storage
|
||
|
var storage = require("Storage");
|
||
|
|
||
|
// read previous logfile
|
||
|
var log = JSON.parse(atob(storage.read(this.logfile)));
|
||
|
|
||
|
// remove last state if it was unknown and is less then 10min ago
|
||
|
if (log.length > 0 && log[0][1] === 0 &&
|
||
|
Math.floor(Date.now()) - log[0][0] < 600000) log.shift();
|
||
|
|
||
|
// add actual status at the first position if it has changed
|
||
|
if (log.length === 0 || log[0][1] !== status)
|
||
|
log.unshift(info ? [date, status, temperature, info] : temperature ? [date, status, temperature] : [date, status]);
|
||
|
|
||
|
// write log to storage
|
||
|
storage.write(this.logfile, btoa(JSON.stringify(log)));
|
||
|
|
||
|
// clear variables
|
||
|
log = undefined;
|
||
|
storage = undefined;
|
||
|
},
|
||
|
|
||
|
// define stop function (logging will restart if enabled and boot file is executed)
|
||
|
stop: function() {
|
||
|
// remove acceleration and kill listener
|
||
|
Bangle.removeListener('accel', global.sleeplog.accel);
|
||
|
E.removeListener('kill', global.sleeplog.stop);
|
||
|
// write log with undefined sleeping status
|
||
|
global.sleeplog.log(Math.floor(Date.now()));
|
||
|
// reset cached values
|
||
|
global.sleeplog.ess_values = [];
|
||
|
global.sleeplog.nomocount = 0;
|
||
|
global.sleeplog.firstnomodate = undefined;
|
||
|
global.sleeplog.resting = undefined;
|
||
|
global.sleeplog.status = 0;
|
||
|
},
|
||
|
|
||
|
// define restart function (also use for initial starting)
|
||
|
start: function() {
|
||
|
// add acceleration listener
|
||
|
Bangle.on('accel', global.sleeplog.accel);
|
||
|
// add kill listener
|
||
|
E.on('kill', global.sleeplog.stop);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
// initial starting
|
||
|
global.sleeplog.start();
|
||
|
}
|