forked from FOSS/BangleApps
449 lines
13 KiB
JavaScript
449 lines
13 KiB
JavaScript
(function(back) {
|
|
// define settings filename
|
|
var filename = "sleeplog.json";
|
|
// define logging prompt display status
|
|
var thresholdsPrompt = true;
|
|
|
|
// define default vaules
|
|
var defaults = {
|
|
// main settings
|
|
enabled: true, // en-/disable completely
|
|
// threshold settings
|
|
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
|
|
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
|
|
deepTh: 100, // threshold for deep sleep
|
|
lightTh: 200, // threshold for light sleep
|
|
wearTemp: 19.5, // temperature threshold to count as worn
|
|
// app settings
|
|
breakToD: 12, // [h] time of day when to start/end graphs
|
|
appTimeout: 0 // lock and backlight timeouts for the app
|
|
};
|
|
|
|
// assign loaded settings to default values
|
|
var settings = Object.assign(defaults, require("Storage").readJSON(filename, true) || {});
|
|
|
|
// write change to storage
|
|
function writeSetting() {
|
|
require("Storage").writeJSON(filename, settings);
|
|
}
|
|
|
|
// plot a debug file
|
|
function plotDebug(filename) {
|
|
// handle swipe events
|
|
function swipeHandler(x, y) {
|
|
if (x) {
|
|
start -= x;
|
|
if (start < 0 || maxStart && start > maxStart) {
|
|
start = start < 0 ? 0 : maxStart;
|
|
} else {
|
|
drawGraph();
|
|
}
|
|
} else {
|
|
minMove += y * 10;
|
|
if (minMove < 0 || minMove > 300) {
|
|
minMove = minMove < 0 ? 0 : 300;
|
|
} else {
|
|
drawGraph();
|
|
}
|
|
}
|
|
}
|
|
// handle touch events
|
|
function touchHandler() {
|
|
invert = !invert;
|
|
drawGraph();
|
|
}
|
|
|
|
// read required entries
|
|
function readEntries(count) {
|
|
// extract usabble data from line
|
|
function extract(line) {
|
|
if (!line) return;
|
|
line = line.trim().split(",");
|
|
return [Math.round((parseFloat(line[0]) - 25569) * 144), parseInt(line[1])];
|
|
}
|
|
|
|
// open debug file
|
|
var file = require("Storage").open(filename, "r");
|
|
// skip title
|
|
file.readLine();
|
|
// skip past entries
|
|
for (var i = 0; i < start * count; i++) { file.readLine(); }
|
|
// define data with first entry
|
|
var data = [extract(file.readLine())];
|
|
// get start time in 10min steps
|
|
var start10min = data[0][0];
|
|
// read first required entry
|
|
var line = extract(file.readLine());
|
|
|
|
// read next count entries from file
|
|
while (data.length < count) {
|
|
// check if line is immediately after the last entry
|
|
if (line[0] === start10min + data.length) {
|
|
// add line to data
|
|
data.push(line);
|
|
// read new line
|
|
line = extract(file.readLine());
|
|
// stop if no more data available
|
|
if (!line) break;
|
|
} else {
|
|
// add line with unknown movement
|
|
data.push([start10min + data.length, 0]);
|
|
}
|
|
}
|
|
|
|
// free ram
|
|
file = undefined
|
|
// set this start as max, if less entries than expected
|
|
if (data.length < count) maxStart = start;
|
|
return data;
|
|
}
|
|
|
|
// draw graph at starting point
|
|
function drawGraph() {
|
|
// set correct or inverted drawing
|
|
function rect(fill, x0, y0, x1, y1) {
|
|
if (fill ^ invert) {
|
|
g.fillRect(x0, y0, x1, y1);
|
|
} else {
|
|
g.clearRect(x0, y0, x1, y1);
|
|
}
|
|
}
|
|
|
|
// set witdh
|
|
var width = g.getWidth();
|
|
// calculate entries to display (+ set width zero based)
|
|
var count = (width--) / 4;
|
|
// read required entries
|
|
var data = readEntries(count);
|
|
|
|
// clear app area
|
|
g.reset().clearRect(0, width - 13, width, width);
|
|
rect(false, 0, 24, width, width - 14);
|
|
// draw x axis
|
|
g.drawLine(0, width - 13, width, width - 13);
|
|
// draw x label
|
|
data.forEach((e, i) => {
|
|
var startTime = new Date(e[0] * 6E5);
|
|
if (startTime.getMinutes() === 0) {
|
|
g.fillRect(4 * i, width - 12, 4 * i, width - 9);
|
|
g.setFontAlign(-1, -1).setFont("6x8")
|
|
.drawString(startTime.getHours(), 4 * i + 1, width - 8);
|
|
} else if (startTime.getMinutes() === 30) {
|
|
g.fillRect(4 * i, width - 12, 4 * i, width - 11);
|
|
}
|
|
});
|
|
|
|
// calculate max height
|
|
var height = width - 38;
|
|
// cycle through entries
|
|
data.forEach((e, i) => {
|
|
// check if movement available
|
|
if (e[1]) {
|
|
// set color depending on recognised status
|
|
var color = e[1] < deepTh ? 31 : e[1] < lightTh ? 2047 : 2016;
|
|
// correct according to min movement
|
|
e[1] -= minMove;
|
|
// keep movement in bounderies
|
|
e[1] = e[1] < 0 ? 0 : e[1] > height ? height : e[1];
|
|
// draw line and rectangle
|
|
g.reset();
|
|
rect(true, 4 * i, width - 14, 4 * i, width - 14 - e[1]);
|
|
g.setColor(color).fillRect(4 * i + 1, width - 14, 4 * i + 3, width - 14 - e[1]);
|
|
} else {
|
|
// draw error in red
|
|
g.setColor(63488).fillRect(4 * i, width - 14, 4 * i, width - 14 - height);
|
|
}
|
|
});
|
|
// draw threshold lines
|
|
[deepTh, lightTh].forEach(th => {
|
|
th -= minMove;
|
|
if (th > 0 && th < height) {
|
|
// draw line
|
|
g.reset();
|
|
rect(true, 0, width - 14 - th, width, width - 14 - th);
|
|
// draw value above or below line
|
|
var yAlign = th < height / 2 ? -1 : 1;
|
|
if (invert) g.setColor(1);
|
|
g.setFontAlign(1, yAlign).setFont("6x8")
|
|
.drawString(th + minMove, width - 2, width - 13 - th + 10 * yAlign);
|
|
}
|
|
});
|
|
|
|
// free ram
|
|
data = undefined;
|
|
}
|
|
|
|
// get thresholds
|
|
var deepTh = global.sleeplog ? global.sleeplog.conf.deepTh : defaults.deepTh;
|
|
var lightTh = global.sleeplog ? global.sleeplog.conf.lightTh : defaults.lightTh;
|
|
// set lowest movement displayed
|
|
var minMove = deepTh - 20;
|
|
// set start point
|
|
var start = 0;
|
|
// define max start point value
|
|
var maxStart = 0;
|
|
// define inverted color status
|
|
var invert = false;
|
|
|
|
// setup UI
|
|
Bangle.setUI({
|
|
mode: "custom",
|
|
back: selectDebug,
|
|
touch: touchHandler,
|
|
swipe: swipeHandler
|
|
});
|
|
|
|
// first draw
|
|
drawGraph(start);
|
|
}
|
|
|
|
// select a debug logfile
|
|
function selectDebug() {
|
|
// load debug files
|
|
var files = require("Storage").list(/^sleeplog_\d\d\d\d\d\d\.csv$/, {sf:true});
|
|
|
|
// check if no files found
|
|
if (!files.length) {
|
|
// show prompt
|
|
E.showPrompt( /*LANG*/"No debug files found.", {
|
|
title: /*LANG*/"Debug log",
|
|
buttons: {
|
|
/*LANG*/"Back": 0
|
|
}
|
|
}).then(showDebug);
|
|
} else {
|
|
// prepare scroller
|
|
const H = 40;
|
|
var menuIcon = "\0\f\f\x81\0\xFF\xFF\xFF\0\0\0\0\x0F\xFF\xFF\xF0\0\0\0\0\xFF\xFF\xFF";
|
|
// show scroller
|
|
E.showScroller({
|
|
h: H, c: files.length,
|
|
back: showDebug,
|
|
scrollMin : -24, scroll : -24, // title is 24px, rendered at -1
|
|
draw : (idx, r) => {
|
|
if (idx < 0) {
|
|
return g.setFont("12x20").setFontAlign(-1,0).drawString(menuIcon + " Select file", r.x + 12, r.y + H - 12);
|
|
} else {
|
|
g.setColor(g.theme.bg2).fillRect({x: r.x + 4, y: r.y + 2, w: r.w - 8, h: r.h - 4, r: 5});
|
|
var name = new Date(parseInt(files[idx].match(/\d\d\d\d\d\d/)[0]) * 36E5);
|
|
name = name.toString().slice(0, -12).split(" ").filter((e, i) => i !== 3).join(" ");
|
|
g.setColor(g.theme.fg2).setFont("12x20").setFontAlign(-1, 0).drawString(name, r.x + 12, r.y + H / 2);
|
|
}
|
|
},
|
|
select: (idx) => plotDebug(files[idx])
|
|
});
|
|
}
|
|
}
|
|
|
|
// show menu or promt to change debugging
|
|
function showDebug() {
|
|
// check if sleeplog is available
|
|
if (global.sleeplog) {
|
|
// get debug status, file and duration
|
|
var enabled = !!global.sleeplog.debug;
|
|
var file = typeof global.sleeplog.debug === "object";
|
|
var duration = 0;
|
|
// setup debugging menu
|
|
var debugMenu = {
|
|
"": {
|
|
title: /*LANG*/"Debugging",
|
|
back: () => {
|
|
// check if some value has changed
|
|
if (enabled !== !!global.sleeplog.debug || file !== (typeof global.sleeplog.debug === "object") || duration)
|
|
require("sleeplog").setDebug(enabled, file ? duration || 12 : undefined);
|
|
// redraw main menu
|
|
showMain(7);
|
|
}
|
|
},
|
|
/*LANG*/"View log": () => selectDebug(),
|
|
/*LANG*/"Enable": {
|
|
value: enabled,
|
|
onchange: v => enabled = v
|
|
},
|
|
/*LANG*/"write File": {
|
|
value: file,
|
|
onchange: v => file = v
|
|
},
|
|
/*LANG*/"Duration": {
|
|
value: file ? (global.sleeplog.debug.writeUntil - Date.now()) / 36E5 | 0 : 12,
|
|
min: 1,
|
|
max: 96,
|
|
wrap: true,
|
|
format: v => v + /*LANG*/ "h",
|
|
onchange: v => duration = v
|
|
},
|
|
/*LANG*/"Cancel": () => showMain(7),
|
|
};
|
|
// show menu
|
|
/*var menu =*/ E.showMenu(debugMenu);
|
|
} else {
|
|
// show error prompt
|
|
E.showPrompt("Sleeplog" + /*LANG*/"not enabled!", {
|
|
title: /*LANG*/"Debugging",
|
|
buttons: {
|
|
/*LANG*/"Back": 7
|
|
}
|
|
}).then(showMain);
|
|
}
|
|
}
|
|
|
|
// show menu to change thresholds
|
|
function showThresholds() {
|
|
// setup logging menu
|
|
//var menu;
|
|
var thresholdsMenu = {
|
|
"": {
|
|
title: /*LANG*/"Thresholds",
|
|
back: () => showMain(2)
|
|
},
|
|
/*LANG*/"Max Awake": {
|
|
value: settings.maxAwake / 6E4,
|
|
step: 10,
|
|
min: 10,
|
|
max: 120,
|
|
wrap: true,
|
|
noList: true,
|
|
format: v => v + /*LANG*/"min",
|
|
onchange: v => {
|
|
settings.maxAwake = v * 6E4;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"Min Consecutive": {
|
|
value: settings.minConsec / 6E4,
|
|
step: 10,
|
|
min: 10,
|
|
max: 120,
|
|
wrap: true,
|
|
noList: true,
|
|
format: v => v + /*LANG*/"min",
|
|
onchange: v => {
|
|
settings.minConsec = v * 6E4;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"Deep Sleep": {
|
|
value: settings.deepTh,
|
|
step: 1,
|
|
min: 30,
|
|
max: 200,
|
|
wrap: true,
|
|
noList: true,
|
|
onchange: v => {
|
|
settings.deepTh = v;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"Light Sleep": {
|
|
value: settings.lightTh,
|
|
step: 10,
|
|
min: 100,
|
|
max: 400,
|
|
wrap: true,
|
|
noList: true,
|
|
onchange: v => {
|
|
settings.lightTh = v;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"Wear Temperature": {
|
|
value: settings.wearTemp,
|
|
step: 0.5,
|
|
min: 19.5,
|
|
max: 40,
|
|
wrap: true,
|
|
noList: true,
|
|
format: v => v === 19.5 ? "Disabled" : v + "°C",
|
|
onchange: v => {
|
|
settings.wearTemp = v;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"Reset to Default": () => {
|
|
settings.maxAwake = defaults.maxAwake;
|
|
settings.minConsec = defaults.minConsec;
|
|
settings.deepTh = defaults.deepTh;
|
|
settings.lightTh = defaults.lightTh;
|
|
writeSetting();
|
|
showThresholds();
|
|
}
|
|
};
|
|
|
|
// display info/warning prompt or menu
|
|
if (thresholdsPrompt) {
|
|
thresholdsPrompt = false;
|
|
E.showPrompt("Changes take effect from now on, not retrospective", {
|
|
title: /*LANG*/"Thresholds",
|
|
buttons: {
|
|
/*LANG*/"Ok": 0
|
|
}
|
|
}).then(() => /*menu =*/ E.showMenu(thresholdsMenu));
|
|
} else {
|
|
/*menu =*/ E.showMenu(thresholdsMenu);
|
|
}
|
|
}
|
|
|
|
// show main menu
|
|
function showMain(selected) {
|
|
// set debug image
|
|
var debugImg = !global.sleeplog ?
|
|
"FBSBAOAAfwAP+AH3wD4+B8Hw+A+fAH/gA/wAH4AB+AA/wAf+APnwHw+D4Hx8A++AH/AA/gAH" : // X
|
|
typeof global.sleeplog.debug === "object" ?
|
|
"FBSBAB/4AQDAF+4BfvAX74F+CBf+gX/oFJKBf+gUkoF/6BSSgX/oFJ6Bf+gX/oF/6BAAgf/4" : // file
|
|
global.sleeplog.debug ?
|
|
"FBSBAP//+f/V///4AAGAABkAAZgAGcABjgAYcAGDgBhwAY4AGcABmH+ZB/mAABgAAYAAH///" : // console
|
|
0; // off
|
|
debugImg = debugImg ? "\0" + atob(debugImg) : false;
|
|
// set menu
|
|
var mainMenu = {
|
|
"": {
|
|
title: "Sleep Log",
|
|
back: back,
|
|
selected: selected
|
|
},
|
|
/*LANG*/"Thresholds": () => showThresholds(),
|
|
/*LANG*/"Break ToD": {
|
|
value: settings.breakToD,
|
|
step: 1,
|
|
min: 0,
|
|
max: 23,
|
|
wrap: true,
|
|
noList: true,
|
|
format: v => v + ":00",
|
|
onchange: v => {
|
|
settings.breakToD = v;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"App Timeout": {
|
|
value: settings.appTimeout / 1E3,
|
|
step: 10,
|
|
min: 0,
|
|
max: 120,
|
|
wrap: true,
|
|
noList: true,
|
|
format: v => v ? v + "s" : "-",
|
|
onchange: v => {
|
|
settings.appTimeout = v * 1E3;
|
|
writeSetting();
|
|
}
|
|
},
|
|
/*LANG*/"Enabled": {
|
|
value: settings.enabled,
|
|
onchange: v => {
|
|
settings.enabled = v;
|
|
require("sleeplog").setEnabled(v);
|
|
}
|
|
},
|
|
/*LANG*/"Debugging": {
|
|
value: debugImg,
|
|
onchange: () => setTimeout(showDebug, 10)
|
|
}
|
|
};
|
|
/*var menu =*/ E.showMenu(mainMenu);
|
|
}
|
|
|
|
// draw main menu
|
|
showMain();
|
|
})
|