Merge pull request #1448 from storm64/master

sleeplog: Add power saving mode, fix #1445
master
Gordon Williams 2022-02-14 09:45:12 +00:00 committed by GitHub
commit 7b72606ac4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 316 additions and 185 deletions

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Fix crash on start 0.02: Fix crash on start
0.03: Added power saving mode, move all read/write log actions into lib/module, fix #1445

View File

@ -2,20 +2,26 @@
This app logs and displays the four following states: This app logs and displays the four following states:
_unknown, not worn, awake, sleeping_ _unknown, not worn, awake, sleeping_
It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm) and uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments ([ESS](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en)) and the internal temperature to decide _sleeping_ or _not worn_ when the watch is resting. It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm) and uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments ([ESS](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en)) and
also provides a power saving mode using the built in movement calculation. The internal temperature is used to decide if the status is _sleeping_ or _not worn_.
#### Operating Principle #### Operating Principle
* __ESS calculation__ * __ESS calculation__
The accelerometer polls values with 12.5Hz. On each poll the magnitude value is saved. When 13 values are collected, every 1.04 seconds, the standard deviation over this values is calculated. The accelerometer polls values with 12.5Hz. On each poll the magnitude value is saved. When 13 values are collected, every 1.04 seconds, the standard deviation over this values is calculated.
Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. The first time no movement is detected the actual timestamp is cached (in _sleeplog.firstnomodate_) for logging.
When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_) When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_)
To check if a resting watch indicates as sleeping, the internal temperature must be greater than the temperature threshold (__TempThresh__). Otherwise the watch is considered as not worn. * __Power Saving Mode__
On power saving mode the movement value of bangle's build in health event is checked against the maximal movement threshold (__MaxMove__). The event is only triggered every 10 minutes which decreases the battery impact but also reduces accurracy.
* ___Sleeping___ __or__ ___Not Worn___
To check if a resting watch indicates a sleeping status, the internal temperature must be greater than the temperature threshold (__TempThresh__). Otherwise the watch is considered as not worn.
* __True Sleep__ * __True Sleep__
The true sleep value is a simple addition of all registert sleeping periods. The true sleep value is a simple addition of all registert sleeping periods.
* __Consecutive Sleep__ * __Consecutive Sleep__
In addition the consecutive sleep value tries to predict the complete time you were asleep, even the light sleeping phases with registered movements. All periods after a sleeping period will be summarized til the first following non sleeping period that is longer then the maximal awake duration (__MaxAwake__). If this sum is lower than the minimal consecutive sleep duration (__MinConsec__) it is not considered, otherwise it will be added to the consecutive sleep value. In addition the consecutive sleep value tries to predict the complete time you were asleep, even the light sleeping phases with registered movements. All periods after a sleeping period will be summarized til the first following non sleeping period that is longer then the maximal awake duration (__MaxAwake__). If this sum is lower than the minimal consecutive sleep duration (__MinConsec__) it is not considered, otherwise it will be added to the consecutive sleep value.
* __Logging__ * __Logging__
To minimize the log size only a changed state is logged. To minimize the log size only a changed state is logged. The logged timestamp is matching the beginning of its measurement period.
When not on power saving mode a movement is detected nearly instantaneous and the detection of a no movement period is delayed by the minimal no movement duration. To match the beginning of the measurement period a cached timestamp (_sleeplog.firstnomodate_) is logged.
On power saving mode the measurement period is fixed to 10 minutes and all logged timestamps are also set back 10 minutes.
--- ---
### Control ### Control
@ -28,28 +34,37 @@ It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm
--- ---
### Settings ### Settings
--- ---
* __BreakTod__ break at time of day * __BreakTod__ | break at time of day
_0_ / _1_ / _..._ / __10__ / _..._ / _12_ _0_ / _1_ / _..._ / __10__ / _..._ / _12_
Change time of day on wich the lower graph starts and the upper graph ends. Change time of day on wich the lower graph starts and the upper graph ends.
* __MaxAwake__ maximal awake duration * __MaxAwake__ | maximal awake duration
_15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_ _15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_
Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period. Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period.
* __MinConsec__ minimal consecutive sleep duration * __MinConsec__ | minimal consecutive sleep duration
_15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_ _15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_
Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value. Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value.
* __TempThresh__ temperature threshold * __TempThresh__ | temperature threshold
_20°C_ / _20.5°C_ / _..._ / __25°C__ / _..._ / _40°C_ _20°C_ / _20.5°C_ / _..._ / __25°C__ / _..._ / _40°C_
The internal temperature must be greater than this threshold to log _sleeping_, otherwise it is _not worn_. The internal temperature must be greater than this threshold to log _sleeping_, otherwise it is _not worn_.
* __NoMoThresh__ no movement threshold * __PowerSaving__
_on_ / __off__
En-/Disable power saving mode. _Saves battery, but might decrease accurracy._
* __MaxMove__ | maximal movement threshold
(only available when on power saving mode)
_50_ / _51_ / _..._ / __100__ / _..._ / _200_
On power saving mode the watch is considered resting if this threshold is lower or equal to the movement value of bangle's health event.
* __NoMoThresh__ | no movement threshold
(only available when not on power saving mode)
_0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_ _0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_
The standard deviation over the measured values needs to be lower then this threshold to count as not moving. The standard deviation over the measured values needs to be lower then this threshold to count as not moving.
The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise. The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise.
* __MinDuration__ minimal no movement duration * __MinDuration__ | minimal no movement duration
(only available when not on power saving mode)
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_ _5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
If no movement is detected for this duration, the watch is considered as resting. If no movement is detected for this duration, the watch is considered as resting.
* __Enabled__ * __Enabled__
__on__ / _off_ __on__ / _off_
En-/Disable the service (all background activities). _Saves battery, but might make this app useless._ En-/Disable the service (all background activities). _Saves the most battery, but might make this app useless._
* __Logfile__ * __Logfile__
__default__ / _off_ __default__ / _off_
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_. En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
@ -65,8 +80,9 @@ For easy access from the console or other apps the following parameters, values
enabled: true, // bool / service status indicator enabled: true, // bool / service status indicator
logfile: "sleeplog.log", // string / used logfile logfile: "sleeplog.log", // string / used logfile
resting: false, // bool / indicates if the watch is resting resting: false, // bool / indicates if the watch is resting
status: 2, // int / actual status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping status: 2, // int / actual status:
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement / undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement, not available in power saving mode
stop: function () { ... }, // funct / stops the service until the next load() stop: function () { ... }, // funct / stops the service until the next load()
start: function () { ... }, // funct / restarts the service start: function () { ... }, // funct / restarts the service
... ...
@ -74,42 +90,54 @@ For easy access from the console or other apps the following parameters, values
>require("sleeplog") >require("sleeplog")
={ ={
setEnabled: function (enable, logfile) { ... }, setEnabled: function (enable, logfile, powersaving) { ... },
// en-/disable the service and/or logging // restarts the service with changed settings
// * enable / bool / service status to change to // * enable / bool / new service status
// * logfile / bool or string // * logfile / bool or string
// - true = enables logging to "sleeplog.log" // - true = enables logging to "sleeplog.log"
// - "some_file.log" = enables logging to "some_file.log" // - "some_file.log" = enables logging to "some_file.log"
// - false = disables logging // - false = disables logging
// returns: bool or undefined // * (powersaving) / bool / new power saving status, default: false
// - true = changes executed // returns: true or undefined
// - false = no changes needed // - true = service restart executed
// - undefined = no global.sleeplog found // - undefined = no global.sleeplog found
readLog: function (since, until) { ... }, readLog: function (logfile, since, until) { ... },
// read the raw log data for a specific time period // read the raw log data for a specific time period
// * since / Date or number / startpoint of period // * logfile / string / on no string uses logfile from global object or "sleeplog.log"
// * until / Date or number / endpoint of period // * (since) / Date or number / startpoint of period, default: 0
// * (until) / Date or number / endpoint of period, default: 1E14
// returns: array // returns: array
// * [[number, int, string], [...], ... ] / sorting: latest first // * [[number, int, string], [...], ... ] / sorting: latest first
// - number // timestamp in ms // - number // timestamp in ms
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping // - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// - string // additional information // - string // additional information
// * [] = no data available or global.sleeplog found // * [] = no data available or global.sleeplog not found
getReadableLog: function (printLog, since, until) { ... } writeLog: function (logfile, input) { ... },
// append or replace log depending on input
// * logfile / string / on no string uses logfile from global object or default
// * input / array
// - append input if array length >1 and element[0] >9E11
// - replace log with input if at least one entry like above is inside another array
// returns: true or undefined
// - true = changest written to storage
// - undefined = wrong input
getReadableLog: function (printLog, since, until, logfile) { ... }
// read the log data as humanreadable string for a specific time period // read the log data as humanreadable string for a specific time period
// * since / Date or number / startpoint of period // * (printLog) / bool / direct print output with additional information, default: false
// * until / Date or number / endpoint of period // * (since) / Date or number / see readLog(..)
// * (until) / Date or number / see readLog(..)
// * (logfile) / string / see readLog(..)
// returns: string // returns: string
// * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last // * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last
// * undefined = no data available or global.sleeplog found // * undefined = no data available or global.sleeplog found
restoreLog: function (logfile) { ... } restoreLog: function (logfile) { ... }
// eliminate some errors inside a specific logfile // eliminate some errors inside a specific logfile
// * logfile / string / name of the logfile that will be restored // * (logfile) / string / see readLog(..)
// returns: int / number of changes that were made // returns: int / number of changes that were made
reinterpretTemp: function (logfile, tempthresh) { ... } reinterpretTemp: function (logfile, tempthresh) { ... }
// reinterpret worn status based on given temperature threshold // reinterpret worn status based on given temperature threshold
// * logfile / string / name of the logfile // * (logfile) / string / see readLog(..)
// * tempthresh / float / new temperature threshold // * (tempthresh) / float / new temperature threshold, on default uses tempthresh from global object or 27
// returns: int / number of changes that were made // returns: int / number of changes that were made
} }
``` ```
@ -120,7 +148,9 @@ For easy access from the console or other apps the following parameters, values
#### To do list #### To do list
* Send the logged information to Gadgetbridge. * Send the logged information to Gadgetbridge.
_(For now I have no idea how to achieve this, help is appreciated.)_ _(For now I have no idea how to achieve this, help is appreciated.)_
* View, down- and upload log functions via App Loader.
* Calculate and display overall sleep statistics. * Calculate and display overall sleep statistics.
* Option to automatically change power saving mode depending on time of day.
#### Requests, Bugs and Feedback #### Requests, Bugs and Feedback
Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) or send me a [mail](mailto:banglejs@storm64.de). Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) or send me a [mail](mailto:banglejs@storm64.de).

