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 000000000..bb7f11f67 Binary files /dev/null and b/apps/sleeplog/app.png differ 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 000000000..9ab384ca9 Binary files /dev/null and b/apps/sleeplog/screenshot1.png differ diff --git a/apps/sleeplog/screenshot2.png b/apps/sleeplog/screenshot2.png new file mode 100644 index 000000000..69e370042 Binary files /dev/null and b/apps/sleeplog/screenshot2.png differ diff --git a/apps/sleeplog/screenshot3.png b/apps/sleeplog/screenshot3.png new file mode 100644 index 000000000..79433949a Binary files /dev/null and b/apps/sleeplog/screenshot3.png differ 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(); +})