commit
7b72606ac4
|
|
@ -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, fix #1445
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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_)
|
||||
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__
|
||||
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,37 @@ 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_.
|
||||
|
|
@ -65,8 +80,9 @@ For easy access from the console or other apps the following parameters, values
|
|||
enabled: true, // bool / service status indicator
|
||||
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
|
||||
status: 2, // int / actual status:
|
||||
/ 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()
|
||||
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")
|
||||
={
|
||||
setEnabled: function (enable, logfile) { ... },
|
||||
// en-/disable the service and/or logging
|
||||
// * enable / bool / service status to change to
|
||||
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
|
||||
}
|
||||
```
|
||||
|
|
@ -120,7 +148,9 @@ For easy access from the console or other apps the following parameters, values
|
|||
#### To do list
|
||||
* Send the logged information to Gadgetbridge.
|
||||
_(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.
|
||||
* Option to automatically change power saving mode depending on time of day.
|
||||
|
||||
#### 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).
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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,41 +2,126 @@
|
|||
// 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: 100, // 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 always used values and functions to global object
|
||||
sleeplog = Object.assign(sleeplog, {
|
||||
// set cached values
|
||||
resting: undefined,
|
||||
status: undefined,
|
||||
|
||||
// define stop function (logging will restart if enabled and boot file is executed)
|
||||
stop: function() {
|
||||
// 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
|
||||
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
|
||||
// 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() {
|
||||
// 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', () => 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
|
||||
global.sleeplog = Object.assign(global.sleeplog, {
|
||||
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,
|
||||
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();
|
||||
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);
|
||||
|
|
@ -49,91 +134,28 @@ if (global.sleeplog.enabled) {
|
|||
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
|
||||
// change resting state
|
||||
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());
|
||||
}
|
||||
// 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
|
||||
// change resting state and set status
|
||||
this.resting = false;
|
||||
// set status and write to log as awake
|
||||
this.status = 2;
|
||||
this.log(Math.floor(Date.now()), 2);
|
||||
// write status to log
|
||||
require("sleeplog").writeLog(0, [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)
|
||||
stop: function() {
|
||||
// remove acceleration and kill listener
|
||||
Bangle.removeListener('accel', global.sleeplog.accel);
|
||||
E.removeListener('kill', global.sleeplog.stop);
|
||||
// write log with undefined sleeping status
|
||||
global.sleeplog.log(Math.floor(Date.now()));
|
||||
// reset cached values
|
||||
global.sleeplog.ess_values = [];
|
||||
global.sleeplog.nomocount = 0;
|
||||
global.sleeplog.firstnomodate = undefined;
|
||||
global.sleeplog.resting = undefined;
|
||||
global.sleeplog.status = 0;
|
||||
},
|
||||
|
||||
// define restart function (also use for initial starting)
|
||||
start: function() {
|
||||
// add acceleration listener
|
||||
Bangle.on('accel', global.sleeplog.accel);
|
||||
// add kill listener
|
||||
E.on('kill', global.sleeplog.stop);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// initial starting
|
||||
global.sleeplog.start();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
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;
|
||||
|
||||
// set default logfile
|
||||
logfile = logfile.endsWith(".log") ? logfile :
|
||||
logfile === false ? undefined :
|
||||
"sleeplog.log";
|
||||
|
||||
// check if status needs to be changed
|
||||
if (enable === global.sleeplog.enabled ||
|
||||
logfile === global.sleeplog.logfile) return false;
|
||||
logfile = (typeof logfile === "string" && logfile.endsWith(".log")) ? logfile :
|
||||
logfile === false ? undefined : "sleeplog.log";
|
||||
|
||||
// stop if enabled
|
||||
if (global.sleeplog.enabled) global.sleeplog.stop();
|
||||
|
|
@ -23,7 +18,8 @@ exports = {
|
|||
// change enabled value in settings
|
||||
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
|
||||
enabled: enable,
|
||||
logfile: logfile
|
||||
logfile: logfile,
|
||||
powersaving: powersaving || false
|
||||
}));
|
||||
|
||||
// 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
|
||||
// - 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 = JSON.parse(atob(require("Storage").read(logfile)));
|
||||
// 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) || [];
|
||||
|
||||
// check if filtering is needed
|
||||
if (since || until) {
|
||||
// search for latest entry befor since
|
||||
since = (log.find(element => element[0] <= since) || [0])[0];
|
||||
|
||||
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();
|
||||
|
|
@ -81,8 +122,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");
|
||||
|
|
@ -100,11 +144,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 = JSON.parse(atob(storage.read(logfile)));
|
||||
// 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;
|
||||
|
|
@ -112,8 +154,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;
|
||||
|
|
@ -121,11 +163,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 = JSON.parse(atob(storage.read(logfile)));
|
||||
// set default tempthresh
|
||||
tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
|
||||
|
||||
// define output variable to show number of changes
|
||||
var output = 0;
|
||||
|
|
@ -140,8 +183,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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: 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
|
||||
|
|
@ -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: 1,
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(50, 200, v);
|
||||
writeSetting("maxmove", v);
|
||||
}
|
||||
},
|
||||
"NoMoThresh": {
|
||||
value: settings.nomothresh,
|
||||
step: 0.001,
|
||||
|
|
@ -100,17 +125,27 @@
|
|||
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" : 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
|
||||
|
|
|
|||
Loading…
Reference in New Issue