1
0
Fork 0

sleeplog: Complete power saving mode + move all read/write log actions into lib.js

* Complete new power saving mode after tests
* Move all read/write log operations into lib/module
Update app.js
 - update `readLog(...)` call
Update boot.js
 - update maxmove default value
 - delete `log(..)`, merging into `writeLog(...)` from lib.js
 - replace all read/write actions to the logfile with `readLog(...)`/`writeLog(...)` from lib.js
 - add restoring the status after a restart (<5 min ago) to last known state
 - add deletion of unused settings on power saving mode
 - update log timestamp on power saving mode to always be 10min before now
Update ChangeLog
Update lib.js
 - update `readLog(...)`, prevent errors on reading, minimize workload if unfiltered
 - add `writeLog(...)` to append or replace log depending on input with plausibility checks on input
 - merging `log(...)` from boot.js into `writeLog(...)`
 - replace all read/write actions to the logfile with `readLog(...)`/`writeLog(...)`
Update metadata.json
 - update version number
 - add power saving mode to description
Update README.md
 - add power saving mode description
 - move sleeping/not worn decision description into its own section
 - add description for timestamp values to Logging section
 - update Global Object and Module Functions section
Update settings.js
 - update maxmove setting and default value
master
storm64 2022-02-13 20:54:26 +01:00
parent b8721fbdcf
commit 0ec1d57add
7 changed files with 160 additions and 116 deletions

View File

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

View File

