sleeplog: Correct fix #1445, display loading info while calculating sleep data

Update app.js
 - move complex data analysis into separate function
 - add displaying a loading info
 - faster feedback by calling the analysis through a timeout
Update boot.js
 - correct fix #1445 as mentioned by splitting the `stop()` function to only remove the kill listener on a manual stop
 - optimize `start()` and `stop()` functions
Update README.md
 - change beautified menu entries according to changes in settings.js
Update settings.js
 - replace `circulate()` function with internal `wrap: true`
 - move delay for menu redraw as suggested in espruino/Espruino#2149
 - beautify menu entries for new touchscreen E.showMenu system (> fw 2v.13.32)
master
storm64 2022-02-15 19:47:15 +01:00
parent ab42b6555f
commit 8c4f2f18c9
6 changed files with 129 additions and 97 deletions

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Fix crash on start
0.03: Added power saving mode, move all read/write log actions into lib/module, fix #1445
0.02: Fix crash on start #1423
0.03: Added power saving mode, move all read/write log actions into lib/module
0.04: Fix #1445, display loading info while calculating sleep data

View File

@ -34,31 +34,31 @@ also provides a power saving mode using the built in movement calculation. The i
---
### Settings
---
* __BreakTod__ | break at time of day
* __Break Tod__ | 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
* __Max Awake__ | 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
* __Min Consec__ | 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
* __Temp Thresh__ | 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_.
* __PowerSaving__
* __Power Saving__
_on_ / __off__
En-/Disable power saving mode. _Saves battery, but might decrease accurracy._
* __MaxMove__ | maximal movement threshold
* __Max Move__ | maximal movement threshold
(only available when on power saving mode)
_50_ / _51_ / _..._ / __100__ / _..._ / _200_
On power saving mode the watch is considered resting if this threshold is lower or equal to the movement value of bangle's health event.
* __NoMoThresh__ | no movement threshold
* __NoMo Thresh__ | no movement threshold
(only available when not on power saving mode)
_0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_
The standard deviation over the measured values needs to be lower then this threshold to count as not moving.
The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise.
* __MinDuration__ | minimal no movement duration
* __Min Duration__ | minimal no movement duration
(only available when not on power saving mode)
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
If no movement is detected for this duration, the watch is considered as resting.

View File

@ -127,34 +127,24 @@ function drawLog(topY, viewUntil) {
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);
// define function to draw the analysis
function drawAnalysis(toDate) {
//var t0 = Date.now();
// 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 or green bg if powersaving enabled
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
g.fillRect(0, 30, width, 66).reset();
g.setFont("12x20").setFontAlign(0, -1);
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +
require('locale').date(date, 1), width / 2, 30);
// clear analysis area
g.clearRect(0, 71, width, width);
// draw log graphs and read outputs
drawLog(110, toDate).forEach(
(value, index) => outputs[index] += value);
drawLog(145, Date(toDate.valueOf() - 432E5)).forEach(
(value, index) => outputs[index] += value);
// draw outputs
g.reset(); // area: 0, 70, width, 105
@ -166,8 +156,47 @@ function drawNightTo(prevDays) {
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);
//print("analysis processing seconds:", Math.round(Date.now() - t0) / 1000);
}
// define draw night to function
function drawNightTo(prevDays) {
// calculate 10am of this or a previous day
var toDate = Date();
toDate = Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate() - prevDays, breaktod);
// get width
var width = g.getWidth();
var center = width / 2;
// reduce date by 1s to ensure correct headline
toDate = Date(toDate.valueOf() - 1E3);
// clear heading area
g.clearRect(0, 24, width, 70);
// display service statuses: service, loggging and powersaving
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
g.fillRect(0, 30, width, 66).reset();
// draw headline
g.setFont("12x20").setFontAlign(0, -1);
g.drawString("Night to " + require('locale').dow(toDate, 1) + "\n" +
require('locale').date(toDate, 1), center, 30);
// show loading info
var info = "calculating data ...\nplease be patient :)";
var y0 = center + 30;
var bounds = [center - 80, y0 - 20, center + 80, y0 + 20];
g.clearRect.apply(g, bounds).drawRect.apply(g, bounds);
g.setFont("6x8").setFontAlign(0, 0);
g.drawString(info, center, y0);
// calculate and draw analysis after timeout for faster feedback
if (ATID) ATID = clearTimeout(ATID);
ATID = setTimeout(drawAnalysis, 50, toDate);
}
// define function to draw and setup UI
function startApp() {
@ -182,8 +211,9 @@ function startApp() {
});
}
// define day to display
// define day to display and analysis timeout id
var prevDays = 0;
var ATID;
// setup app
g.clear();

View File

