mirror of https://github.com/espruino/BangleApps
sleeplog: New power saving mode using build in movement detection
* New power saving mode using build in movement detection Update app.js - add displaying powersaving status Update boot.js - add power save mode - minimize fix #1425 to issue #1423 - minimize wake/sleep decision - add checking for correct this reference - add checking for global object existence - remove unneeded global. prefix Update lib.js - add powersaving setting to setEnabled() - fix missing logfile issue #1423 on all functions - remove check for changes in setEnabled(...) to always restart the service Update settings.js - add settings powersaving and maxmove - add displaying settings depending on power saving mode - restart service when changing enabled, logfile and powersavingpull/1448/head
parent
405de6d6c0
commit
b8721fbdcf
|
@ -149,8 +149,8 @@ function drawNightTo(prevDays) {
|
|||
|
||||
// reduce date by 1s to ensure correct headline
|
||||
date = Date(date.valueOf() - 1E3);
|
||||
// draw headline, on red bg if service or loggging disabled
|
||||
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? g.theme.bg : 63488);
|
||||
// draw headline, on red bg if service or loggging disabled or green bg if powersaving enabled
|
||||
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
|
||||
g.fillRect(0, 30, width, 66).reset();
|
||||
g.setFont("12x20").setFontAlign(0, -1);
|
||||
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +
|
||||
|
|
|
@ -2,82 +2,36 @@
|
|||
// 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
|
||||
// sleeplog.status values: undefined = service stopped, 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
|
||||
powersaving: false, // disables ESS and uses build in movement detection
|
||||
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
|
||||
maxmove: 44, // movement threshold on power saving mode
|
||||
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]);
|
||||
["breaktod", "maxawake", "minconsec"].forEach(property => delete sleeplog[property]);
|
||||
|
||||
// check if service enabled
|
||||
if (global.sleeplog.enabled) {
|
||||
if (sleeplog.enabled) {
|
||||
|
||||
// add cached values and functions to global object
|
||||
global.sleeplog = Object.assign(global.sleeplog, {
|
||||
// add always used values and functions to global object
|
||||
sleeplog = Object.assign(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
status: undefined,
|
||||
|
||||
// define logging function
|
||||
log: function(date, status, temperature, info) {
|
||||
// exit on wrong this
|
||||
if (this.enabled === undefined) return;
|
||||
// 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
|
||||
|
@ -90,10 +44,8 @@ if (global.sleeplog.enabled) {
|
|||
var storage = require("Storage");
|
||||
|
||||
// read previous logfile
|
||||
var logContent = storage.read(this.logfile) || "";
|
||||
|
||||
// parse previous logfile
|
||||
var log = JSON.parse(logContent.length > 0 ? atob(logContent) : "[]") ;
|
||||
var log = storage.read(this.logfile) || "";
|
||||
log = log ? JSON.parse(atob(log)) || [] : [];
|
||||
|
||||
// remove last state if it was unknown and is less then 10min ago
|
||||
if (log.length > 0 && log[0][1] === 0 &&
|
||||
|
@ -113,28 +65,123 @@ if (global.sleeplog.enabled) {
|
|||
|
||||
// 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);
|
||||
// remove all listeners
|
||||
Bangle.removeListener('accel', sleeplog.accel);
|
||||
Bangle.removeListener('health', sleeplog.health);
|
||||
E.removeListener('kill', sleeplog.stop);
|
||||
// exit on missing global object
|
||||
if (!global.sleeplog) return;
|
||||
// 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;
|
||||
sleeplog.log(Math.floor(Date.now()));
|
||||
// reset always used cached values
|
||||
sleeplog.resting = undefined;
|
||||
sleeplog.status = undefined;
|
||||
sleeplog.ess_values = [];
|
||||
sleeplog.nomocount = 0;
|
||||
sleeplog.firstnomodate = undefined;
|
||||
},
|
||||
|
||||
// define restart function (also use for initial starting)
|
||||
start: function() {
|
||||
// add acceleration listener
|
||||
Bangle.on('accel', global.sleeplog.accel);
|
||||
// exit on missing global object
|
||||
if (!global.sleeplog) return;
|
||||
// add health listener if defined and
|
||||
if (sleeplog.health) Bangle.on('health', sleeplog.health);
|
||||
// add acceleration listener if defined and set status to unknown
|
||||
if (sleeplog.accel) Bangle.on('accel', sleeplog.accel);
|
||||
// add kill listener
|
||||
E.on('kill', global.sleeplog.stop);
|
||||
},
|
||||
E.on('kill', sleeplog.stop);
|
||||
// set status to unknown
|
||||
sleeplog.status = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// check for power saving mode
|
||||
if (sleeplog.powersaving) {
|
||||
// power saving mode using build in movement detection
|
||||
// add cached values and functions to global object
|
||||
sleeplog = Object.assign(sleeplog, {
|
||||
// define health listener function
|
||||
health: function(data) {
|
||||
// set global object and check for existence
|
||||
var gObj = global.sleeplog;
|
||||
if (!gObj) return;
|
||||
|
||||
// check for non-movement according to the threshold
|
||||
if (data.movement <= gObj.maxmove) {
|
||||
// check resting state
|
||||
if (gObj.resting !== true) {
|
||||
// change resting state
|
||||
gObj.resting = true;
|
||||
// set status to sleeping or worn
|
||||
gObj.status = E.getTemperature() > gObj.tempthresh ? 3 : 1;
|
||||
// write status to log, correct timestamp by health interval in ms
|
||||
gObj.log(Math.floor(Date.now() - 6E5), gObj.status, E.getTemperature());
|
||||
}
|
||||
} else {
|
||||
// check resting state
|
||||
if (gObj.resting !== false) {
|
||||
// change resting state, set status and write to log as awake
|
||||
gObj.resting = false;
|
||||
gObj.status = 2;
|
||||
gObj.log(Math.floor(Date.now()), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// full ESS calculation
|
||||
// add cached values and functions to global object
|
||||
sleeplog = Object.assign(sleeplog, {
|
||||
// set cached values
|
||||
ess_values: [],
|
||||
nomocount: 0,
|
||||
firstnomodate: undefined,
|
||||
|
||||
// define acceleration listener function
|
||||
accel: function(xyz) {
|
||||
// save acceleration magnitude and start calculation on enough saved data
|
||||
if (global.sleeplog && sleeplog.ess_values.push(xyz.mag) >= sleeplog.winwidth) sleeplog.calc();
|
||||
},
|
||||
|
||||
// define calculator function
|
||||
calc: function() {
|
||||
// exit on wrong this
|
||||
if (this.enabled === undefined) return;
|
||||
// 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
|
||||
this.resting = true;
|
||||
// set status to sleeping or worn
|
||||
this.status = E.getTemperature() > this.tempthresh ? 3 : 1;
|
||||
// write status to log, correct timestamp by health interval in ms
|
||||
this.log(this.firstnomodate, this.status, E.getTemperature());
|
||||
}
|
||||
} else {
|
||||
// reset non-movement sections count
|
||||
this.nomocount = 0;
|
||||
// check resting state
|
||||
if (this.resting !== false) {
|
||||
// change resting state, set status and write to log as awake
|
||||
this.resting = false;
|
||||
this.status = 2;
|
||||
this.log(Math.floor(Date.now()), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// initial starting
|
||||
global.sleeplog.start();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
exports = {
|
||||
// define en-/disable function
|
||||
setEnabled: function(enable, logfile) {
|
||||
// define en-/disable function, restarts the service to make changes take effect
|
||||
setEnabled: function(enable, logfile, powersaving) {
|
||||
// check if sleeplog is available
|
||||
if (typeof global.sleeplog !== "object") return;
|
||||
|
||||
|
@ -9,10 +9,6 @@ exports = {
|
|||
logfile === false ? undefined :
|
||||
"sleeplog.log";
|
||||
|
||||
// check if status needs to be changed
|
||||
if (enable === global.sleeplog.enabled ||
|
||||
logfile === global.sleeplog.logfile) return false;
|
||||
|
||||
// stop if enabled
|
||||
if (global.sleeplog.enabled) global.sleeplog.stop();
|
||||
|
||||
|
@ -23,7 +19,8 @@ exports = {
|
|||
// change enabled value in settings
|
||||
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
|
||||
enabled: enable,
|
||||
logfile: logfile
|
||||
logfile: logfile,
|
||||
powersaving: powersaving
|
||||
}));
|
||||
|
||||
// force changes to take effect by executing the boot script
|
||||
|
@ -50,7 +47,8 @@ exports = {
|
|||
if (since > Date()) return [];
|
||||
|
||||
// read log json to array
|
||||
var log = JSON.parse(atob(require("Storage").read(logfile)));
|
||||
var log = storage.read(this.logfile) || "";
|
||||
log = log ? JSON.parse(atob(log)) || [] : [];
|
||||
|
||||
// search for latest entry befor since
|
||||
since = (log.find(element => element[0] <= since) || [0])[0];
|
||||
|
@ -104,7 +102,8 @@ exports = {
|
|||
var storage = require("Storage");
|
||||
|
||||
// read log json to array
|
||||
var log = JSON.parse(atob(storage.read(logfile)));
|
||||
var log = storage.read(this.logfile) || "";
|
||||
log = log ? JSON.parse(atob(log)) || [] : [];
|
||||
|
||||
// define output variable to show number of changes
|
||||
var output = log.length;
|
||||
|
@ -125,7 +124,8 @@ exports = {
|
|||
var storage = require("Storage");
|
||||
|
||||
// read log json to array
|
||||
var log = JSON.parse(atob(storage.read(logfile)));
|
||||
var log = storage.read(this.logfile) || "";
|
||||
log = log ? JSON.parse(atob(log)) || [] : [];
|
||||
|
||||
// define output variable to show number of changes
|
||||
var output = 0;
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
maxawake: 36E5, // 60min in ms
|
||||
minconsec: 18E5, // 30min in ms
|
||||
tempthresh: 27, // every temperature above ist registered as worn
|
||||
powersaving: false, // disables ESS and uses build in movement detection
|
||||
maxmove: 44, // movement threshold on power saving mode
|
||||
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
|
||||
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
|
||||
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
|
||||
|
@ -32,14 +34,20 @@
|
|||
return value > max ? min : value < min ? max : value;
|
||||
}
|
||||
|
||||
// define function to change values that need a restart of the service
|
||||
function changeRestart() {
|
||||
require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving);
|
||||
}
|
||||
|
||||
// calculate sleepthresh factor
|
||||
var stFactor = settings.winwidth / 12.5 / 60;
|
||||
|
||||
// show main menu
|
||||
function showMain() {
|
||||
var mainMenu = E.showMenu({
|
||||
function showMain(selected) {
|
||||
var mainMenu = {
|
||||
"": {
|
||||
title: "Sleep Log"
|
||||
title: "Sleep Log",
|
||||
selected: selected
|
||||
},
|
||||
"< Exit": () => load(),
|
||||
"< Back": () => back(),
|
||||
|
@ -78,6 +86,23 @@
|
|||
writeSetting("tempthresh", v);
|
||||
}
|
||||
},
|
||||
"PowerSaving": {
|
||||
value: settings.powersaving,
|
||||
format: v => v ? "on" : "off",
|
||||
onchange: function(v) {
|
||||
settings.powersaving = v;
|
||||
changeRestart();
|
||||
showMain(7);
|
||||
}
|
||||
},
|
||||
"MaxMove": {
|
||||
value: settings.maxmove,
|
||||
step: 44,
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(40, 100, v);
|
||||
writeSetting("maxmove", v);
|
||||
}
|
||||
},
|
||||
"NoMoThresh": {
|
||||
value: settings.nomothresh,
|
||||
step: 0.001,
|
||||
|
@ -100,17 +125,32 @@
|
|||
value: settings.enabled,
|
||||
format: v => v ? "on" : "off",
|
||||
onchange: function(v) {
|
||||
writeSetting("enabled", v);
|
||||
settings.enabled = v;
|
||||
changeRestart();
|
||||
}
|
||||
},
|
||||
"Logfile ": {
|
||||
value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false,
|
||||
format: v => v === true ? "default" : v ? "custom" : "off",
|
||||
onchange: function(v) {
|
||||
if (v !== "custom") writeSetting("logfile", v ? "sleeplog.log" : undefined);
|
||||
if (v !== "custom") {
|
||||
settings.logfile = v ? "sleeplog.log" : undefined;
|
||||
changeRestart();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
// check power saving mode to delete unused entries
|
||||
(settings.powersaving ? ["NoMoThresh", "MinDuration"] : ["MaxMove"]).forEach(property => delete mainMenu[property]);
|
||||
var menu = E.showMenu(mainMenu);
|
||||
// workaround to display changed entries correct
|
||||
if (selected) setTimeout(_ => {
|
||||
menu.move(1);
|
||||
menu.move(1);
|
||||
menu.move(-1);
|
||||
menu.move(-1);
|
||||
menu.move(-1);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// draw main menu
|
||||
|
|
Loading…
Reference in New Issue