@ -2,20 +2,26 @@
This app logs and displays the four following states:
_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
* __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.
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.
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.
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_)
* __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__
The true sleep value is a simple addition of all registert sleeping periods.
* __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.
* __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
@ -28,28 +34,34 @@ It derived from the [SleepPhaseAlarm](https://banglejs.com/apps/#sleepphasealarm
---
### Settings
---
* __BreakTod__ break at time of day
* __BreakTod__ | break at time of day
_0_ / _1_ / _..._ / __10__ / _..._ / _12_
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_
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_
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_
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_
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.
* __MinDuration__ minimal no movement duration
* __MinDuration__ | minimal no movement duration | only available when not on power saving mode
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
If no movement is detected for this duration, the watch is considered as resting.
* __Enabled__
__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__
__default__ / _off_
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
@ -66,7 +78,7 @@ For easy access from the console or other apps the following parameters, values
logfile: "sleeplog.log", // string / used logfile
resting: false, // bool / indicates if the watch is resting
status: 2, // int / actual status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement
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()
start: function () { ... }, // funct / restarts the service
...
@ -74,42 +86,54 @@ For easy access from the console or other apps the following parameters, values
>require("sleeplog")
={
setEnabled: function (enable, logfile) { ... },
// en-/disable the service and/or logging
// * enable / bool / service status to change to
// * logfile / bool or string
setEnabled: function (enable, logfile, powersaving) { ... },
// restarts the service with changed settings
// * enable / bool / new service status
// * logfile / bool or string
// - true = enables logging to "sleeplog.log"
// - "some_file.log" = enables logging to "some_file.log"
// - false = disables logging
// returns: bool or undefined
// - true = changes executed
// - false = no changes needed
// * (powersaving) / bool / new power saving status, default: false
// returns: true or undefined
// - true = service restart executed
// - undefined = no global.sleeplog found
readLog: function (since, until) { ... },
readLog: function (logfile, since, until) { ... },
// read the raw log data for a specific time period
// * since / Date or number / startpoint of period
// * until / Date or number / endpoint of period
// * logfile / string / on no string uses logfile from global object or "sleeplog.log"
// * (since) / Date or number / startpoint of period, default: 0
// * (until) / Date or number / endpoint of period, default: 1E14
// returns: array
// * [[number, int, string], [...], ... ] / sorting: latest first
// - number // timestamp in ms
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// - string // additional information
// * [] = no data available or global.sleeplog found
getReadableLog: function (printLog, since, until) { ... }
// * [] = no data available or global.sleeplog not found
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
// * since / Date or number / startpoint of period
// * until / Date or number / endpoint of period
// * (printLog) / bool / direct print output with additional information, default: false
// * (since) / Date or number / see readLog(..)
// * (until) / Date or number / see readLog(..)
// * (logfile) / string / see readLog(..)
// returns: string
// * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last
// * undefined = no data available or global.sleeplog found
restoreLog: function (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
reinterpretTemp: function (logfile, tempthresh) { ... }
// reinterpret worn status based on given temperature threshold
// * logfile / string / name of the logfile
// * tempthresh / float / new temperature threshold
// * (logfile) / string / see readLog(..)
// * (tempthresh) / float / new temperature threshold, on default uses tempthresh from global object or 27
// returns: int / number of changes that were made
}
```

View File

@ -25,7 +25,7 @@ function drawLog(topY, viewUntil) {
var y = topY + graphHeight;
// 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
if (log.length) {

View File

@ -12,7 +12,7 @@ global.sleeplog = Object.assign({
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
maxmove: 100, // movement threshold on power saving mode
tempthresh: 27, // every temperature above ist registered as worn
}, require("Storage").readJSON("sleeplog.json", true) || {});
@ -28,41 +28,6 @@ if (sleeplog.enabled) {
resting: undefined,
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
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 = 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 &&
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 all listeners
@ -72,7 +37,7 @@ if (sleeplog.enabled) {
// exit on missing global object
if (!global.sleeplog) return;
// write log with undefined sleeping status
sleeplog.log(Math.floor(Date.now()));
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
// reset always used cached values
sleeplog.resting = undefined;
sleeplog.status = undefined;
@ -91,14 +56,20 @@ if (sleeplog.enabled) {
if (sleeplog.accel) Bangle.on('accel', sleeplog.accel);
// add kill listener
E.on('kill', sleeplog.stop);
// set status to unknown
sleeplog.status = 0;
// 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
@ -107,24 +78,27 @@ if (sleeplog.enabled) {
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) {
if (true || gObj.resting !== true) { // log always for testing
// 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());
// write status to log,
require("sleeplog").writeLog(0, [timestamp, gObj.status, E.getTemperature(), data.movement]);
}
} else {
// check resting state
if (gObj.resting !== false) {
// change resting state, set status and write to log as awake
if (true || gObj.resting !== false) { // log always for testing
// change resting state, set status and write status to log
gObj.resting = false;
gObj.status = 2;
gObj.log(Math.floor(Date.now()), 2);
require("sleeplog").writeLog(0, [timestamp, 2, E.getTemperature(), data.movement]);
}
}
}
@ -164,18 +138,19 @@ if (sleeplog.enabled) {
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());
// 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, set status and write to log as awake
// change resting state and set status
this.resting = false;
this.status = 2;
this.log(Math.floor(Date.now()), 2);
// write status to log
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 2]);
}
}
}

View File

@ -20,7 +20,7 @@ exports = {
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
enabled: enable,
logfile: logfile,
powersaving: powersaving
powersaving: powersaving || false
}));
// force changes to take effect by executing the boot script
@ -39,33 +39,77 @@ exports = {
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
// - float // internal temperature
// - string // additional information
readLog: function(since, until) {
// set logfile
var logfile = (global.sleeplog || {}).logfile || "sleeplog.log";
readLog: function(logfile, since, until) {
// check/set logfile
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
(global.sleeplog || {}).logfile || "sleeplog.log";
// check if since is in the future
if (since > Date()) return [];
// read log json to array
var log = storage.read(this.logfile) || "";
log = log ? JSON.parse(atob(log)) || [] : [];
// 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
since = (log.find(element => element[0] <= since) || [0])[0];
// filter selected time period
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
// check if filtering is needed
if (since || until) {
// search for latest entry befor since
if (since) since = (log.find(element => element[0] <= since) || [0])[0];
// filter selected time period
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
}
// output 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
// sorting: latest last, format:
// "{substring of ISO date} - {status} for {duration}min\n..."
getReadableLog: function(printLog, since, until) {
getReadableLog: function(printLog, since, until, logfile) {
// read log and check
var log = this.readLog(since, until);
var log = this.readLog(logfile, since, until);
if (!log.length) return;
// reverse array to set last timestamp to the end
log.reverse();
@ -79,8 +123,11 @@ exports = {
logString[index] = "" +
Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
statusText[element[1]] +
(index === log.length - 1 ? "" : " for " + Math.round((log[index + 1][0] - element[0]) / 60000) + "min") +
(element[2] ? " | Temp: " + element[2] + "°C" : "") +
(index === log.length - 1 ?
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] : "");
});
logString = logString.join("\n");
@ -98,12 +145,9 @@ exports = {
// define function to eliminate some errors inside the log
restoreLog: function(logfile) {
// define storage
var storage = require("Storage");
// read log json to array
var log = storage.read(this.logfile) || "";
log = log ? JSON.parse(atob(log)) || [] : [];
// read log and check
var log = this.readLog(logfile);
if (!log.length) return;
// define output variable to show number of changes
var output = log.length;
@ -111,8 +155,8 @@ exports = {
// remove non decremental entries
log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
// write log to storage
storage.write(logfile, btoa(JSON.stringify(log)));
// write log
this.writeLog(logfile, log);
// return difference in length
return output - log.length;
@ -120,12 +164,12 @@ exports = {
// define function to reinterpret worn status based on given temperature threshold
reinterpretTemp: function(logfile, tempthresh) {
// define storage
var storage = require("Storage");
// read log and check
var log = this.readLog(logfile);
if (!log.length) return;
// read log json to array
var log = storage.read(this.logfile) || "";
log = log ? JSON.parse(atob(log)) || [] : [];
// set default tempthresh
tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
// define output variable to show number of changes
var output = 0;
@ -140,8 +184,8 @@ exports = {
return element;
});
// write log to storage
storage.write(logfile, btoa(JSON.stringify(log)));
// write log
this.writeLog(logfile, log);
// return output
return output;

View File

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

View File

@ -9,7 +9,7 @@
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
maxmove: 100, // 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
@ -97,9 +97,9 @@
},
"MaxMove": {
value: settings.maxmove,
step: 44,
step: 1,
onchange: function(v) {
this.value = v = circulate(40, 100, v);
this.value = v = circulate(50, 200, v);
writeSetting("maxmove", v);
}
},