@ -28,36 +28,43 @@ if (sleeplog.enabled) {
resting: undefined,
status: undefined,
// define stop function (logging will restart if enabled and boot file is executed)
stop: function() {
// define function to handle stopping the service, it will be restarted on reload if enabled
stopHandler: function() {
// remove all listeners
Bangle.removeListener('accel', sleeplog.accel);
Bangle.removeListener('health', sleeplog.health);
E.removeListener('kill', () => sleeplog.stop());
// exit on missing global object
if (!global.sleeplog) return;
// write log with undefined sleeping status
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
// reset always used cached values
sleeplog.resting = undefined;
sleeplog.status = undefined;
sleeplog.ess_values = [];
sleeplog.nomocount = 0;
sleeplog.firstnomodate = undefined;
// reset cached values if sleeplog is defined
if (global.sleeplog) {
sleeplog.resting = undefined;
sleeplog.status = undefined;
// reset cached ESS calculation values
if (!sleeplog.powersaving) {
sleeplog.ess_values = [];
sleeplog.nomocount = 0;
sleeplog.firstnomodate = undefined;
}
}
},
// define restart function (also use for initial starting)
// define function to remove the kill listener and stop the service
// https://github.com/espruino/BangleApps/issues/1445
stop: function() {
E.removeListener('kill', sleeplog.stopHandler);
sleeplog.stopHandler();
},
// define function to initialy start or restart the service
start: function() {
// exit on missing global object
if (!global.sleeplog) return;
// add kill listener
E.on('kill', sleeplog.stopHandler);
// add health listener if defined and
if (sleeplog.health) Bangle.on('health', sleeplog.health);
// add acceleration listener if defined and set status to unknown
if (sleeplog.accel) Bangle.on('accel', sleeplog.accel);
// add kill listener
E.on('kill', () => sleeplog.stop());
// read log since 5min ago and restore status to last known state or unknown
sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1]
sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1];
// update resting according to status
sleeplog.resting = sleeplog.status % 2;
// write restored status to log

View File

@ -2,7 +2,7 @@
"id":"sleeplog",
"name":"Sleep Log",
"shortName": "SleepLog",
"version": "0.03",
"version": "0.04",
"description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.",
"icon": "app.png",
"type": "app",

View File

@ -29,11 +29,6 @@
storage.writeJSON(filename, settings);
}
// define circulate function
function circulate(min, max, value) {
return value > max ? min : value < min ? max : value;
}
// define function to change values that need a restart of the service
function changeRestart() {
require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving);
@ -49,77 +44,79 @@
title: "Sleep Log",
selected: selected
},
"< Exit": () => load(),
"Exit": () => load(),
"< Back": () => back(),
"BreakTod": {
"Break Tod": {
value: settings.breaktod,
step: 1,
onchange: function(v) {
this.value = v = circulate(0, 23, v);
writeSetting("breaktod", v);
}
min: 0,
max: 23,
wrap: true,
onchange: v => writeSetting("breaktod", v),
},
"MaxAwake": {
"Max Awake": {
value: settings.maxawake / 6E4,
step: 5,
min: 15,
max: 120,
wrap: true,
format: v => v + "min",
onchange: function(v) {
this.value = v = circulate(15, 120, v);
writeSetting("maxawake", v * 6E4);
}
onchange: v => writeSetting("maxawake", v * 6E4),
},
"MinConsec": {
"Min Consec": {
value: settings.minconsec / 6E4,
step: 5,
min: 15,
max: 120,
wrap: true,
format: v => v + "min",
onchange: function(v) {
this.value = v = circulate(15, 120, v);
writeSetting("minconsec", v * 6E4);
}
onchange: v => writeSetting("minconsec", v * 6E4),
},
"TempThresh": {
"Temp Thresh": {
value: settings.tempthresh,
step: 0.5,
min: 20,
max: 40,
wrap: true,
format: v => v + "°C",
onchange: function(v) {
this.value = v = circulate(20, 40, v);
writeSetting("tempthresh", v);
}
onchange: v => writeSetting("tempthresh", v),
},
"PowerSaving": {
"Power Saving": {
value: settings.powersaving,
format: v => v ? "on" : "off",
onchange: function(v) {
settings.powersaving = v;
changeRestart();
showMain(7);
// redraw menu with changed entries subsequent to onchange
// https://github.com/espruino/Espruino/issues/2149
setTimeout(showMain, 1, 6);
}
},
"MaxMove": {
"Max Move": {
value: settings.maxmove,
step: 1,
onchange: function(v) {
this.value = v = circulate(50, 200, v);
writeSetting("maxmove", v);
}
min: 50,
max: 200,
wrap: true,
onchange: v => writeSetting("maxmove", v),
},
"NoMoThresh": {
"NoMo Thresh": {
value: settings.nomothresh,
step: 0.001,
min: 0.006,
max: 0.02,
wrap: true,
format: v => ("" + v).padEnd(5, "0"),
onchange: function(v) {
this.value = v = circulate(0.006, 0.02, v);
writeSetting("nomothresh", v);
}
onchange: v => writeSetting("nomothresh", v),
},
"MinDuration": {
"Min Duration": {
value: Math.floor(settings.sleepthresh * stFactor),
step: 1,
min: 5,
max: 15,
wrap: true,
format: v => v + "min",
onchange: function(v) {
this.value = v = circulate(5, 15, v);
writeSetting("sleepthresh", Math.ceil(v / stFactor));
}
onchange: v => writeSetting("sleepthresh", Math.ceil(v / stFactor)),
},
"Enabled": {
value: settings.enabled,
@ -141,11 +138,8 @@
}
};
// check power saving mode to delete unused entries
(settings.powersaving ? ["NoMoThresh", "MinDuration"] : ["MaxMove"]).forEach(property => delete mainMenu[property]);
(settings.powersaving ? ["NoMo Thresh", "Min Duration"] : ["Max Move"]).forEach(property => delete mainMenu[property]);
var menu = E.showMenu(mainMenu);
// workaround to display changed entries correct
// https://github.com/espruino/Espruino/issues/2149
if (selected) setTimeout(m => m.draw(), 1, menu);
}
// draw main menu