From b8721fbdcfe56c6e1d269e3ad00bb8e08837089a Mon Sep 17 00:00:00 2001 From: storm64 Date: Sat, 12 Feb 2022 01:43:58 +0100 Subject: [PATCH] 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 powersaving --- apps/sleeplog/app.js | 4 +- apps/sleeplog/boot.js | 195 +++++++++++++++++++++++--------------- apps/sleeplog/lib.js | 20 ++-- apps/sleeplog/settings.js | 54 +++++++++-- 4 files changed, 180 insertions(+), 93 deletions(-) diff --git a/apps/sleeplog/app.js b/apps/sleeplog/app.js index cbfad4bda..19ef52ef8 100644 --- a/apps/sleeplog/app.js +++ b/apps/sleeplog/app.js @@ -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" + diff --git a/apps/sleeplog/boot.js b/apps/sleeplog/boot.js index 883e497a5..1e52497a3 100644 --- a/apps/sleeplog/boot.js +++ b/apps/sleeplog/boot.js @@ -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(); } diff --git a/apps/sleeplog/lib.js b/apps/sleeplog/lib.js index 81bca0f3f..8db3e8600 100644 --- a/apps/sleeplog/lib.js +++ b/apps/sleeplog/lib.js @@ -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; diff --git a/apps/sleeplog/settings.js b/apps/sleeplog/settings.js index 6137e2082..09d2eb28e 100644 --- a/apps/sleeplog/settings.js +++ b/apps/sleeplog/settings.js @@ -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