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
storm64 2022-02-11 09:29:02 +01:00
parent 4cdce7e907
commit 02aeaff86b
12 changed files with 771 additions and 0 deletions

1
apps/sleeplog/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

142
apps/sleeplog/README.md Normal file
View File

@ -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)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///4H8m2ZtVN/nl1P9vXXBoJT/AGcbtuwCJ3btu2CBsG7fZ23ACJk2CIXYCBcB2w1C7YRO/oR/CKp9CCIJ9MUIQRBUI8CpMgYpwRGdJQRGABQRUhdtCJ9btugCJsiM4O0kmSpICFCKJUCCMpZDCJx9CCJsyBIQRxBpACDyAR/CJZeCAA8BCPIA/AFQ"))

194
apps/sleeplog/app.js Normal file
View File

@ -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();

BIN
apps/sleeplog/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

137
apps/sleeplog/boot.js Normal file
View File

@ -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();
}

150
apps/sleeplog/lib.js Normal file
View File

@ -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;
}
};

View File

@ -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

118
apps/sleeplog/settings.js Normal file
View File

@ -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();
})