View File

@ -25,7 +25,7 @@ function drawLog(topY, viewUntil) {
var y = topY + graphHeight; var y = topY + graphHeight;
// read 12h wide log // read 12h wide log
var log = require("sleeplog").readLog(timestamp0, viewUntil.valueOf()); var log = require("sleeplog").readLog(0, timestamp0, viewUntil.valueOf());
// format log array if not empty // format log array if not empty
if (log.length) { if (log.length) {
@ -149,8 +149,8 @@ function drawNightTo(prevDays) {
// reduce date by 1s to ensure correct headline // reduce date by 1s to ensure correct headline
date = Date(date.valueOf() - 1E3); date = Date(date.valueOf() - 1E3);
// draw headline, on red bg if service or loggging disabled // draw headline, on red bg if service or loggging disabled or green bg if powersaving enabled
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? g.theme.bg : 63488); g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
g.fillRect(0, 30, width, 66).reset(); g.fillRect(0, 30, width, 66).reset();
g.setFont("12x20").setFontAlign(0, -1); g.setFont("12x20").setFontAlign(0, -1);
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" + g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +

View File

@ -2,139 +2,161 @@
// 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. // 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 // 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 // load settings into global object
global.sleeplog = Object.assign({ global.sleeplog = Object.assign({
enabled: true, // en-/disable completely enabled: true, // en-/disable completely
logfile: "sleeplog.log", // logfile 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 winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
maxmove: 100, // movement threshold on power saving mode
tempthresh: 27, // every temperature above ist registered as worn tempthresh: 27, // every temperature above ist registered as worn
}, require("Storage").readJSON("sleeplog.json", true) || {}); }, require("Storage").readJSON("sleeplog.json", true) || {});
// delete app settings // delete app settings
["breaktod", "maxawake", "minconsec"].forEach(property => delete global.sleeplog[property]); ["breaktod", "maxawake", "minconsec"].forEach(property => delete sleeplog[property]);
// check if service enabled // check if service enabled
if (global.sleeplog.enabled) { if (sleeplog.enabled) {
// add cached values and functions to global object // add always used values and functions to global object
global.sleeplog = Object.assign(global.sleeplog, { sleeplog = Object.assign(sleeplog, {
// set cached values // set cached values
ess_values: [],
nomocount: 0,
firstnomodate: undefined,
resting: undefined, resting: undefined,
status: 0, status: undefined,
// 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 logContent = storage.read(this.logfile) || "";
// parse previous logfile
var log = JSON.parse(logContent.length > 0 ? atob(logContent) : "[]") ;
// 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) // define stop function (logging will restart if enabled and boot file is executed)
stop: function() { stop: function() {
// remove acceleration and kill listener // remove all listeners
Bangle.removeListener('accel', global.sleeplog.accel); Bangle.removeListener('accel', sleeplog.accel);
E.removeListener('kill', global.sleeplog.stop); 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 // write log with undefined sleeping status
global.sleeplog.log(Math.floor(Date.now())); require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
// reset cached values // reset always used cached values
global.sleeplog.ess_values = []; sleeplog.resting = undefined;
global.sleeplog.nomocount = 0; sleeplog.status = undefined;
global.sleeplog.firstnomodate = undefined; sleeplog.ess_values = [];
global.sleeplog.resting = undefined; sleeplog.nomocount = 0;
global.sleeplog.status = 0; sleeplog.firstnomodate = undefined;
}, },
// define restart function (also use for initial starting) // define restart function (also use for initial starting)
start: function() { start: function() {
// add acceleration listener // exit on missing global object
Bangle.on('accel', global.sleeplog.accel); 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 // add kill listener
E.on('kill', global.sleeplog.stop); E.on('kill', () => sleeplog.stop());
}, // read log since 5min ago and restore status to last known state or unknown
sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1]
// update resting according to status
sleeplog.resting = sleeplog.status % 2;
// write restored status to log
require("sleeplog").writeLog(0, [Math.floor(Date.now()), sleeplog.status]);
}
}); });
// check for power saving mode
if (sleeplog.powersaving) {
// power saving mode using build in movement detection
// delete unused settings
["winwidth", "nomothresh", "sleepthresh"].forEach(property => delete sleeplog[property]);
// 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;
// calculate timestamp for this measurement
var timestamp = Math.floor(Date.now() - 6E5);
// 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,
require("sleeplog").writeLog(0, [timestamp, gObj.status, E.getTemperature()]);
}
} else {
// check resting state
if (gObj.resting !== false) {
// change resting state, set status and write status to log
gObj.resting = false;
gObj.status = 2;
require("sleeplog").writeLog(0, [timestamp, 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, with first no movement timestamp
require("sleeplog").writeLog(0, [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 and set status
this.resting = false;
this.status = 2;
// write status to log
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 2]);
}
}
}
});
}
// initial starting // initial starting
global.sleeplog.start(); global.sleeplog.start();
} }

