From 02aeaff86bf14ad3b29e134060791ac962f9e8eb Mon Sep 17 00:00:00 2001 From: storm64 Date: Fri, 11 Feb 2022 09:29:02 +0100 Subject: [PATCH] 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. --- apps/sleeplog/ChangeLog | 1 + apps/sleeplog/README.md | 142 +++++++++++++++++++++++++ apps/sleeplog/app-icon.js | 1 + apps/sleeplog/app.js | 194 ++++++++++++++++++++++++++++++++++ apps/sleeplog/app.png | Bin 0 -> 698 bytes apps/sleeplog/boot.js | 137 ++++++++++++++++++++++++ apps/sleeplog/lib.js | 150 ++++++++++++++++++++++++++ apps/sleeplog/metadata.json | 28 +++++ apps/sleeplog/screenshot1.png | Bin 0 -> 4074 bytes apps/sleeplog/screenshot2.png | Bin 0 -> 3497 bytes apps/sleeplog/screenshot3.png | Bin 0 -> 4111 bytes apps/sleeplog/settings.js | 118 +++++++++++++++++++++ 12 files changed, 771 insertions(+) create mode 100644 apps/sleeplog/ChangeLog create mode 100644 apps/sleeplog/README.md create mode 100644 apps/sleeplog/app-icon.js create mode 100644 apps/sleeplog/app.js create mode 100644 apps/sleeplog/app.png create mode 100644 apps/sleeplog/boot.js create mode 100644 apps/sleeplog/lib.js create mode 100644 apps/sleeplog/metadata.json create mode 100644 apps/sleeplog/screenshot1.png create mode 100644 apps/sleeplog/screenshot2.png create mode 100644 apps/sleeplog/screenshot3.png create mode 100644 apps/sleeplog/settings.js diff --git a/apps/sleeplog/ChangeLog b/apps/sleeplog/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/sleeplog/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/sleeplog/README.md b/apps/sleeplog/README.md new file mode 100644 index 000000000..24f47c23c --- /dev/null +++ b/apps/sleeplog/README.md @@ -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) diff --git a/apps/sleeplog/app-icon.js b/apps/sleeplog/app-icon.js new file mode 100644 index 000000000..36b890491 --- /dev/null +++ b/apps/sleeplog/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///4H8m2ZtVN/nl1P9vXXBoJT/AGcbtuwCJ3btu2CBsG7fZ23ACJk2CIXYCBcB2w1C7YRO/oR/CKp9CCIJ9MUIQRBUI8CpMgYpwRGdJQRGABQRUhdtCJ9btugCJsiM4O0kmSpICFCKJUCCMpZDCJx9CCJsyBIQRxBpACDyAR/CJZeCAA8BCPIA/AFQ")) diff --git a/apps/sleeplog/app.js b/apps/sleeplog/app.js new file mode 100644 index 000000000..cbfad4bda --- /dev/null +++ b/apps/sleeplog/app.js @@ -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(); diff --git a/apps/sleeplog/app.png b/apps/sleeplog/app.png new file mode 100644 index 0000000000000000000000000000000000000000..bb7f11f67bf96368c6c8994d43608a3c9ece108d GIT binary patch literal 698 zcmV;r0!96aP)>_62_vrVs7$x%)KEhY2bLH`8}|aP+4?loNZgNtQ$xdbF(5^0LHrv z*+n$Jo?TtG5znfDhWMwg>pnPhA64bntJ|$Z7122xCSpMsXp$@DCL257wkfKLqTb6Of7R0_aEmw-F2N`7O;DfXna3pU>Y z0M@gs%k^p^C2$Ye0)950Efu{>yq>Zp9zw{#ps}8TLDSP+NXwZdb@vg%IRJ!BR2F1v ztrChl1B^4EU9T=yX`_vGYC;HyCw zeu2ImiyxT3@-F?QwZq&%vuyl1->))54uWLrwWsTPj9&& zHGOc%+hA@$WdZo^)AJ3j-3%Zq5XJ^%YOO*nb_#d~{AfH&XzjFIc>XCYfq&KIZSXsZ zKc4t(wAhPlZ!AdKpR1K@o92-}iAUamQ3?G^crTVCpMl80uo-y+Mwhu^{gIh`P)0rj gk%5Sah=~82Kj4M5noGmPng9R*07*qoM6N<$f_3Oc+5i9m literal 0 HcmV?d00001 diff --git a/apps/sleeplog/boot.js b/apps/sleeplog/boot.js new file mode 100644 index 000000000..7ec71742c --- /dev/null +++ b/apps/sleeplog/boot.js @@ -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(); +} diff --git a/apps/sleeplog/lib.js b/apps/sleeplog/lib.js new file mode 100644 index 000000000..81bca0f3f --- /dev/null +++ b/apps/sleeplog/lib.js @@ -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; + } + +}; diff --git a/apps/sleeplog/metadata.json b/apps/sleeplog/metadata.json new file mode 100644 index 000000000..4a67af301 --- /dev/null +++ b/apps/sleeplog/metadata.json @@ -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"} + ] +} diff --git a/apps/sleeplog/screenshot1.png b/apps/sleeplog/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..9ab384ca93dedea2d7499bf8f9e7cd61b3ee3826 GIT binary patch literal 4074 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 A1^@s6 literal 0 HcmV?d00001 diff --git a/apps/sleeplog/screenshot2.png b/apps/sleeplog/screenshot2.png new file mode 100644 index 0000000000000000000000000000000000000000..69e370042b9d83e90c9fc5c9930f54a884dfa77c GIT binary patch literal 3497 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 literal 0 HcmV?d00001 diff --git a/apps/sleeplog/screenshot3.png b/apps/sleeplog/screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..79433949a4f1abe017d9dd469e489b75caaa9983 GIT binary patch literal 4111 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 PA3ku+#LBqB5EcI)fkepR literal 0 HcmV?d00001 diff --git a/apps/sleeplog/settings.js b/apps/sleeplog/settings.js new file mode 100644 index 000000000..6137e2082 --- /dev/null +++ b/apps/sleeplog/settings.js @@ -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(); +})