mirror of https://github.com/espruino/BangleApps
sleeplog: Add Sleep Log App
This app logs and displays the four following states: _unknown, not worn, awake, sleeping_ It derived from the 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.pull/1422/head
parent
4cdce7e907
commit
02aeaff86b
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,142 @@
|
|||
# Sleep Log
|
||||
|
||||
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.
|
||||
|
||||
#### 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.
|
||||
* __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.
|
||||
|
||||
---
|
||||
### Control
|
||||
---
|
||||
* __Swipe__
|
||||
Swipe left/right to display the previous/following day.
|
||||
* __Touch__ / __BTN__
|
||||
Touch the screen to open the settings menu to exit or change settings.
|
||||
|
||||
---
|
||||
### Settings
|
||||
---
|
||||
* __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
|
||||
_15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_
|
||||
Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period.
|
||||
* __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
|
||||
_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
|
||||
_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
|
||||
_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._
|
||||
* __Logfile__
|
||||
__default__ / _off_
|
||||
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
|
||||
If the logfile has been customized it is displayed with _custom_.
|
||||
|
||||
---
|
||||
### Global Object and Module Functions
|
||||
---
|
||||
For easy access from the console or other apps the following parameters, values and functions are noteworthy:
|
||||
```
|
||||
>global.sleeplog
|
||||
={
|
||||
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
|
||||
stop: function () { ... }, // funct / stops the service until the next load()
|
||||
start: function () { ... }, // funct / restarts the service
|
||||
...
|
||||
}
|
||||
|
||||
>require("sleeplog")
|
||||
={
|
||||
setEnabled: function (enable, logfile) { ... },
|
||||
// en-/disable the service and/or logging
|
||||
// * enable / bool / service status to change to
|
||||
// * 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
|
||||
// - undefined = no global.sleeplog found
|
||||
readLog: function (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
|
||||
// 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) { ... }
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// returns: int / number of changes that were made
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### Worth Mentioning
|
||||
---
|
||||
#### To do list
|
||||
* Send the logged information to Gadgetbridge.
|
||||
_(For now I have no idea how to achieve this, help is appreciated.)_
|
||||
* Calculate and display overall sleep statistics.
|
||||
|
||||
#### 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).
|
||||
|
||||
#### Creator
|
||||
Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
|
||||
|
||||
#### Contributors
|
||||
nxdefiant ([github](https://github.com/nxdefiant))
|
||||
|
||||
#### Attributions
|
||||
* ESS calculation based on nxdefiant interpretation of the following publication by:
|
||||
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](https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en),
|
||||
ICHI 2014, Verona, Italy, IEEE Press, 2014.
|
||||
* Icons used in this app are from [https://icons8.com](https://icons8.com).
|
||||
|
||||
#### License
|
||||
[MIT License](LICENSE)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4cA///4H8m2ZtVN/nl1P9vXXBoJT/AGcbtuwCJ3btu2CBsG7fZ23ACJk2CIXYCBcB2w1C7YRO/oR/CKp9CCIJ9MUIQRBUI8CpMgYpwRGdJQRGABQRUhdtCJ9btugCJsiM4O0kmSpICFCKJUCCMpZDCJx9CCJsyBIQRxBpACDyAR/CJZeCAA8BCPIA/AFQ"))
|
|
@ -0,0 +1,194 @@
|
|||
// set storage and define settings
|
||||
var storage = require("Storage");
|
||||
var breaktod, maxawake, minconsec;
|
||||
|
||||
// read required settings from storage
|
||||
function readSettings(settings) {
|
||||
breaktod = settings.breaktod || (settings.breaktod === 0 ? 0 : 10); // time of day when to start/end graphs
|
||||
maxawake = settings.maxawake || 36E5; // 60min in ms
|
||||
minconsec = settings.minconsec || 18E5; // 30min in ms
|
||||
}
|
||||
|
||||
// define draw log function
|
||||
function drawLog(topY, viewUntil) {
|
||||
// set default view time
|
||||
viewUntil = viewUntil || Date();
|
||||
|
||||
// define parameters
|
||||
var statusValue = [0, 0.4, 0.6, 1]; // unknown, not worn, awake, sleeping, consecutive sleep
|
||||
var statusColor = [0, 63488, 2016, 32799, 31]; // black, red, green, violet, blue
|
||||
var period = 432E5; // 12h
|
||||
var graphHeight = 18;
|
||||
var labelHeight = 12;
|
||||
var width = g.getWidth();
|
||||
var timestamp0 = viewUntil.valueOf() - period;
|
||||
var y = topY + graphHeight;
|
||||
|
||||
// read 12h wide log
|
||||
var log = require("sleeplog").readLog(timestamp0, viewUntil.valueOf());
|
||||
|
||||
// format log array if not empty
|
||||
if (log.length) {
|
||||
// if the period goes into the future add unknown status at the beginning
|
||||
if (viewUntil > Date()) log.unshift([Date().valueOf(), 0]);
|
||||
|
||||
// check if the period goes earlier than logged data
|
||||
if (log[log.length - 1][0] < timestamp0) {
|
||||
// set time of last entry according to period
|
||||
log[log.length - 1][0] = timestamp0;
|
||||
} else {
|
||||
// add entry with unknown status at the end
|
||||
log.push([timestamp0, 0]);
|
||||
}
|
||||
|
||||
// remap each element to [status, relative beginning, relative end, duration]
|
||||
log = log.map((element, index) => [
|
||||
element[1],
|
||||
element[0] - timestamp0,
|
||||
(log[index - 1] || [viewUntil.valueOf()])[0] - timestamp0,
|
||||
(log[index - 1] || [viewUntil.valueOf()])[0] - element[0]
|
||||
]);
|
||||
|
||||
// start with the oldest entry to build graph left to right
|
||||
log.reverse();
|
||||
}
|
||||
|
||||
// clear area
|
||||
g.reset().clearRect(0, topY, width, y + labelHeight);
|
||||
// draw x axis
|
||||
g.drawLine(0, y + 1, width, y + 1);
|
||||
// draw x label
|
||||
var hours = period / 36E5;
|
||||
var stepwidth = width / hours;
|
||||
var startHour = 24 + viewUntil.getHours() - hours;
|
||||
for (var x = 0; x < hours; x++) {
|
||||
g.fillRect(x * stepwidth, y + 2, x * stepwidth, y + 4);
|
||||
g.setFontAlign(-1, -1).setFont("6x8")
|
||||
.drawString((startHour + x) % 24, x * stepwidth, y + 6);
|
||||
}
|
||||
|
||||
// define variables for sleep calculation
|
||||
var consecutive = 0;
|
||||
var output = [0, 0]; // [estimated, true]
|
||||
var i, nosleepduration;
|
||||
|
||||
// draw graph
|
||||
log.forEach((element, index) => {
|
||||
// set bar color depending on type
|
||||
g.setColor(statusColor[consecutive ? 4 : element[0]]);
|
||||
|
||||
// check for sleeping status
|
||||
if (element[0] === 3) {
|
||||
// count true sleeping hours
|
||||
output[1] += element[3];
|
||||
// count duration of subsequent non sleeping periods
|
||||
i = index + 1;
|
||||
nosleepduration = 0;
|
||||
while (log[i] !== undefined && log[i][0] < 3 && nosleepduration < maxawake) {
|
||||
nosleepduration += log[i++][3];
|
||||
}
|
||||
// check if counted duration lower than threshold to start/stop counting
|
||||
if (log[i] !== undefined && nosleepduration < maxawake) {
|
||||
// start counting consecutive sleeping hours
|
||||
consecutive += element[3];
|
||||
// correct color to match consecutive sleeping
|
||||
g.setColor(statusColor[4]);
|
||||
} else {
|
||||
// check if counted consecutive sleeping greater then threshold
|
||||
if (consecutive >= minconsec) {
|
||||
// write verified consecutive sleeping hours to output
|
||||
output[0] += consecutive + element[3];
|
||||
} else {
|
||||
// correct color to display a canceled consecutive sleeping period
|
||||
g.setColor(statusColor[3]);
|
||||
}
|
||||
// stop counting consecutive sleeping hours
|
||||
consecutive = 0;
|
||||
}
|
||||
} else {
|
||||
// count durations of non sleeping periods for consecutive sleeping
|
||||
if (consecutive) consecutive += element[3];
|
||||
}
|
||||
|
||||
// calculate points
|
||||
var x1 = Math.ceil(element[1] / period * width);
|
||||
var x2 = Math.floor(element[2] / period * width);
|
||||
var y2 = y - graphHeight * statusValue[element[0]];
|
||||
// draw bar
|
||||
g.clearRect(x1, topY, x2, y);
|
||||
g.fillRect(x1, y, x2, y2).reset();
|
||||
if (y !== y2) g.fillRect(x1, y2, x2, y2);
|
||||
});
|
||||
|
||||
// clear variables
|
||||
log = undefined;
|
||||
|
||||
// return convert output into minutes
|
||||
return output.map(value => value /= 6E4);
|
||||
}
|
||||
|
||||
// define draw night to function
|
||||
function drawNightTo(prevDays) {
|
||||
// calculate 10am of this or a previous day
|
||||
var date = Date();
|
||||
date = Date(date.getFullYear(), date.getMonth(), date.getDate() - prevDays, breaktod);
|
||||
|
||||
// get width
|
||||
var width = g.getWidth();
|
||||
|
||||
// clear app area
|
||||
g.clearRect(0, 24, width, width);
|
||||
|
||||
// define variable for sleep calculation
|
||||
var outputs = [0, 0]; // [estimated, true]
|
||||
// draw log graphs and read outputs
|
||||
drawLog(110, date).forEach(
|
||||
(value, index) => outputs[index] += value);
|
||||
drawLog(145, Date(date.valueOf() - 432E5)).forEach(
|
||||
(value, index) => outputs[index] += value);
|
||||
|
||||
// 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);
|
||||
g.fillRect(0, 30, width, 66).reset();
|
||||
g.setFont("12x20").setFontAlign(0, -1);
|
||||
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +
|
||||
require('locale').date(date, 1), width / 2, 30);
|
||||
|
||||
// draw outputs
|
||||
g.reset(); // area: 0, 70, width, 105
|
||||
g.setFont("6x8").setFontAlign(-1, -1);
|
||||
g.drawString("consecutive\nsleeping", 10, 70);
|
||||
g.drawString("true\nsleeping", 10, 90);
|
||||
g.setFont("12x20").setFontAlign(1, -1);
|
||||
g.drawString(Math.floor(outputs[0] / 60) + "h " +
|
||||
Math.floor(outputs[0] % 60) + "min", width - 10, 70);
|
||||
g.drawString(Math.floor(outputs[1] / 60) + "h " +
|
||||
Math.floor(outputs[1] % 60) + "min", width - 10, 90);
|
||||
}
|
||||
|
||||
|
||||
// define function to draw and setup UI
|
||||
function startApp() {
|
||||
readSettings(storage.readJSON("sleeplog.json", true) || {});
|
||||
drawNightTo(prevDays);
|
||||
Bangle.setUI("leftright", (cb) => {
|
||||
if (!cb) {
|
||||
eval(storage.read("sleeplog.settings.js"))(startApp);
|
||||
} else if (prevDays + cb >= -1) {
|
||||
drawNightTo((prevDays += cb));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// define day to display
|
||||
var prevDays = 0;
|
||||
|
||||
// setup app
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// start app
|
||||
startApp();
|
Binary file not shown.
After Width: | Height: | Size: 698 B |
|
@ -0,0 +1,137 @@
|
|||
// Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS):
|
||||
// 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
|
||||
|
||||
// load settings into global object
|
||||
global.sleeplog = Object.assign({
|
||||
enabled: true, // en-/disable completely
|
||||
logfile: "sleeplog.log", // logfile
|
||||
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
|
||||
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]);
|
||||
|
||||
// check if service enabled
|
||||
if (global.sleeplog.enabled) {
|
||||
|
||||
// add cached values and functions to global object
|
||||
global.sleeplog = Object.assign(global.sleeplog, {
|
||||
// set cached values
|
||||
ess_values: [],
|
||||
nomocount: 0,
|
||||
firstnomodate: undefined,
|
||||
resting: undefined,
|
||||
status: 0,
|
||||
|
||||
// define acceleration listener function
|
||||
accel: function(xyz) {
|
||||
// save acceleration magnitude and start calculation on enough saved data
|
||||
if (global.sleeplog.ess_values.push(xyz.mag) >= global.sleeplog.winwidth) global.sleeplog.calc();
|
||||
},
|
||||
|
||||
// define calculator function
|
||||
calc: function() {
|
||||
// calculate standard deviation over
|
||||
var mean = this.ess_values.reduce((prev, cur) => cur + prev) / this.winwidth;
|
||||
var stddev = Math.sqrt(this.ess_values.map(val => Math.pow(val - mean, 2)).reduce((prev, cur) => prev + cur) / this.winwidth);
|
||||
// reset saved acceleration data
|
||||
this.ess_values = [];
|
||||
|
||||
// check for non-movement according to the threshold
|
||||
if (stddev < this.nomothresh) {
|
||||
// increment non-movement sections count, set date of first non-movement
|
||||
if (++this.nomocount == 1) this.firstnomodate = Math.floor(Date.now());
|
||||
// check resting state and non-movement count against threshold
|
||||
if (this.resting !== true && this.nomocount >= this.sleepthresh) {
|
||||
// change resting state, status and write to log
|
||||
this.resting = true;
|
||||
// check if the watch is worn
|
||||
if (E.getTemperature() > this.tempthresh) {
|
||||
// set status and write to log as sleping
|
||||
this.status = 3;
|
||||
this.log(this.firstnomodate, 3, E.getTemperature());
|
||||
} else {
|
||||
// set status and write to log as not worn
|
||||
this.status = 1;
|
||||
this.log(this.firstnomodate, 1, E.getTemperature());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// reset non-movement sections count
|
||||
this.nomocount = 0;
|
||||
// check resting state
|
||||
if (this.resting !== false) {
|
||||
// change resting state
|
||||
this.resting = false;
|
||||
// set status and write to log as awake
|
||||
this.status = 2;
|
||||
this.log(Math.floor(Date.now()), 2);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 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 log = JSON.parse(atob(storage.read(this.logfile)));
|
||||
|
||||
// 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();
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
exports = {
|
||||
// define en-/disable function
|
||||
setEnabled: function(enable, logfile) {
|
||||
// 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;
|
||||
|
||||
// stop if enabled
|
||||
if (global.sleeplog.enabled) global.sleeplog.stop();
|
||||
|
||||
// define storage and filename
|
||||
var storage = require("Storage");
|
||||
var filename = "sleeplog.json";
|
||||
|
||||
// change enabled value in settings
|
||||
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
|
||||
enabled: enable,
|
||||
logfile: logfile
|
||||
}));
|
||||
|
||||
// force changes to take effect by executing the boot script
|
||||
eval(storage.read("sleeplog.boot.js"));
|
||||
|
||||
// clear variables
|
||||
storage = undefined;
|
||||
filename = undefined;
|
||||
return true;
|
||||
},
|
||||
|
||||
// define read log function
|
||||
// sorting: latest first, format:
|
||||
// [[number, int, float, string], [...], ... ]
|
||||
// - number // timestamp in ms
|
||||
// - 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";
|
||||
|
||||
// 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)));
|
||||
|
||||
// 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)));
|
||||
|
||||
// output log
|
||||
return log;
|
||||
},
|
||||
|
||||
// define log to humanreadable string function
|
||||
// sorting: latest last, format:
|
||||
// "{substring of ISO date} - {status} for {duration}min\n..."
|
||||
getReadableLog: function(printLog, since, until) {
|
||||
// read log and check
|
||||
var log = this.readLog(since, until);
|
||||
if (!log.length) return;
|
||||
// reverse array to set last timestamp to the end
|
||||
log.reverse();
|
||||
|
||||
// define status description and log string
|
||||
var statusText = ["unknown ", "not worn", "awake ", "sleeping"];
|
||||
var logString = [];
|
||||
|
||||
// rewrite each entry
|
||||
log.forEach((element, index) => {
|
||||
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" : "") +
|
||||
(element[3] ? " | " + element[3] : "");
|
||||
});
|
||||
logString = logString.join("\n");
|
||||
|
||||
// if set print and return string
|
||||
if (printLog) {
|
||||
print(logString);
|
||||
print("- first", Date(log[0][0]));
|
||||
print("- last", Date(log[log.length - 1][0]));
|
||||
var period = log[log.length - 1][0] - log[0][0];
|
||||
print("- period= " + Math.floor(period / 864E5) + "d " + Math.floor(period % 864E5 / 36E5) + "h " + Math.floor(period % 36E5 / 6E4) + "min");
|
||||
}
|
||||
return logString;
|
||||
},
|
||||
|
||||
// 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)));
|
||||
|
||||
// define output variable to show number of changes
|
||||
var output = log.length;
|
||||
|
||||
// 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)));
|
||||
|
||||
// return difference in length
|
||||
return output - log.length;
|
||||
},
|
||||
|
||||
// define function to reinterpret worn status based on given temperature threshold
|
||||
reinterpretTemp: function(logfile, tempthresh) {
|
||||
// define storage
|
||||
var storage = require("Storage");
|
||||
|
||||
// read log json to array
|
||||
var log = JSON.parse(atob(storage.read(logfile)));
|
||||
|
||||
// define output variable to show number of changes
|
||||
var output = 0;
|
||||
|
||||
// remove non decremental entries
|
||||
log = log.map(element => {
|
||||
if (element[2]) {
|
||||
var tmp = element[1];
|
||||
element[1] = element[2] > tempthresh ? 3 : 1;
|
||||
if (tmp !== element[1]) output++;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
// write log to storage
|
||||
storage.write(logfile, btoa(JSON.stringify(log)));
|
||||
|
||||
// return output
|
||||
return output;
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"id":"sleeplog",
|
||||
"name":"Sleep Log",
|
||||
"shortName": "SleepLog",
|
||||
"version": "0.01",
|
||||
"description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS).",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,boot",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name": "sleeplog.app.js", "url": "app.js"},
|
||||
{"name": "sleeplog.img", "url": "app-icon.js", "evaluate":true},
|
||||
{"name": "sleeplog.boot.js", "url": "boot.js"},
|
||||
{"name": "sleeplog", "url": "lib.js"},
|
||||
{"name": "sleeplog.settings.js", "url": "settings.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name": "sleeplog.json"},
|
||||
{"name": "sleeplog.log"}
|
||||
],
|
||||
"screenshots": [
|
||||
{"url": "screenshot1.png"},
|
||||
{"url": "screenshot2.png"},
|
||||
{"url": "screenshot3.png"}
|
||||
]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
|
@ -0,0 +1,118 @@
|
|||
(function(back) {
|
||||
var filename = "sleeplog.json";
|
||||
|
||||
// set storage and load settings
|
||||
var storage = require("Storage");
|
||||
var settings = Object.assign({
|
||||
breaktod: 10, // time of day when to start/end graphs
|
||||
maxawake: 36E5, // 60min in ms
|
||||
minconsec: 18E5, // 30min in ms
|
||||
tempthresh: 27, // every temperature above ist registered as worn
|
||||
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
|
||||
enabled: true, // en-/disable completely
|
||||
logfile: "sleeplog.log", // logfile
|
||||
}, storage.readJSON(filename, true) || {});
|
||||
|
||||
// write change to global.sleeplog and storage
|
||||
function writeSetting(key, value) {
|
||||
// change key in global.sleeplog
|
||||
if (typeof global.sleeplog === "object") global.sleeplog[key] = value;
|
||||
// reread settings to only change key
|
||||
settings = Object.assign(settings, storage.readJSON(filename, true) || {});
|
||||
// change the value of key
|
||||
settings[key] = value;
|
||||
// write to storage
|
||||
storage.writeJSON(filename, settings);
|
||||
}
|
||||
|
||||
// define circulate function
|
||||
function circulate(min, max, value) {
|
||||
return value > max ? min : value < min ? max : value;
|
||||
}
|
||||
|
||||
// calculate sleepthresh factor
|
||||
var stFactor = settings.winwidth / 12.5 / 60;
|
||||
|
||||
// show main menu
|
||||
function showMain() {
|
||||
var mainMenu = E.showMenu({
|
||||
"": {
|
||||
title: "Sleep Log"
|
||||
},
|
||||
"< Exit": () => load(),
|
||||
"< Back": () => back(),
|
||||
"BreakTod": {
|
||||
value: settings.breaktod,
|
||||
step: 1,
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(0, 23, v);
|
||||
writeSetting("breaktod", v);
|
||||
}
|
||||
},
|
||||
"MaxAwake": {
|
||||
value: settings.maxawake / 6E4,
|
||||
step: 5,
|
||||
format: v => v + "min",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(15, 120, v);
|
||||
writeSetting("maxawake", v * 6E4);
|
||||
}
|
||||
},
|
||||
"MinConsec": {
|
||||
value: settings.minconsec / 6E4,
|
||||
step: 5,
|
||||
format: v => v + "min",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(15, 120, v);
|
||||
writeSetting("minconsec", v * 6E4);
|
||||
}
|
||||
},
|
||||
"TempThresh": {
|
||||
value: settings.tempthresh,
|
||||
step: 0.5,
|
||||
format: v => v + "°C",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(20, 40, v);
|
||||
writeSetting("tempthresh", v);
|
||||
}
|
||||
},
|
||||
"NoMoThresh": {
|
||||
value: settings.nomothresh,
|
||||
step: 0.001,
|
||||
format: v => ("" + v).padEnd(5, "0"),
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(0.006, 0.02, v);
|
||||
writeSetting("nomothresh", v);
|
||||
}
|
||||
},
|
||||
"MinDuration": {
|
||||
value: Math.floor(settings.sleepthresh * stFactor),
|
||||
step: 1,
|
||||
format: v => v + "min",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(5, 15, v);
|
||||
writeSetting("sleepthresh", Math.ceil(v / stFactor));
|
||||
}
|
||||
},
|
||||
"Enabled": {
|
||||
value: settings.enabled,
|
||||
format: v => v ? "on" : "off",
|
||||
onchange: function(v) {
|
||||
writeSetting("enabled", v);
|
||||
}
|
||||
},
|
||||
"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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// draw main menu
|
||||
showMain();
|
||||
})
|
Loading…
Reference in New Issue