View File

@ -1,17 +1,12 @@
exports = { exports = {
// define en-/disable function // define en-/disable function, restarts the service to make changes take effect
setEnabled: function(enable, logfile) { setEnabled: function(enable, logfile, powersaving) {
// check if sleeplog is available // check if sleeplog is available
if (typeof global.sleeplog !== "object") return; if (typeof global.sleeplog !== "object") return;
// set default logfile // set default logfile
logfile = logfile.endsWith(".log") ? logfile : logfile = (typeof logfile === "string" && logfile.endsWith(".log")) ? logfile :
logfile === false ? undefined : logfile === false ? undefined : "sleeplog.log";
"sleeplog.log";
// check if status needs to be changed
if (enable === global.sleeplog.enabled ||
logfile === global.sleeplog.logfile) return false;
// stop if enabled // stop if enabled
if (global.sleeplog.enabled) global.sleeplog.stop(); if (global.sleeplog.enabled) global.sleeplog.stop();
@ -23,7 +18,8 @@ exports = {
// change enabled value in settings // change enabled value in settings
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, { storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
enabled: enable, enabled: enable,
logfile: logfile logfile: logfile,
powersaving: powersaving || false
})); }));
// force changes to take effect by executing the boot script // force changes to take effect by executing the boot script
@ -42,32 +38,77 @@ exports = {
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping // - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// - float // internal temperature // - float // internal temperature
// - string // additional information // - string // additional information
readLog: function(since, until) { readLog: function(logfile, since, until) {
// set logfile // check/set logfile
var logfile = (global.sleeplog || {}).logfile || "sleeplog.log"; logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
(global.sleeplog || {}).logfile || "sleeplog.log";
// check if since is in the future // check if since is in the future
if (since > Date()) return []; if (since > Date()) return [];
// read log json to array // read logfile
var log = JSON.parse(atob(require("Storage").read(logfile))); var log = require("Storage").read(logfile);
// return empty log
if (!log) return [];
// decode data if needed
if (log[0] !== "[") log = atob(log);
// do a simple check before parsing
if (!log.startsWith("[[") || !log.endsWith("]]")) return [];
log = JSON.parse(log) || [];
// search for latest entry befor since // check if filtering is needed
since = (log.find(element => element[0] <= since) || [0])[0]; if (since || until) {
// search for latest entry befor since
// filter selected time period if (since) since = (log.find(element => element[0] <= since) || [0])[0];
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14))); // filter selected time period
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
}
// output log // output log
return log; return log;
}, },
// define write log function, append or replace log depending on input
// append input if array length >1 and element[0] >9E11
// replace log with input if at least one entry like above is inside another array
writeLog: function(logfile, input) {
// check/set logfile
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
(global.sleeplog || {}).logfile || "sleeplog.log";
// check if input is an array
if (typeof input !== "object" || typeof input.length !== "number") return;
// check for entry plausibility
if (input.length > 1 && input[0] * 1 > 9E11) {
// read log
var log = this.readLog(logfile);
// remove last state if it was unknown and less then 5min ago
if (log.length > 0 && log[0][1] === 0 &&
Math.floor(Date.now()) - log[0][0] < 3E5) log.shift();
// add entry at the first position if it has changed
if (log.length === 0 || input.some((e, index) => index > 0 && input[index] !== log[0][index])) log.unshift(input);
// map log as input
input = log;
}
// simple check for log plausibility
if (input[0].length > 1 && input[0][0] * 1 > 9E11) {
// write log to storage
require("Storage").write(logfile, btoa(JSON.stringify(input)));
return true;
}
},
// define log to humanreadable string function // define log to humanreadable string function
// sorting: latest last, format: // sorting: latest last, format:
// "{substring of ISO date} - {status} for {duration}min\n..." // "{substring of ISO date} - {status} for {duration}min\n..."
getReadableLog: function(printLog, since, until) { getReadableLog: function(printLog, since, until, logfile) {
// read log and check // read log and check
var log = this.readLog(since, until); var log = this.readLog(logfile, since, until);
if (!log.length) return; if (!log.length) return;
// reverse array to set last timestamp to the end // reverse array to set last timestamp to the end
log.reverse(); log.reverse();
@ -81,8 +122,11 @@ exports = {
logString[index] = "" + logString[index] = "" +
Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " + Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
statusText[element[1]] + statusText[element[1]] +
(index === log.length - 1 ? "" : " for " + Math.round((log[index + 1][0] - element[0]) / 60000) + "min") + (index === log.length - 1 ?
(element[2] ? " | Temp: " + element[2] + "°C" : "") + element.length < 3 ? "" : " ".repeat(12) :
" for " + ("" + Math.round((log[index + 1][0] - element[0]) / 60000)).padStart(4) + "min"
) +
(element[2] ? " | Temp: " + ("" + element[2]).padEnd(5) + "°C" : "") +
(element[3] ? " | " + element[3] : ""); (element[3] ? " | " + element[3] : "");
}); });
logString = logString.join("\n"); logString = logString.join("\n");
@ -100,11 +144,9 @@ exports = {
// define function to eliminate some errors inside the log // define function to eliminate some errors inside the log
restoreLog: function(logfile) { restoreLog: function(logfile) {
// define storage // read log and check
var storage = require("Storage"); var log = this.readLog(logfile);
if (!log.length) return;
// read log json to array
var log = JSON.parse(atob(storage.read(logfile)));
// define output variable to show number of changes // define output variable to show number of changes
var output = log.length; var output = log.length;
@ -112,8 +154,8 @@ exports = {
// remove non decremental entries // remove non decremental entries
log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]); log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
// write log to storage // write log
storage.write(logfile, btoa(JSON.stringify(log))); this.writeLog(logfile, log);
// return difference in length // return difference in length
return output - log.length; return output - log.length;
@ -121,11 +163,12 @@ exports = {
// define function to reinterpret worn status based on given temperature threshold // define function to reinterpret worn status based on given temperature threshold
reinterpretTemp: function(logfile, tempthresh) { reinterpretTemp: function(logfile, tempthresh) {
// define storage // read log and check
var storage = require("Storage"); var log = this.readLog(logfile);
if (!log.length) return;
// read log json to array // set default tempthresh
var log = JSON.parse(atob(storage.read(logfile))); tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
// define output variable to show number of changes // define output variable to show number of changes
var output = 0; var output = 0;
@ -140,8 +183,8 @@ exports = {
return element; return element;
}); });
// write log to storage // write log
storage.write(logfile, btoa(JSON.stringify(log))); this.writeLog(logfile, log);
// return output // return output
return output; return output;

View File

@ -2,8 +2,8 @@
"id":"sleeplog", "id":"sleeplog",
"name":"Sleep Log", "name":"Sleep Log",
"shortName": "SleepLog", "shortName": "SleepLog",
"version": "0.02", "version": "0.03",
"description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS).", "description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",
"tags": "tool,boot", "tags": "tool,boot",

View File

@ -8,6 +8,8 @@
maxawake: 36E5, // 60min in ms maxawake: 36E5, // 60min in ms
minconsec: 18E5, // 30min in ms minconsec: 18E5, // 30min in ms
tempthresh: 27, // every temperature above ist registered as worn tempthresh: 27, // every temperature above ist registered as worn
powersaving: false, // disables ESS and uses build in movement detection
maxmove: 100, // movement threshold on power saving mode
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
@ -32,14 +34,20 @@
return value > max ? min : value < min ? max : value; 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 // calculate sleepthresh factor
var stFactor = settings.winwidth / 12.5 / 60; var stFactor = settings.winwidth / 12.5 / 60;
// show main menu // show main menu
function showMain() { function showMain(selected) {
var mainMenu = E.showMenu({ var mainMenu = {
"": { "": {
title: "Sleep Log" title: "Sleep Log",
selected: selected
}, },
"< Exit": () => load(), "< Exit": () => load(),
"< Back": () => back(), "< Back": () => back(),
@ -78,6 +86,23 @@
writeSetting("tempthresh", v); 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: 1,
onchange: function(v) {
this.value = v = circulate(50, 200, v);
writeSetting("maxmove", v);
}
},
"NoMoThresh": { "NoMoThresh": {
value: settings.nomothresh, value: settings.nomothresh,
step: 0.001, step: 0.001,
@ -100,17 +125,27 @@
value: settings.enabled, value: settings.enabled,
format: v => v ? "on" : "off", format: v => v ? "on" : "off",
onchange: function(v) { onchange: function(v) {
writeSetting("enabled", v); settings.enabled = v;
changeRestart();
} }
}, },
"Logfile ": { "Logfile ": {
value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false, value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false,
format: v => v === true ? "default" : v ? "custom" : "off", format: v => v === true ? "default" : v ? "custom" : "off",
onchange: function(v) { onchange: function(v) {
if (v !== "custom") writeSetting("logfile", v ? "sleeplog.log" : undefined); if (v !== "custom") {
settings.logfile = v ? "sleeplog.log" : false;
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
// https://github.com/espruino/Espruino/issues/2149
if (selected) setTimeout(m => m.draw(), 1, menu);
} }
// draw main menu // draw main menu