Merge branch 'espruino:master' into master
|
@ -11,3 +11,6 @@ tests/Layout/testresult.bmp
|
||||||
apps.local.json
|
apps.local.json
|
||||||
_site
|
_site
|
||||||
.jekyll-cache
|
.jekyll-cache
|
||||||
|
.owncloudsync.log
|
||||||
|
Desktop.ini
|
||||||
|
.sync_*.db*
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
|
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
|
||||||
0.07: Fix bug on the cutting edge firmware
|
0.07: Fix bug on the cutting edge firmware
|
||||||
0.08: Use default Bangle formatter for booleans
|
0.08: Use default Bangle formatter for booleans
|
||||||
|
0.09: New app screen (instead of showing settings or the alert) and some optimisations
|
|
@ -0,0 +1,37 @@
|
||||||
|
(function () {
|
||||||
|
// load variable before defining functions cause it can trigger a ReferenceError
|
||||||
|
const activityreminder = require("activityreminder");
|
||||||
|
const storage = require("Storage");
|
||||||
|
let activityreminder_data = activityreminder.loadData();
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
E.showPrompt("Inactivity detected", {
|
||||||
|
title: "Activity reminder",
|
||||||
|
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
|
||||||
|
}).then(function (v) {
|
||||||
|
if (v == 1) {
|
||||||
|
activityreminder_data.okDate = new Date();
|
||||||
|
}
|
||||||
|
if (v == 2) {
|
||||||
|
activityreminder_data.dismissDate = new Date();
|
||||||
|
}
|
||||||
|
if (v == 3) {
|
||||||
|
activityreminder_data.pauseDate = new Date();
|
||||||
|
}
|
||||||
|
activityreminder.saveData(activityreminder_data);
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obey system quiet mode:
|
||||||
|
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
|
||||||
|
Bangle.buzz(400);
|
||||||
|
}
|
||||||
|
setTimeout(load, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
run();
|
||||||
|
|
||||||
|
})();
|
|
@ -1,46 +1,54 @@
|
||||||
(function () {
|
(function () {
|
||||||
// load variable before defining functions cause it can trigger a ReferenceError
|
// load variable before defining functions cause it can trigger a ReferenceError
|
||||||
const activityreminder = require("activityreminder");
|
const activityreminder = require("activityreminder");
|
||||||
const storage = require("Storage");
|
|
||||||
const activityreminder_settings = activityreminder.loadSettings();
|
|
||||||
let activityreminder_data = activityreminder.loadData();
|
let activityreminder_data = activityreminder.loadData();
|
||||||
|
let W = g.getWidth();
|
||||||
|
// let H = g.getHeight();
|
||||||
|
|
||||||
function drawAlert() {
|
function getHoursMins(date){
|
||||||
E.showPrompt("Inactivity detected", {
|
var h = date.getHours();
|
||||||
title: "Activity reminder",
|
var m = date.getMinutes();
|
||||||
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
|
return (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
|
||||||
}).then(function (v) {
|
|
||||||
if (v == 1) {
|
|
||||||
activityreminder_data.okDate = new Date();
|
|
||||||
}
|
}
|
||||||
if (v == 2) {
|
|
||||||
activityreminder_data.dismissDate = new Date();
|
|
||||||
}
|
|
||||||
if (v == 3) {
|
|
||||||
activityreminder_data.pauseDate = new Date();
|
|
||||||
}
|
|
||||||
activityreminder.saveData(activityreminder_data);
|
|
||||||
load();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Obey system quiet mode:
|
function drawData(name, value, y){
|
||||||
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
|
g.drawString(name, 10, y);
|
||||||
Bangle.buzz(400);
|
g.drawString(value, 100, y);
|
||||||
}
|
}
|
||||||
setTimeout(load, 20000);
|
|
||||||
|
function drawInfo() {
|
||||||
|
var h=18, y = h;
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.setFont("Vector",h).setFontAlign(-1,-1);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
g.drawLine(0,25,W,25);
|
||||||
|
g.drawLine(0,26,W,26);
|
||||||
|
|
||||||
|
g.drawString("Current Cycle", 10, y+=h);
|
||||||
|
drawData("Start", getHoursMins(activityreminder_data.stepsDate), y+=h);
|
||||||
|
drawData("Steps", getCurrentSteps(), y+=h);
|
||||||
|
|
||||||
|
/*
|
||||||
|
g.drawString("Button Press", 10, y+=h*2);
|
||||||
|
drawData("Ok", getHoursMins(activityreminder_data.okDate), y+=h);
|
||||||
|
drawData("Dismiss", getHoursMins(activityreminder_data.dismissDate), y+=h);
|
||||||
|
drawData("Pause", getHoursMins(activityreminder_data.pauseDate), y+=h);
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSteps(){
|
||||||
|
let health = Bangle.getHealthStatus("day");
|
||||||
|
return health.steps - activityreminder_data.stepsOnDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
|
|
||||||
drawAlert();
|
|
||||||
} else {
|
|
||||||
eval(storage.read("activityreminder.settings.js"))(() => load());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g.clear();
|
g.clear();
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
drawInfo();
|
||||||
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
||||||
})();
|
})();
|
|
@ -27,8 +27,8 @@
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activityreminder.mustAlert(activityreminder_data, activityreminder_settings)) {
|
if (mustAlert(now)) {
|
||||||
load('activityreminder.app.js');
|
load('activityreminder.alert.js');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mustAlert(now) {
|
||||||
|
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
|
||||||
|
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
|
||||||
|
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
|
||||||
|
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Bangle.on('midnight', function () {
|
Bangle.on('midnight', function () {
|
||||||
/*
|
/*
|
||||||
Usefull trick to have the app working smothly for people using it at night
|
Usefull trick to have the app working smothly for people using it at night
|
||||||
|
|
|
@ -42,15 +42,3 @@ exports.loadData = function () {
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.mustAlert = function(activityreminder_data, activityreminder_settings) {
|
|
||||||
let now = new Date();
|
|
||||||
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
|
|
||||||
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
|
|
||||||
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
|
|
||||||
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Activity Reminder",
|
"name": "Activity Reminder",
|
||||||
"shortName":"Activity Reminder",
|
"shortName":"Activity Reminder",
|
||||||
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
|
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
|
||||||
"version":"0.08",
|
"version":"0.09",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
"tags": "tool,activity",
|
"tags": "tool,activity",
|
||||||
|
@ -13,11 +13,12 @@
|
||||||
{"name": "activityreminder.app.js", "url":"app.js"},
|
{"name": "activityreminder.app.js", "url":"app.js"},
|
||||||
{"name": "activityreminder.boot.js", "url": "boot.js"},
|
{"name": "activityreminder.boot.js", "url": "boot.js"},
|
||||||
{"name": "activityreminder.settings.js", "url": "settings.js"},
|
{"name": "activityreminder.settings.js", "url": "settings.js"},
|
||||||
|
{"name": "activityreminder.alert.js", "url": "alert.js"},
|
||||||
{"name": "activityreminder", "url": "lib.js"},
|
{"name": "activityreminder", "url": "lib.js"},
|
||||||
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
|
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
{"name": "activityreminder.s.json"},
|
{"name": "activityreminder.s.json"},
|
||||||
{"name": "activityreminder.data.json"}
|
{"name": "activityreminder.data.json", "storageFile": true}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
const activityreminder = require("activityreminder");
|
const activityreminder = require("activityreminder");
|
||||||
let settings = activityreminder.loadSettings();
|
let settings = activityreminder.loadSettings();
|
||||||
|
|
||||||
// Show the menu
|
function getMainMenu(){
|
||||||
E.showMenu({
|
var mainMenu = {
|
||||||
"": { "title": "Activity Reminder" },
|
"": { "title": "Activity Reminder" },
|
||||||
"< Back": () => back(),
|
"< Back": () => back(),
|
||||||
'Enable': {
|
'Enable': {
|
||||||
|
@ -76,5 +76,11 @@
|
||||||
activityreminder.writeSettings(settings);
|
activityreminder.writeSettings(settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
return mainMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the menu
|
||||||
|
E.showMenu(getMainMenu());
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: Basic agenda with events from GB
|
0.01: Basic agenda with events from GB
|
||||||
0.02: Added settings page to force calendar sync
|
0.02: Added settings page to force calendar sync
|
||||||
0.03: Disable past events display from settings
|
0.03: Disable past events display from settings
|
||||||
|
0.04: Added awareness of allDay field
|
||||||
|
|
|
@ -26,18 +26,21 @@ var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
|
||||||
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
|
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
|
||||||
var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
|
var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
|
||||||
|
|
||||||
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp)
|
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
|
||||||
|
|
||||||
function getDate(timestamp) {
|
function getDate(timestamp) {
|
||||||
return new Date(timestamp*1000);
|
return new Date(timestamp*1000);
|
||||||
}
|
}
|
||||||
function formatDateLong(date, includeDay) {
|
function formatDateLong(date, includeDay, allDay) {
|
||||||
if(includeDay)
|
let shortTime = Locale.time(date,1)+Locale.meridian(date);
|
||||||
return Locale.date(date)+" "+Locale.time(date,1);
|
if(allDay) shortTime = "";
|
||||||
return Locale.time(date,1);
|
if(includeDay || allDay)
|
||||||
|
return Locale.date(date)+" "+shortTime;
|
||||||
|
return shortTime;
|
||||||
}
|
}
|
||||||
function formatDateShort(date) {
|
function formatDateShort(date, allDay) {
|
||||||
return Locale.date(date).replace(/\d\d\d\d/,"")+Locale.time(date,1);
|
return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay?
|
||||||
|
"" : Locale.time(date,1)+Locale.meridian(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
var lines = [];
|
var lines = [];
|
||||||
|
@ -46,7 +49,7 @@ function showEvent(ev) {
|
||||||
if(!ev) return;
|
if(!ev) return;
|
||||||
g.setFont(bodyFont);
|
g.setFont(bodyFont);
|
||||||
//var lines = [];
|
//var lines = [];
|
||||||
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10)
|
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
|
||||||
var titleCnt = lines.length;
|
var titleCnt = lines.length;
|
||||||
var start = getDate(ev.timestamp);
|
var start = getDate(ev.timestamp);
|
||||||
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
|
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
|
||||||
|
@ -54,17 +57,17 @@ function showEvent(ev) {
|
||||||
if (titleCnt) lines.push(""); // add blank line after title
|
if (titleCnt) lines.push(""); // add blank line after title
|
||||||
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
|
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
|
||||||
includeDay = false;
|
includeDay = false;
|
||||||
if(includeDay) {
|
if(includeDay || ev.allDay) {
|
||||||
lines = lines.concat(
|
lines = lines.concat(
|
||||||
/*LANG*/"Start:",
|
/*LANG*/"Start:",
|
||||||
g.wrapString(formatDateLong(start, includeDay), g.getWidth()-10),
|
g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||||
/*LANG*/"End:",
|
/*LANG*/"End:",
|
||||||
g.wrapString(formatDateLong(end, includeDay), g.getWidth()-10));
|
g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||||
} else {
|
} else {
|
||||||
lines = lines.concat(
|
lines = lines.concat(
|
||||||
g.wrapString(Locale.date(start), g.getWidth()-10),
|
g.wrapString(Locale.date(start), g.getWidth()-10),
|
||||||
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay), g.getWidth()-10),
|
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||||
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay), g.getWidth()-10));
|
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||||
}
|
}
|
||||||
if(ev.location)
|
if(ev.location)
|
||||||
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
||||||
|
@ -110,7 +113,7 @@ function showList() {
|
||||||
if (!ev) return;
|
if (!ev) return;
|
||||||
var isPast = false;
|
var isPast = false;
|
||||||
var x = r.x+2, title = ev.title;
|
var x = r.x+2, title = ev.title;
|
||||||
var body = formatDateShort(getDate(ev.timestamp))+"\n"+(ev.location?ev.location:/*LANG*/"No location");
|
var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
|
||||||
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
|
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
|
||||||
if (title) g.setFontAlign(-1,-1).setFont(fontBig)
|
if (title) g.setFontAlign(-1,-1).setFont(fontBig)
|
||||||
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,r.y+2);
|
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x,r.y+2);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "agenda",
|
"id": "agenda",
|
||||||
"name": "Agenda",
|
"name": "Agenda",
|
||||||
"version": "0.03",
|
"version": "0.04",
|
||||||
"description": "Simple agenda",
|
"description": "Simple agenda",
|
||||||
"icon": "agenda.png",
|
"icon": "agenda.png",
|
||||||
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
||||||
|
|
|
@ -13,3 +13,7 @@
|
||||||
0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled.
|
0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled.
|
||||||
0.14: Adds humidity to weather data.
|
0.14: Adds humidity to weather data.
|
||||||
0.15: Added option for a dynamic mode to show widgets only if unlocked.
|
0.15: Added option for a dynamic mode to show widgets only if unlocked.
|
||||||
|
0.16: You can now show your agenda if your calendar is synced with Gadgetbridge.
|
||||||
|
0.17: Fix - Step count was no more shown in the menu.
|
||||||
|
0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set.
|
||||||
|
0.19: Fix - Compatibility with "Digital clock widget"
|
||||||
|
|
|
@ -7,6 +7,7 @@ A very minimalistic clock to mainly show date and time.
|
||||||
The BW clock provides many features and also 3rd party integrations:
|
The BW clock provides many features and also 3rd party integrations:
|
||||||
- Bangle data such as steps, heart rate, battery or charging state.
|
- Bangle data such as steps, heart rate, battery or charging state.
|
||||||
- A timer can be set directly. *Requirement: Scheduler library*
|
- A timer can be set directly. *Requirement: Scheduler library*
|
||||||
|
- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled*
|
||||||
- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
|
- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
|
||||||
- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
|
- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
|
||||||
|
|
||||||
|
@ -28,11 +29,11 @@ to e.g. send a trigger via HomeAssistant once you selected it.
|
||||||
```
|
```
|
||||||
+5min
|
+5min
|
||||||
|
|
|
|
||||||
Bangle -- Timer[Optional] -- Weather[Optional] -- HomeAssistant [Optional]
|
Bangle -- Timer[Optional] -- Agenda 1[Optional] -- Weather[Optional] -- HomeAssistant [Optional]
|
||||||
|
| | | | |
|
||||||
|
Bpm -5min Agenda 2 Temperature Trigger1
|
||||||
| | | |
|
| | | |
|
||||||
Bpm -5min Temperature Trigger1
|
Steps ... ... ...
|
||||||
| | |
|
|
||||||
Steps ... ...
|
|
||||||
|
|
|
|
||||||
Battery
|
Battery
|
||||||
```
|
```
|
||||||
|
|
|
@ -8,10 +8,16 @@ const storage = require('Storage');
|
||||||
* Statics
|
* Statics
|
||||||
*/
|
*/
|
||||||
const SETTINGS_FILE = "bwclk.setting.json";
|
const SETTINGS_FILE = "bwclk.setting.json";
|
||||||
const TIMER_IDX = "bwclk";
|
const TIMER_IDX = "bwclk_timer";
|
||||||
|
const TIMER_AGENDA_IDX = "bwclk_agenda";
|
||||||
const W = g.getWidth();
|
const W = g.getWidth();
|
||||||
const H = g.getHeight();
|
const H = g.getHeight();
|
||||||
|
|
||||||
|
/************
|
||||||
|
* Global data
|
||||||
|
*/
|
||||||
|
var pressureData;
|
||||||
|
|
||||||
/************
|
/************
|
||||||
* Settings
|
* Settings
|
||||||
*/
|
*/
|
||||||
|
@ -33,17 +39,6 @@ for (const key in saved_settings) {
|
||||||
* Assets
|
* Assets
|
||||||
*/
|
*/
|
||||||
// Manrope font
|
// Manrope font
|
||||||
Graphics.prototype.setXLargeFont = function(scale) {
|
|
||||||
// Actual height 53 (55 - 3)
|
|
||||||
this.setFontCustom(
|
|
||||||
E.toString(require('heatshrink').decompress(atob('AHM/8AIG/+AA4sD/wQGh/4EWQA/AC8YA40HNA0BRY8/RY0P/6LFgf//4iFA4IiFj4HBEQkHCAQiDHIIZGv4HCFQY5BDAo5CAAIpDDAfACA3wLYv//hsFKYxcCMgoiBOooiBQwwiBS40AHIgA/ACS/DLYjYCBAjQEBAYQDBAgHDUAbyDZQi3CegoHEVQQZFagUfW4Y0DaAgECaIJSEFYMPbIYNDv5ACGAIrBCgJ1EFYILCAAQWCj4zDGgILCegcDEQRNDHIIiCHgZ2BEQShFIqUDFYidCh5ODg4NCn40DAgd/AYR5BDILZEAAIMDAAYVCh7aHdYhKDbQg4Dv7rGBAihFCAwIDCAgA/AB3/eoa7GAAk/dgbVGDJrvCDK67DDIjaGdYpbCdYonCcQjjDEVUBEQ4A/AEMcAYV/NAUHcYUDawd/cYUPRYSmBBgaLBToP8BgYiBSgIiCj4iCg//EQSuDW4IMDVwYiCBgIiBBgrRDCATeBaIYqCv70DCgT4CEQMfIgQZBBoRnDv/3EQIvBDIffEQMHFwReBRYUfOgX/+IiDKIeHEQRRECwUHKwIuB8AiDIoJEBCwZFCv/4HIZaBIgPAEQS2CUYQiCD4SABEQcfOwIZBEQaHBO4RcEAAI/BEQQgBSIQiDTIRZBEQZuBVYQiDHoKWCEQQICFQIiDBAQeCEQQA/AANwA40BLIJ5BO4JWCBAUPAYR5En7RBUIQECN4SYCQQIiEh6CCEQk/BoQiBgYeCBoTrCAgT0CCgIfCFYQiBg4IBGgIiDj6rBg4rCBYLRDFYIiBbYIfBLgQiBIQYiD4JCCLgf/bQIWDBYV/EQV/BYXz/5FBgIiD5//IowZBD4M/NAX/BIPgDIJoC//5GgKUDn//4f/8KLE/wTBAAI8BEQPwj4HBVwYmBDgIZDN4QZCGYKJCHQP/JoSgCBATrCh5dBKITVDG4gICAAbvDAH5SCL4QADK4J5CCAiTCCAp1BCAqCDCAgiGCAIiFCAQiFeoIiFg6/FCAgiECAXnEQgQB/kfEQYQC4F/EQYQCgIiDfoIQBg4iDCAUAEQZUCcgIiDDIIQBEQhuBBoIiENoYiFDwQiECAQiFwEBPQQNCAQKDDEYMDDoMfRh4iGUwqvEESBiBaQ5oEbgr0FNAo+EEIwA+oAHGgJoFRAMHe4L0CAALNBBAT0BfwScDCAXweAL0DWgUPQYQiDwF/QYQiC/zTB+C0FBAL0CEQYIBGgMPCgIxBg4rCJIKsCh5IBBwTPCj4WBgYLBZ4V/MAIiBBQQrBEQYtCBYQiCO4QLFCwgiDIQIiGIoMHEQpFBn5FFD4JoENwRoGDgSUCAoKfBw//DgIiCT4auCFwN/T4RRET4TaCEQKoCDIQiCGgK/DAAQICdYQACHoIqCBAoQFEwIhFAH4AFQIROEj4IGXwIIGNwIACbgIhEBAiRCVwoqDTogHEW4QZFXgIZB/z9Cv49CF4MPBwI0Ca4LlB8ATCJoP4AoINDfQPAg7PBg4cBBwUfD4MfFYILCCwgOCf4QLEwEPCwILCgJaBn4WBBYQxCIQQiD+EDCYI5CBYRQBIo4fBMQIuBC4N/NAv8AoIcBSgU/FYIIBZIYrCW4hOCXIQZCgYUBv7jEh4uBZAscewZ8CgEgUYT0EEoQIBA4gICFQQIEHYQA+KQzdDAArdCAArpCEScHaIQiEvwiGe4QiFUwQiEbgIiFYIL0DEQTkBEQrJEEQc/cYYiCg4HBDIQiCfoRoEHQLaDEQQHBbQYiBCAT8Dn/BCAoXBJYP/OgZKC/6OEEARLCEQZLEEQZLEEQjKFEQI6EEQZLDEQbsGEQLjGYYYA/JIxzEg/AfgJSDAoPgfgiDC8COFAoPnaQj6CAAR+CW4TCFA4i6CDIqhCDIfwHoYHCYIN/GgKuBJ4JDBFYUf/C5CBYIZBv/Ag4ZBg4rBBYQTBAQIcBg4FBn5UBAQUfFwIfCEQeAgYfBAQUBFAKbCAQQiCGwIiE+A2BwBFNwE/AoM/EQJoIWwKCCh4cBFYKUERYV/W46uHFYIZGaJA0B/glBGYT0JIITiEMIJvCFQQAEHYQA/ABBlEOIhdGQAIRFSgQIBgQICn4IB8EAjiBCUYglCbQYeBEoQZCTwM/CYIZD/gEBUwIzBJ4UHYAU/EwIrBh4rCAoIXCn4rBCgUDAQN/FYMfBYIXBCYJnCBYXggf8HgQLCwEPEQQuBgJOECwILDCwgiLHIUHBYJFGD4IxBgYWCn4rBBwJoFDIYNBCgPADgKHBRYfDBQN/GAIrBToTLDVwYACDILiCWAb8DAAYzBYAjTCAAI9BAARNCBAoqCBAgQDFgbYCAH4AufgQACf4T8CAAT/CfgQACBwITCAAYOBCYQioh4iEAHQA=='))),
|
|
||||||
46,
|
|
||||||
atob("FR4uHyopKyksJSssGA=="),
|
|
||||||
70+(scale<<8)+(1<<16)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
Graphics.prototype.setLargeFont = function(scale) {
|
Graphics.prototype.setLargeFont = function(scale) {
|
||||||
// Actual height 47 (48 - 2)
|
// Actual height 47 (48 - 2)
|
||||||
this.setFontCustom(
|
this.setFontCustom(
|
||||||
|
@ -75,6 +70,19 @@ Graphics.prototype.setSmallFont = function(scale) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Graphics.prototype.setMiniFont = function(scale) {
|
||||||
|
// Actual height 16 (15 - 0)
|
||||||
|
this.setFontCustom(
|
||||||
|
atob('AAAAAAAAAAAAAP+w/5AAAAAA4ADgAOAA4AAAAAAAAAABgBmAGbAb8D+A+YDZ8B/wf4D5gJmAGQAQAAAAAAAeOD8cMwzxj/GPMYwc/Az4AAAAAHAA+DDIYMjA+YBzAAYADeA7MHMw4zDD4ADAAAAz4H/wzjDHMMMwwbBj4APgADAAAAAA4ADgAAAAAAAAAAfwH/54B+ABAAAAAOABeAcf/gfwAAAAACAAaAD4APgAOABgAAAAAAACAAIAAgA/wAMAAgACAAAAAAAAPAA4AAAAAAIAAgACAAIAAgAAAAAAADAAMAAAAAAAcAfwf4D4AIAAAAA/wH/gwDDAMMAwwDB/4D/AAAAAAGAAwAD/8P/wAAAAAHAw8HDA8MHww7DnMH4wGBAAAMBgyHDcMPww/DDv4MfAAAAAAAHgD+A+YPhgwGAH8AfwAEAAAAAA/GD8cMwwzDDMMM5wx+ABgAAAP8B/4MwwzDDMMMwwx+ADwAAAgADAAMBwwfDPgP4A8ADAAAAAe+D/8M4wxjDGMP5wf+ABwAAAfAB+cMYwwjDCMMYwf+A/wAAAAAAAAAxgBCAAAAAAAAAYPBA4AAAAAAAAAgAHAA+AHMAYYAAAAAAAAAAAAAAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAAABhgHMAPgAcAAgAAAAAAAABgAOAAwbDDsMYA/AA4AAAAAAAD4A/wGBgxzDPsMyQjJDPkM+wYIBxgD+AAAAAAABAA8A/gf8DwwODA/sAfwAHwADAAAP/w//DGMMYwxjDOMP9we+ABwA8AP8Bw4MAwwDDAMMAwwDDgcHDgMMAAAAAA//D/8MAwwDDAMMAw4HB/4D/AAAAAAP/w//DGMMYwxjDGMMQwgBAAAP/w//DDAMMAwwDDAMAADwA/wHDgwDDAMMAwwDDCMOJwc+ADwAAA//D/8AMAAwADAAMAAwD/8P/wAAAAAP/w//AAAABgAHAAMAAwAHD/4P+AAAAAAP/w//AOAB+AOcBw4MBwgDAAEAAA//D/8AAwADAAMAAwADAAAP/w//A8AA8AA+AA8AHwB8AeAHgA//D/8AAAAAD/8P/wcAAcAA8AA4AB4P/w//AAAA8AP8Bw4MAwwDDAMMAwwDDgcH/gP8AAAAAA//D/8MMAwwDDAMYA7gB8ABgADwA/wHDgwDDAMMAwwDDA8ODwf/A/8AAAAAD/8P/wwwDDAMMAx4Dv4HxwEBAAAHjg/HDMMMYwxjDGMONwc+ABwMAAwADAAMAA//D/8MAAwADAAIAAAAD/wP/gAHAAMAAwADAAMAHg/8AAAMAA+AA/AAfgAPAA8AfgPwD4AMAAwAD4AD+AA/AA8A/g/gDwAP4AH8AB8APwH8D8AMAAgBDAMPDgO8APAB8AOcDw8MAwgBCAAOAAeAAeAAfwH/B4AOAAwAAAAMAwwPDB8Mew3jD4MPAwwDAAAAAAAAB//3//QABAAAAAAADgAP4AH+AB8AAQAABAAEAAf/9//wAAAAAAAAAAGAAwAGAAwABgADAAGAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAQA3wHbAZMBswGzAf4A/wAAAAAP/w//AYMBgwGDAYMA/gB8AAAAEAD+Ae8BgwGDAYMBgwDGAAAAMAD+Ae8BgwGDAYMBhw//D/8AAAAYAP4B/wGTAZMBkwGTAP4AcAEAAYAP/w//CQAJAAAwAP4hz3GDMQMxAzGHcf/h/8AAAAAP/w//AYABgAGAAYAA/wB/AAAAAA3/Df8AAAAAOf/9//AAAAAP/w//ADgAfADGAYMBAQAAD/8P/wAAAAAB/wH/AYABgAGAAf8A/wGAAYABgAH/AP8AAAAAAf8B/wGAAYABgAGAAP8AfwAAADAA/gHvAYMBgwGDAYMA/gB8AAAAAAH/8f/xgwGDAYMBgwD+AHwAAAAwAP4B7wGDAYMBgwGHAf/x//AAAAAB/wH/AYABgAEAAAAA5gHzAbMBkwGbAd8AzgEAAYAP/wf/AQMBAwAAAAAB/gH/AAMAAwADAAcB/wH/AAABAAHgAPwAHwAPAH4B8AGAAQAB8AB+AA8APwHwAeAA/AAPAD8B+AHAAQEBgwHOAHwAOAD+AccBAwAAAQAB4AD4EB/wB8A/APgBwAAAAAEBgwGPAZ8B8wHjAcMBAQAAAAAABgf/9/n2AAAAAAAP/w//AAAEAAYAB/nz//AGAAAAAAAAAAAAcABgAGAAcAAwAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'),
|
||||||
|
32,
|
||||||
|
atob("AwUHDwoOCwQHBwcJBAcEBgoGCQkKCQoICQoFBQoMCgkPCgoMCwkICwsECAoIDgsMCgwKCgoLCg8KCQoHBgcLCwgJCgkKCQYKCgQECAQOCgoKCgYIBwoIDAkJCAcEBwsQ"),
|
||||||
|
16+(scale<<8)+(1<<16)
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function imgLock(){
|
function imgLock(){
|
||||||
return {
|
return {
|
||||||
width : 16, height : 16, bpp : 1,
|
width : 16, height : 16, bpp : 1,
|
||||||
|
@ -165,12 +173,27 @@ function imgWatch() {
|
||||||
|
|
||||||
function imgHomeAssistant() {
|
function imgHomeAssistant() {
|
||||||
return {
|
return {
|
||||||
width : 48, height : 48, bpp : 1,
|
width : 24, height : 24, bpp : 1,
|
||||||
transparent : 0,
|
transparent : 1,
|
||||||
buffer : require("heatshrink").decompress(atob("AD8BwAFDg/gAocP+AFDj4FEn/8Aod//wFD/1+FAf4j+8AoMD+EPDAUH+OPAoUP+fPAoUfBYk/C4l/EYIwC//8n//FwIFEgYFD4EH+E8nkP8BdBAonjjk44/wj/nzk58/4gAFDF4PgCIMHAoPwhkwh4FB/EEkEfIIWAHwIFC4A+BAoXgg4FDL4IFDL4IFDLIYFkAEQA=="))
|
buffer : require("heatshrink").decompress(atob("/4AF84CB4YCBwICBCAP+jFH/k8g/4kkH+AFB8ACB4cY4eHzPhgmZkHnzPn8fb4/gvwUD8EYARhAC"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imgAgenda() {
|
||||||
|
return {
|
||||||
|
width : 24, height : 24, bpp : 1,
|
||||||
|
transparent : 1,
|
||||||
|
buffer : require("heatshrink").decompress(atob("/4AFnPP+ALBAQX4CIgLFAQvggEBAQvAgEDAQMCwEAgwTBhgiB/AlCGQ8BGQQ"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function imgMountain() {
|
||||||
|
return {
|
||||||
|
width : 24, height : 24, bpp : 1,
|
||||||
|
transparent : 1,
|
||||||
|
buffer : atob("//////////////////////3///n///D//uZ//E8//A+/+Z+f8//P5//n7//3z//zn//5AAAAAAAA////////////////////")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/************
|
/************
|
||||||
* 2D MENU with entries of:
|
* 2D MENU with entries of:
|
||||||
|
@ -186,6 +209,7 @@ var menu = [
|
||||||
function(){ return [ E.getBattery() + "%", Bangle.isCharging() ? imgCharging() : imgBattery() ] },
|
function(){ return [ E.getBattery() + "%", Bangle.isCharging() ? imgCharging() : imgBattery() ] },
|
||||||
function(){ return [ getSteps(), imgSteps() ] },
|
function(){ return [ getSteps(), imgSteps() ] },
|
||||||
function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] },
|
function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] },
|
||||||
|
function(){ return [ getAltitude(), imgMountain() ]},
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -196,14 +220,90 @@ try{
|
||||||
require('sched');
|
require('sched');
|
||||||
menu.push([
|
menu.push([
|
||||||
function(){
|
function(){
|
||||||
var text = isAlarmEnabled() ? getAlarmMinutes() + " min." : "Timer";
|
var text = isAlarmEnabled(TIMER_IDX) ? getAlarmMinutes(TIMER_IDX) + " min." : "Timer";
|
||||||
return [text, imgTimer(), () => decreaseAlarm(), () => increaseAlarm(), null ]
|
return [text, imgTimer(), () => decreaseAlarm(TIMER_IDX), () => increaseAlarm(TIMER_IDX), null ]
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
// If sched is not installed, we hide this menu item
|
// If sched is not installed, we hide this menu item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AGENDA MENU
|
||||||
|
* Note that we handle the agenda differently in order to hide old entries...
|
||||||
|
*/
|
||||||
|
var agendaIdx = 0;
|
||||||
|
var agendaTimerIdx = 0;
|
||||||
|
if(storage.readJSON("android.calendar.json") !== undefined){
|
||||||
|
function nextAgendaEntry(){
|
||||||
|
agendaIdx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousAgendaEntry(){
|
||||||
|
agendaIdx -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.push([
|
||||||
|
function(){
|
||||||
|
var now = new Date();
|
||||||
|
var agenda = storage.readJSON("android.calendar.json")
|
||||||
|
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
|
||||||
|
.sort((a,b)=>a.timestamp - b.timestamp);
|
||||||
|
|
||||||
|
if(agenda.length <= 0){
|
||||||
|
return ["All done", imgAgenda()]
|
||||||
|
}
|
||||||
|
|
||||||
|
agendaIdx = agendaIdx < 0 ? 0 : agendaIdx;
|
||||||
|
agendaIdx = agendaIdx >= agenda.length ? agendaIdx -1 : agendaIdx;
|
||||||
|
|
||||||
|
var entry = agenda[agendaIdx];
|
||||||
|
var title = entry.title.slice(0,14);
|
||||||
|
var date = new Date(entry.timestamp*1000);
|
||||||
|
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
|
||||||
|
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
|
||||||
|
|
||||||
|
function dynImgAgenda(){
|
||||||
|
if(isAlarmEnabled(TIMER_AGENDA_IDX) && agendaTimerIdx == agendaIdx){
|
||||||
|
return imgTimer();
|
||||||
|
} else {
|
||||||
|
return imgAgenda();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [title + "\n" + dateStr, dynImgAgenda(), () => nextAgendaEntry(), () => previousAgendaEntry(), function(){
|
||||||
|
try{
|
||||||
|
var alarm = require('sched')
|
||||||
|
|
||||||
|
// If other time, we disable the old one and enable this one.
|
||||||
|
if(agendaIdx != agendaTimerIdx){
|
||||||
|
agendaTimerIdx = -1;
|
||||||
|
alarm.setAlarm(TIMER_AGENDA_IDX, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable alarm if enabled
|
||||||
|
if(isAlarmEnabled(TIMER_AGENDA_IDX)){
|
||||||
|
agendaTimerIdx = -1;
|
||||||
|
alarm.setAlarm(TIMER_AGENDA_IDX, undefined);
|
||||||
|
alarm.reload();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, set alarm for given event
|
||||||
|
agendaTimerIdx = agendaIdx;
|
||||||
|
alarm.setAlarm(TIMER_AGENDA_IDX, {
|
||||||
|
msg: title,
|
||||||
|
timer : parseInt((date - now)),
|
||||||
|
});
|
||||||
|
alarm.reload();
|
||||||
|
} catch(ex){ }
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* WEATHER MENU
|
* WEATHER MENU
|
||||||
*/
|
*/
|
||||||
|
@ -280,6 +380,14 @@ function getSteps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getAltitude(){
|
||||||
|
if(pressureData && pressureData.altitude){
|
||||||
|
return Math.round(pressureData.altitude) + "m";
|
||||||
|
}
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function getWeather(){
|
function getWeather(){
|
||||||
var weatherJson;
|
var weatherJson;
|
||||||
|
|
||||||
|
@ -314,10 +422,10 @@ function getWeather(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isAlarmEnabled(){
|
function isAlarmEnabled(idx){
|
||||||
try{
|
try{
|
||||||
var alarm = require('sched');
|
var alarm = require('sched');
|
||||||
var alarmObj = alarm.getAlarm(TIMER_IDX);
|
var alarmObj = alarm.getAlarm(idx);
|
||||||
if(alarmObj===undefined || !alarmObj.on){
|
if(alarmObj===undefined || !alarmObj.on){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -329,22 +437,22 @@ function isAlarmEnabled(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getAlarmMinutes(){
|
function getAlarmMinutes(idx){
|
||||||
if(!isAlarmEnabled()){
|
if(!isAlarmEnabled(idx)){
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var alarm = require('sched');
|
var alarm = require('sched');
|
||||||
var alarmObj = alarm.getAlarm(TIMER_IDX);
|
var alarmObj = alarm.getAlarm(idx);
|
||||||
return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000));
|
return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function increaseAlarm(){
|
function increaseAlarm(idx){
|
||||||
try{
|
try{
|
||||||
var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0;
|
var minutes = isAlarmEnabled(idx) ? getAlarmMinutes(idx) : 0;
|
||||||
var alarm = require('sched')
|
var alarm = require('sched');
|
||||||
alarm.setAlarm(TIMER_IDX, {
|
alarm.setAlarm(idx, {
|
||||||
timer : (minutes+5)*60*1000,
|
timer : (minutes+5)*60*1000,
|
||||||
});
|
});
|
||||||
alarm.reload();
|
alarm.reload();
|
||||||
|
@ -352,16 +460,16 @@ function increaseAlarm(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function decreaseAlarm(){
|
function decreaseAlarm(idx){
|
||||||
try{
|
try{
|
||||||
var minutes = getAlarmMinutes();
|
var minutes = getAlarmMinutes(idx);
|
||||||
minutes -= 5;
|
minutes -= 5;
|
||||||
|
|
||||||
var alarm = require('sched')
|
var alarm = require('sched')
|
||||||
alarm.setAlarm(TIMER_IDX, undefined);
|
alarm.setAlarm(idx, undefined);
|
||||||
|
|
||||||
if(minutes > 0){
|
if(minutes > 0){
|
||||||
alarm.setAlarm(TIMER_IDX, {
|
alarm.setAlarm(idx, {
|
||||||
timer : minutes*60*1000,
|
timer : minutes*60*1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -371,6 +479,19 @@ function decreaseAlarm(){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleAsyncData(){
|
||||||
|
try{
|
||||||
|
|
||||||
|
if (settings.menuPosX == 1){
|
||||||
|
Bangle.getPressure().then(data=>{
|
||||||
|
pressureData = data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}catch(ex){ }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/************
|
/************
|
||||||
* DRAW
|
* DRAW
|
||||||
*/
|
*/
|
||||||
|
@ -378,6 +499,9 @@ function draw() {
|
||||||
// Queue draw again
|
// Queue draw again
|
||||||
queueDraw();
|
queueDraw();
|
||||||
|
|
||||||
|
// Now lets measure some data..
|
||||||
|
handleAsyncData();
|
||||||
|
|
||||||
// Draw clock
|
// Draw clock
|
||||||
drawDate();
|
drawDate();
|
||||||
drawTime();
|
drawTime();
|
||||||
|
@ -437,17 +561,13 @@ function drawTime(){
|
||||||
y += parseInt((H - y)/2) + 5;
|
y += parseInt((H - y)/2) + 5;
|
||||||
|
|
||||||
var menuEntry = getMenuEntry();
|
var menuEntry = getMenuEntry();
|
||||||
var menuName = menuEntry[0];
|
var menuName = String(menuEntry[0]);
|
||||||
var menuImg = menuEntry[1];
|
var menuImg = menuEntry[1];
|
||||||
var printImgLeft = settings.menuPosY != 0;
|
var printImgLeft = settings.menuPosY != 0;
|
||||||
|
|
||||||
// Show large or small time depending on info entry
|
// Show large or small time depending on info entry
|
||||||
if(menuName == null){
|
if(menuName == null){
|
||||||
if(settings.hideColon){
|
|
||||||
g.setXLargeFont();
|
|
||||||
} else {
|
|
||||||
g.setLargeFont();
|
g.setLargeFont();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
y -= 15;
|
y -= 15;
|
||||||
g.setMediumFont();
|
g.setMediumFont();
|
||||||
|
@ -461,7 +581,13 @@ function drawTime(){
|
||||||
|
|
||||||
y += 35;
|
y += 35;
|
||||||
g.setFontAlign(0,0);
|
g.setFontAlign(0,0);
|
||||||
|
|
||||||
|
if(menuName.split('\n').length > 1){
|
||||||
|
g.setMiniFont();
|
||||||
|
} else {
|
||||||
g.setSmallFont();
|
g.setSmallFont();
|
||||||
|
}
|
||||||
|
|
||||||
var imgWidth = 0;
|
var imgWidth = 0;
|
||||||
if(menuImg !== undefined){
|
if(menuImg !== undefined){
|
||||||
imgWidth = 24.0;
|
imgWidth = 24.0;
|
||||||
|
@ -469,7 +595,7 @@ function drawTime(){
|
||||||
var scale = imgWidth / menuImg.width;
|
var scale = imgWidth / menuImg.width;
|
||||||
g.drawImage(
|
g.drawImage(
|
||||||
menuImg,
|
menuImg,
|
||||||
W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - parseInt(imgWidth/2),
|
W/2 + (printImgLeft ? -strWidth/2-4 : strWidth/2+4) - parseInt(imgWidth/2),
|
||||||
y - parseInt(imgWidth/2),
|
y - parseInt(imgWidth/2),
|
||||||
{ scale: scale }
|
{ scale: scale }
|
||||||
);
|
);
|
||||||
|
@ -590,6 +716,9 @@ Bangle.on('touch', function(btn, e){
|
||||||
}
|
}
|
||||||
|
|
||||||
if(is_right){
|
if(is_right){
|
||||||
|
// A bit hacky but we ensure that always the first agenda entry is shown...
|
||||||
|
agendaIdx = 0;
|
||||||
|
|
||||||
Bangle.buzz(40, 0.6);
|
Bangle.buzz(40, 0.6);
|
||||||
settings.menuPosX = (settings.menuPosX+1) % menu.length;
|
settings.menuPosX = (settings.menuPosX+1) % menu.length;
|
||||||
settings.menuPosY = 0;
|
settings.menuPosY = 0;
|
||||||
|
@ -597,6 +726,9 @@ Bangle.on('touch', function(btn, e){
|
||||||
}
|
}
|
||||||
|
|
||||||
if(is_left){
|
if(is_left){
|
||||||
|
// A bit hacky but we ensure that always the first agenda entry is shown...
|
||||||
|
agendaIdx = 0;
|
||||||
|
|
||||||
Bangle.buzz(40, 0.6);
|
Bangle.buzz(40, 0.6);
|
||||||
settings.menuPosY = 0;
|
settings.menuPosY = 0;
|
||||||
settings.menuPosX = settings.menuPosX-1;
|
settings.menuPosX = settings.menuPosX-1;
|
||||||
|
@ -606,12 +738,13 @@ Bangle.on('touch', function(btn, e){
|
||||||
|
|
||||||
if(is_center){
|
if(is_center){
|
||||||
var menuEntry = getMenuEntry();
|
var menuEntry = getMenuEntry();
|
||||||
if(menuEntry.length > 4){
|
if(menuEntry.length > 4 && menuEntry[4] != null){
|
||||||
Bangle.buzz(80, 0.6).then(()=>{
|
Bangle.buzz(80, 0.6).then(()=>{
|
||||||
try{
|
try{
|
||||||
menuEntry[4]();
|
menuEntry[4]();
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
Bangle.buzz(80, 0.6);
|
Bangle.buzz(80, 0.6);
|
||||||
|
drawTime();
|
||||||
}, 250);
|
}, 250);
|
||||||
} catch(ex){
|
} catch(ex){
|
||||||
// In case it fails, we simply ignore it.
|
// In case it fails, we simply ignore it.
|
||||||
|
@ -640,6 +773,9 @@ E.on("kill", function(){
|
||||||
// dark/light theme as well as the colors.
|
// dark/light theme as well as the colors.
|
||||||
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
|
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
|
||||||
|
|
||||||
|
// Show launcher when middle button pressed
|
||||||
|
Bangle.setUI("clock");
|
||||||
|
|
||||||
// Load widgets and draw clock the first time
|
// Load widgets and draw clock the first time
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
|
||||||
|
@ -649,6 +785,3 @@ for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;}
|
||||||
|
|
||||||
// Draw first time
|
// Draw first time
|
||||||
draw();
|
draw();
|
||||||
|
|
||||||
// Show launcher when middle button pressed
|
|
||||||
Bangle.setUI("clock");
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "bwclk",
|
"id": "bwclk",
|
||||||
"name": "BW Clock",
|
"name": "BW Clock",
|
||||||
"version": "0.15",
|
"version": "0.19",
|
||||||
"description": "A very minimalistic clock to mainly show date and time.",
|
"description": "A very minimalistic clock to mainly show date and time.",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
|
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: Initial version
|
||||||
|
0.02: More compact rendering & app icon
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Calendar Clock - Your day at a glance
|
||||||
|
|
||||||
|
This clock shows a chronological view of your current and future events.
|
||||||
|
It uses events synced from Gadgetbridge to achieve this.
|
||||||
|
|
||||||
|
The current time and date is highlighted in cyan.
|
||||||
|
|
||||||
|
## Screenshot
|
||||||
|

|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwgpm5gAB4AVRhgWCAAQWWDCARC/4ACJR4uB54WDAAP8DBotFGIgXLFwv4GAouQC4gwMLooXF/gXJOowXGJBIXBCIgXQxgXLMAIXXMAmIC5OIx4XJhH/wAXIxnIC78IxGIHoIABI44MBC4wQBEQIDB5gXGPAJgEC6IxBC5oABC4wwDa4YTCxAWD5nPDAzvGFYgAB5AXWJBK+GcAq5CGBIuBC5X4GBIJBdoQXB/GIx4CDPJAuEC5JoCDAgWBFwYXJxCBIFwYXKYwoACCwZ3IPQoWIC5YABGYIABCwpHKAQYMBCwwX/C5QAMC8R3/R/4XNhAXNwAXHgGIABgWIAFwA=="))
|
|
@ -0,0 +1,119 @@
|
||||||
|
var calendar = [];
|
||||||
|
var current = [];
|
||||||
|
var next = [];
|
||||||
|
|
||||||
|
function updateCalendar() {
|
||||||
|
calendar = require("Storage").readJSON("android.calendar.json",true)||[];
|
||||||
|
calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp);
|
||||||
|
calendar.sort((a,b) => a.timestamp - b.timestamp);
|
||||||
|
|
||||||
|
current = calendar.filter(isActive);
|
||||||
|
next = calendar.filter(e=>!isActive(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(event) {
|
||||||
|
var timeActive = getTime() - event.timestamp;
|
||||||
|
return timeActive >= 0 && timeActive <= event.durationInSeconds;
|
||||||
|
}
|
||||||
|
function zp(str) {
|
||||||
|
return ("0"+str).substr(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEventHeader(event, y) {
|
||||||
|
g.setFont("Vector", 24);
|
||||||
|
|
||||||
|
var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000);
|
||||||
|
var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes());
|
||||||
|
g.drawString(timeStr, 5, y);
|
||||||
|
y += 24;
|
||||||
|
|
||||||
|
g.setFont("12x20", 1);
|
||||||
|
if (isActive(event)) {
|
||||||
|
g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21);
|
||||||
|
} else {
|
||||||
|
var offset = 0-time.getTimezoneOffset()/1440;
|
||||||
|
var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset);
|
||||||
|
if(days > 0) {
|
||||||
|
var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days";
|
||||||
|
g.drawString(daysStr,15*timeStr.length,y-21);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEventBody(event, y) {
|
||||||
|
g.setFont("12x20", 1);
|
||||||
|
var lines = g.wrapString(event.title, g.getWidth()-10);
|
||||||
|
if (lines.length > 2) {
|
||||||
|
lines = lines.slice(0,2);
|
||||||
|
lines[1] = lines[1].slice(0,-3)+"...";
|
||||||
|
}
|
||||||
|
g.drawString(lines.join('\n'), 5, y);
|
||||||
|
y+=20 * lines.length;
|
||||||
|
if(event.location) {
|
||||||
|
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y);
|
||||||
|
g.drawString(event.location, 20, y);
|
||||||
|
y+=20;
|
||||||
|
}
|
||||||
|
y+=5;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawEvent(event, y) {
|
||||||
|
y = drawEventHeader(event, y);
|
||||||
|
y = drawEventBody(event, y);
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
var curEventHeight = 0;
|
||||||
|
|
||||||
|
function drawCurrentEvents(y) {
|
||||||
|
g.setColor("#0ff");
|
||||||
|
g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight);
|
||||||
|
curEventHeight = y;
|
||||||
|
|
||||||
|
if(current.length === 0) {
|
||||||
|
y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y);
|
||||||
|
} else {
|
||||||
|
y = drawEventHeader(current[0], y);
|
||||||
|
for (var e of current) {
|
||||||
|
y = drawEventBody(e, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
curEventHeight = y - curEventHeight;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFutureEvents(y) {
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
for (var e of next) {
|
||||||
|
y = drawEvent(e, y);
|
||||||
|
if(y>g.getHeight())break;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fullRedraw() {
|
||||||
|
g.clearRect(5,24,g.getWidth()-5,g.getHeight());
|
||||||
|
updateCalendar();
|
||||||
|
var y = 30;
|
||||||
|
y = drawCurrentEvents(y);
|
||||||
|
drawFutureEvents(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redraw() {
|
||||||
|
g.reset();
|
||||||
|
if (current.find(e=>!isActive(e)) || next.find(isActive)) {
|
||||||
|
fullRedraw();
|
||||||
|
} else {
|
||||||
|
drawCurrentEvents(30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
fullRedraw();
|
||||||
|
var minuteInterval = setInterval(redraw, 60 * 1000);
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
Bangle.setUI("clock");
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 692 B |
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"id": "calclock",
|
||||||
|
"name": "Calendar Clock",
|
||||||
|
"shortName": "CalClock",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "Show the current and upcoming events synchronized from Gadgetbridge",
|
||||||
|
"icon": "calclock.png",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock agenda",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"calclock.app.js","url":"calclock.js"},
|
||||||
|
{"name":"calclock.img","url":"calclock-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"screenshots": [{"url":"screenshot.png"}]
|
||||||
|
}
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: Initial Creation
|
||||||
|
0.02: Fixed some sleep bugs. Added a sleep mode toggle
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Chimer - For the BangleJS
|
||||||
|
|
||||||
|
A fork of [Hour Chime](https://github.com/espruino/BangleApps/tree/master/apps/widchime) that adds extra features such as:
|
||||||
|
|
||||||
|
- Buzz or beep on every 60, 30 or 15 minutes.
|
||||||
|
- Repeat Chime up to 3 times
|
||||||
|
- Set hours to disable chime
|
||||||
|
|
||||||
|
Setting the hours you don't want your watch to chime for is done by setting the hour you want it to stop, and the hour you want it to start.
|
||||||
|
|
||||||
|
Hours range from 0 - 23.
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
widget.png: "https://icons8.com/icon/114436/alarm"
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"id": "chimer",
|
||||||
|
"name": "Chimer",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Reapeat Chime up to 3 times \n - Set hours to disable chime",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "widget",
|
||||||
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
|
"readme": "README.MD",
|
||||||
|
"storage": [
|
||||||
|
{ "name": "chimer.wid.js", "url": "widget.js" },
|
||||||
|
{ "name": "chimer.settings.js", "url": "settings.js" }
|
||||||
|
],
|
||||||
|
"data": [{ "name": "chimer.json" }]
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* @param {function} back Use back() to return to settings menu
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (back) {
|
||||||
|
// default to buzzing
|
||||||
|
var FILE = "chimer.json";
|
||||||
|
var settings = {};
|
||||||
|
const chimes = ["Off", "Buzz", "Beep", "Both"];
|
||||||
|
const frequency = ["60 min", "30 min", "15 min", "1 min"];
|
||||||
|
|
||||||
|
var showMainMenu = () => {
|
||||||
|
E.showMenu({
|
||||||
|
"": { title: "Chimer" },
|
||||||
|
"< Back": () => back(),
|
||||||
|
"Chime Type": {
|
||||||
|
value: settings.type,
|
||||||
|
min: 0,
|
||||||
|
max: 2, // both is just silly
|
||||||
|
format: (v) => chimes[v],
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.type = v;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Frequency: {
|
||||||
|
value: settings.freq,
|
||||||
|
min: 0,
|
||||||
|
max: 2,
|
||||||
|
format: (v) => frequency[v],
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.freq = v;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Repetition: {
|
||||||
|
value: settings.repeat,
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
format: (v) => v,
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.repeat = v;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Sleep Mode": {
|
||||||
|
value: !!settings.sleep,
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.sleep = v;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Sleep Start": {
|
||||||
|
value: settings.start,
|
||||||
|
min: 0,
|
||||||
|
max: 23,
|
||||||
|
format: (v) => v,
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.start = v;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Sleep End": {
|
||||||
|
value: settings.end,
|
||||||
|
min: 0,
|
||||||
|
max: 23,
|
||||||
|
format: (v) => v,
|
||||||
|
onchange: (v) => {
|
||||||
|
settings.end = v;
|
||||||
|
writeSettings(settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var readSettings = () => {
|
||||||
|
var settings = require("Storage").readJSON(FILE, 1) || {
|
||||||
|
type: 1,
|
||||||
|
freq: 0,
|
||||||
|
repeat: 1,
|
||||||
|
sleep: true,
|
||||||
|
start: 6,
|
||||||
|
end: 22,
|
||||||
|
};
|
||||||
|
return settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
var writeSettings = (settings) => {
|
||||||
|
require("Storage").writeJSON(FILE, settings);
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = readSettings();
|
||||||
|
showMainMenu();
|
||||||
|
});
|
|
@ -0,0 +1,134 @@
|
||||||
|
(function () {
|
||||||
|
// 0: off, 1: buzz, 2: beep, 3: both
|
||||||
|
var FILE = "chimer.json";
|
||||||
|
|
||||||
|
var readSettings = () => {
|
||||||
|
var settings = require("Storage").readJSON(FILE, 1) || {
|
||||||
|
type: 1,
|
||||||
|
freq: 0,
|
||||||
|
repeat: 1,
|
||||||
|
sleep: true,
|
||||||
|
start: 6,
|
||||||
|
end: 22,
|
||||||
|
};
|
||||||
|
return settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
var settings = readSettings();
|
||||||
|
|
||||||
|
function sleep(milliseconds) {
|
||||||
|
const date = Date.now();
|
||||||
|
let currentDate = null;
|
||||||
|
do {
|
||||||
|
currentDate = Date.now();
|
||||||
|
} while (currentDate - date < milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chime() {
|
||||||
|
for (var i = 0; i < settings.repeat; i++) {
|
||||||
|
if (settings.type === 1) {
|
||||||
|
Bangle.buzz(100);
|
||||||
|
} else if (settings.type === 2) {
|
||||||
|
Bangle.beep();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sleep(150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastHour = new Date().getHours();
|
||||||
|
let lastMinute = new Date().getMinutes(); // don't chime when (re)loaded at a whole hour
|
||||||
|
function check() {
|
||||||
|
const now = new Date(),
|
||||||
|
h = now.getHours(),
|
||||||
|
m = now.getMinutes(),
|
||||||
|
s = now.getSeconds(),
|
||||||
|
ms = now.getMilliseconds();
|
||||||
|
if (
|
||||||
|
(settings.sleep && h > settings.end) ||
|
||||||
|
(settings.sleep && h >= settings.end && m !== 0) ||
|
||||||
|
(settings.sleep && h < settings.start)
|
||||||
|
) {
|
||||||
|
var mLeft = 60 - m,
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
setTimeout(check, msLeft);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (settings.freq === 1) {
|
||||||
|
if ((m !== lastMinute && m === 0) || (m !== lastMinute && m === 30))
|
||||||
|
chime();
|
||||||
|
lastHour = h;
|
||||||
|
lastMinute = m;
|
||||||
|
// check again in 30 minutes
|
||||||
|
switch (true) {
|
||||||
|
case m / 30 >= 1:
|
||||||
|
var mLeft = 30 - (m - 30),
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
break;
|
||||||
|
case m / 30 < 1:
|
||||||
|
var mLeft = 30 - m,
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setTimeout(check, msLeft);
|
||||||
|
} else if (settings.freq === 2) {
|
||||||
|
if (
|
||||||
|
(m !== lastMinute && m === 0) ||
|
||||||
|
(m !== lastMinute && m === 15) ||
|
||||||
|
(m !== lastMinute && m === 30) ||
|
||||||
|
(m !== lastMinute && m === 45)
|
||||||
|
)
|
||||||
|
chime();
|
||||||
|
lastHour = h;
|
||||||
|
lastMinute = m;
|
||||||
|
// check again in 15 minutes
|
||||||
|
switch (true) {
|
||||||
|
case m / 15 >= 3:
|
||||||
|
var mLeft = 15 - (m - 45),
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
break;
|
||||||
|
case m / 15 >= 2:
|
||||||
|
var mLeft = 15 - (m - 30),
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
break;
|
||||||
|
case m / 15 >= 1:
|
||||||
|
var mLeft = 15 - (m - 15),
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
break;
|
||||||
|
case m / 15 < 1:
|
||||||
|
var mLeft = 15 - m,
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setTimeout(check, msLeft);
|
||||||
|
} else if (settings.freq === 3) {
|
||||||
|
if (m !== lastMinute) chime();
|
||||||
|
lastHour = h;
|
||||||
|
lastMinute = m;
|
||||||
|
// check again in 1 minute
|
||||||
|
|
||||||
|
var mLeft = 1,
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
setTimeout(check, msLeft);
|
||||||
|
} else {
|
||||||
|
if (h !== lastHour && m === 0) chime();
|
||||||
|
lastHour = h;
|
||||||
|
// check again in 60 minutes
|
||||||
|
var mLeft = 60 - m,
|
||||||
|
sLeft = mLeft * 60 - s,
|
||||||
|
msLeft = sLeft * 1000 - ms;
|
||||||
|
setTimeout(check, msLeft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check();
|
||||||
|
})();
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -83,6 +83,7 @@ function onInit(device) {
|
||||||
if (crc==3508163280 || crc==1418074094) version = "2v12";
|
if (crc==3508163280 || crc==1418074094) version = "2v12";
|
||||||
if (crc==4056371285) version = "2v13";
|
if (crc==4056371285) version = "2v13";
|
||||||
if (crc==1038322422) version = "2v14";
|
if (crc==1038322422) version = "2v14";
|
||||||
|
if (crc==2560806221) version = "2v15";
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
version += `(⚠ update required)`;
|
version += `(⚠ update required)`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,3 +4,4 @@
|
||||||
0.04: Fix great circle formula
|
0.04: Fix great circle formula
|
||||||
0.05: Use locale for speed and distance + fix Vector font sizes
|
0.05: Use locale for speed and distance + fix Vector font sizes
|
||||||
0.06: Move waypoints.json (and editor) to 'waypoints' app
|
0.06: Move waypoints.json (and editor) to 'waypoints' app
|
||||||
|
0.07: Add support for b2
|
|
@ -4,9 +4,12 @@ The app is aimed at small boat navigation although it can also be used to mark t
|
||||||
|
|
||||||
The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix.
|
The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix.
|
||||||
|
|
||||||
|
[Bangle.js 2] Button mappings in brackests. One additional feature:
|
||||||
|
On swiping on the main screen you can change the displayed metrics: Right changes to nautical metrics, left to the default locale metrics.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn white. Then use BTN1 and BTN3 to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected.
|
The large digits are the course and speed. The top of the display is a linear compass which displays the direction of travel when a fix is received and you are moving. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) [touch / BTN] and wait for the blue text to turn white. Then use BTN1 and BTN3 [swipe left/right] to select a waypoint. The waypoint choice is fixed by pressing BTN2 [touch / BTN] again. In the screen shot below a waypoint giving the location of Stone Henge has been selected.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -18,7 +21,7 @@ The app lets you mark your current location as follows. There are vacant slots i
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2.
|
Bearing and distance are both zero as WP1 has currently no GPS location associated with it. To mark the location, press BTN2 [touch / BTN].
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
var candraw = true;
|
||||||
|
var brg = 0;
|
||||||
|
var wpindex = 0;
|
||||||
|
var locindex = 0;
|
||||||
|
const labels = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
||||||
|
var loc = {
|
||||||
|
speed: [
|
||||||
|
require("locale").speed,
|
||||||
|
(kph) => {
|
||||||
|
return (kph / 1.852).toFixed(1) + "kn ";
|
||||||
|
}
|
||||||
|
],
|
||||||
|
distance: [
|
||||||
|
require("locale").distance,
|
||||||
|
(m) => {
|
||||||
|
return (m / 1.852).toFixed(3) + "nm ";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function drawCompass(course) {
|
||||||
|
if (!candraw) return;
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.setFont("Vector", 18);
|
||||||
|
var start = course - 90;
|
||||||
|
if (start < 0) start += 360;
|
||||||
|
g.fillRect(14, 67, 162, 71);
|
||||||
|
var xpos = 16;
|
||||||
|
var frag = 15 - start % 15;
|
||||||
|
if (frag < 15) xpos += Math.floor((frag * 4) / 5);
|
||||||
|
else frag = 0;
|
||||||
|
for (var i = frag; i <= 180 - frag; i += 15) {
|
||||||
|
var res = start + i;
|
||||||
|
if (res % 90 == 0) {
|
||||||
|
g.drawString(labels[Math.floor(res / 45) % 8], xpos - 6, 28);
|
||||||
|
g.fillRect(xpos - 2, 47, xpos + 2, 67);
|
||||||
|
} else if (res % 45 == 0) {
|
||||||
|
g.drawString(labels[Math.floor(res / 45) % 8], xpos - 9, 28);
|
||||||
|
g.fillRect(xpos - 2, 52, xpos + 2, 67);
|
||||||
|
} else if (res % 15 == 0) {
|
||||||
|
g.fillRect(xpos, 58, xpos + 1, 67);
|
||||||
|
}
|
||||||
|
xpos += 12;
|
||||||
|
}
|
||||||
|
if (wpindex != 0) {
|
||||||
|
var bpos = brg - course;
|
||||||
|
if (bpos > 180) bpos -= 360;
|
||||||
|
if (bpos < -180) bpos += 360;
|
||||||
|
bpos = Math.round((bpos * 4) / 5) + 88;
|
||||||
|
if (bpos < 16) bpos = 7;
|
||||||
|
if (bpos > 160) bpos = 169;
|
||||||
|
g.setColor(g.theme.bgH);
|
||||||
|
g.fillCircle(bpos, 63, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//displayed heading
|
||||||
|
var heading = 0;
|
||||||
|
|
||||||
|
function newHeading(m, h) {
|
||||||
|
var s = Math.abs(m - h);
|
||||||
|
var delta = (m > h) ? 1 : -1;
|
||||||
|
if (s >= 180) {
|
||||||
|
s = 360 - s;
|
||||||
|
delta = -delta;
|
||||||
|
}
|
||||||
|
if (s < 2) return h;
|
||||||
|
var hd = h + delta * (1 + Math.round(s / 5));
|
||||||
|
if (hd < 0) hd += 360;
|
||||||
|
if (hd > 360) hd -= 360;
|
||||||
|
return hd;
|
||||||
|
}
|
||||||
|
|
||||||
|
var course = 0;
|
||||||
|
var speed = 0;
|
||||||
|
var satellites = 0;
|
||||||
|
var wp;
|
||||||
|
var dist = 0;
|
||||||
|
|
||||||
|
function radians(a) {
|
||||||
|
return a * Math.PI / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function degrees(a) {
|
||||||
|
var d = a * 180 / Math.PI;
|
||||||
|
return (d + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearing(a, b) {
|
||||||
|
var delta = radians(b.lon - a.lon);
|
||||||
|
var alat = radians(a.lat);
|
||||||
|
var blat = radians(b.lat);
|
||||||
|
var y = Math.sin(delta) * Math.cos(blat);
|
||||||
|
var x = Math.cos(alat) * Math.sin(blat) -
|
||||||
|
Math.sin(alat) * Math.cos(blat) * Math.cos(delta);
|
||||||
|
return Math.round(degrees(Math.atan2(y, x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(a, b) {
|
||||||
|
var x = radians(a.lon - b.lon) * Math.cos(radians((a.lat + b.lat) / 2));
|
||||||
|
var y = radians(b.lat - a.lat);
|
||||||
|
return Math.round(Math.sqrt(x * x + y * y) * 6371000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected = false;
|
||||||
|
|
||||||
|
function drawN() {
|
||||||
|
g.clearRect(0, 89, 175, 175);
|
||||||
|
var txt = loc.speed[locindex](speed);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.setFont("6x8", 2);
|
||||||
|
g.drawString("o", 68, 87);
|
||||||
|
g.setFont("6x8", 1);
|
||||||
|
g.drawString(txt.substring(txt.length - 3), 156, 119);
|
||||||
|
g.setFont("Vector", 36);
|
||||||
|
var cs = course.toString().padStart(3, "0");
|
||||||
|
g.drawString(cs, 2, 89);
|
||||||
|
g.drawString(txt.substring(0, txt.length - 3), 92, 89);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.setFont("Vector", 18);
|
||||||
|
var bs = brg.toString().padStart(3, "0");
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.drawString("Brg:", 1, 128);
|
||||||
|
g.drawString("Dist:", 1, 148);
|
||||||
|
g.setColor(selected ? g.theme.bgH : g.theme.bg);
|
||||||
|
g.fillRect(90, 127, 175, 143);
|
||||||
|
g.setColor(selected ? g.theme.fgH : g.theme.fg);
|
||||||
|
g.drawString(wp.name, 92, 128);
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.drawString(bs, 42, 128);
|
||||||
|
g.drawString(loc.distance[locindex](dist), 42, 148);
|
||||||
|
g.setFont("6x8", 0.5);
|
||||||
|
g.drawString("o", 75, 127);
|
||||||
|
g.setFont("6x8", 1);
|
||||||
|
g.setColor(satellites ? g.theme.bg : g.theme.bgH);
|
||||||
|
g.fillRect(0, 167, 75, 175);
|
||||||
|
g.setColor(satellites ? g.theme.fg : g.theme.fgH);
|
||||||
|
g.drawString("Sats:", 1, 168);
|
||||||
|
g.drawString(satellites.toString(), 42, 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedfix;
|
||||||
|
|
||||||
|
function onGPS(fix) {
|
||||||
|
savedfix = fix;
|
||||||
|
if (fix !== undefined) {
|
||||||
|
course = isNaN(fix.course) ? course : Math.round(fix.course);
|
||||||
|
speed = isNaN(fix.speed) ? speed : fix.speed;
|
||||||
|
satellites = fix.satellites;
|
||||||
|
}
|
||||||
|
if (candraw) {
|
||||||
|
if (fix !== undefined && fix.fix == 1) {
|
||||||
|
dist = distance(fix, wp);
|
||||||
|
if (isNaN(dist)) dist = 0;
|
||||||
|
brg = bearing(fix, wp);
|
||||||
|
if (isNaN(brg)) brg = 0;
|
||||||
|
}
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var intervalRef;
|
||||||
|
|
||||||
|
function stopdraw() {
|
||||||
|
candraw = false;
|
||||||
|
if (intervalRef) {
|
||||||
|
clearInterval(intervalRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimers() {
|
||||||
|
candraw = true;
|
||||||
|
intervalRefSec = setInterval(function() {
|
||||||
|
heading = newHeading(course, heading);
|
||||||
|
if (course != heading) drawCompass(heading);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawAll() {
|
||||||
|
g.setColor(1, 0, 0);
|
||||||
|
g.fillPoly([88, 71, 78, 88, 98, 88]);
|
||||||
|
drawN();
|
||||||
|
drawCompass(heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startdraw() {
|
||||||
|
g.clear();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
startTimers();
|
||||||
|
drawAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setButtons() {
|
||||||
|
Bangle.setUI("leftright", btn => {
|
||||||
|
if (!btn) {
|
||||||
|
doselect();
|
||||||
|
} else {
|
||||||
|
nextwp(btn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var SCREENACCESS = {
|
||||||
|
withApp: true,
|
||||||
|
request: function() {
|
||||||
|
this.withApp = false;
|
||||||
|
stopdraw();
|
||||||
|
clearWatch();
|
||||||
|
},
|
||||||
|
release: function() {
|
||||||
|
this.withApp = true;
|
||||||
|
startdraw();
|
||||||
|
setButtons();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on('lcdPower', function(on) {
|
||||||
|
if (!SCREENACCESS.withApp) return;
|
||||||
|
if (on) {
|
||||||
|
startdraw();
|
||||||
|
} else {
|
||||||
|
stopdraw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var waypoints = require("waypoints").load();
|
||||||
|
wp = waypoints[0];
|
||||||
|
|
||||||
|
function nextwp(inc) {
|
||||||
|
if (selected) {
|
||||||
|
wpindex += inc;
|
||||||
|
if (wpindex >= waypoints.length) wpindex = 0;
|
||||||
|
if (wpindex < 0) wpindex = waypoints.length - 1;
|
||||||
|
wp = waypoints[wpindex];
|
||||||
|
drawN();
|
||||||
|
} else {
|
||||||
|
locindex = inc > 0 ? 1 : 0;
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doselect() {
|
||||||
|
if (selected && wpindex != 0 && waypoints[wpindex].lat === undefined && savedfix.fix) {
|
||||||
|
waypoints[wpindex] = {
|
||||||
|
name: "@" + wp.name,
|
||||||
|
lat: savedfix.lat,
|
||||||
|
lon: savedfix.lon
|
||||||
|
};
|
||||||
|
wp = waypoints[wpindex];
|
||||||
|
require("waypoints").save(waypoints);
|
||||||
|
}
|
||||||
|
selected = !selected;
|
||||||
|
print("selected = " + selected);
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
Bangle.setLCDBrightness(1);
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
// load widgets can turn off GPS
|
||||||
|
Bangle.setGPSPower(1);
|
||||||
|
drawAll();
|
||||||
|
startTimers();
|
||||||
|
Bangle.on('GPS', onGPS);
|
||||||
|
// Toggle selected
|
||||||
|
setButtons();
|
|
@ -1,15 +1,17 @@
|
||||||
{
|
{
|
||||||
"id": "gpsnav",
|
"id": "gpsnav",
|
||||||
"name": "GPS Navigation",
|
"name": "GPS Navigation",
|
||||||
"version": "0.06",
|
"version": "0.07",
|
||||||
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor",
|
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor",
|
||||||
|
"screenshots": [{"url":"screenshot-b2.png"}],
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"tags": "tool,outdoors,gps",
|
"tags": "tool,outdoors,gps",
|
||||||
"supports": ["BANGLEJS"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"dependencies" : { "waypoints":"type" },
|
"dependencies" : { "waypoints":"type" },
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name":"gpsnav.app.js","url":"app.min.js"},
|
{"name":"gpsnav.app.js","url":"app.min.js","supports":["BANGLEJS"]},
|
||||||
|
{"name":"gpsnav.app.js","url":"app_b2.js","supports":["BANGLEJS2"]},
|
||||||
{"name":"gpsnav.img","url":"app-icon.js","evaluate":true}
|
{"name":"gpsnav.img","url":"app-icon.js","evaluate":true}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -21,3 +21,4 @@
|
||||||
0.21: Add custom theming.
|
0.21: Add custom theming.
|
||||||
0.22: Fix alarm and add build in function for step counting.
|
0.22: Fix alarm and add build in function for step counting.
|
||||||
0.23: Add warning for low flash memory
|
0.23: Add warning for low flash memory
|
||||||
|
0.24: Add ability to disable alarm functionality
|
|
@ -12,6 +12,7 @@ let settings = {
|
||||||
themeColor1BG: "#FF9900",
|
themeColor1BG: "#FF9900",
|
||||||
themeColor2BG: "#FF00DC",
|
themeColor2BG: "#FF00DC",
|
||||||
themeColor3BG: "#0094FF",
|
themeColor3BG: "#0094FF",
|
||||||
|
disableAlarms: false,
|
||||||
};
|
};
|
||||||
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
||||||
for (const key in saved_settings) {
|
for (const key in saved_settings) {
|
||||||
|
@ -722,12 +723,12 @@ Bangle.on('touch', function(btn, e){
|
||||||
}
|
}
|
||||||
|
|
||||||
if(lcarsViewPos == 0){
|
if(lcarsViewPos == 0){
|
||||||
if(is_upper){
|
if(is_upper && !settings.disableAlarms){
|
||||||
feedback();
|
feedback();
|
||||||
increaseAlarm();
|
increaseAlarm();
|
||||||
drawState();
|
drawState();
|
||||||
return;
|
return;
|
||||||
} if(is_lower){
|
} if(is_lower && !settings.disableAlarms){
|
||||||
feedback();
|
feedback();
|
||||||
decreaseAlarm();
|
decreaseAlarm();
|
||||||
drawState();
|
drawState();
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
themeColor1BG: "#FF9900",
|
themeColor1BG: "#FF9900",
|
||||||
themeColor2BG: "#FF00DC",
|
themeColor2BG: "#FF00DC",
|
||||||
themeColor3BG: "#0094FF",
|
themeColor3BG: "#0094FF",
|
||||||
|
disableAlarms: false,
|
||||||
};
|
};
|
||||||
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
|
||||||
for (const key in saved_settings) {
|
for (const key in saved_settings) {
|
||||||
|
@ -102,6 +103,14 @@
|
||||||
settings.themeColor3BG = bg_code[v];
|
settings.themeColor3BG = bg_code[v];
|
||||||
save();
|
save();
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
'Disable alarm functionality': {
|
||||||
|
value: settings.disableAlarms,
|
||||||
|
format: () => (settings.disableAlarms ? 'Yes' : 'No'),
|
||||||
|
onchange: () => {
|
||||||
|
settings.disableAlarms = !settings.disableAlarms;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "LCARS Clock",
|
"name": "LCARS Clock",
|
||||||
"shortName":"LCARS",
|
"shortName":"LCARS",
|
||||||
"icon": "lcars.png",
|
"icon": "lcars.png",
|
||||||
"version":"0.23",
|
"version":"0.24",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
0.01: Created first version of the app with numeric date, only works in light mode
|
0.01: Created first version of the app with numeric date, only works in light mode
|
||||||
0.02: New icon, shimmied date right a bit
|
0.02: New icon, shimmied date right a bit
|
||||||
0.03: Incorporated improvements from Peer David for accuracy, fix dark mode, widgets run in background
|
0.03: Incorporated improvements from Peer David for accuracy, fix dark mode, widgets run in background
|
||||||
|
0.04: Changed clock to use 12/24 hour format based on locale
|
||||||
|
|
|
@ -30,15 +30,15 @@ function draw() {
|
||||||
g.setFontAlign(0, -1, 0);
|
g.setFontAlign(0, -1, 0);
|
||||||
g.setColor(0,0,0);
|
g.setColor(0,0,0);
|
||||||
var d = new Date();
|
var d = new Date();
|
||||||
var da = d.toString().split(" ");
|
var dt = require("locale").time(d, 1);
|
||||||
hh = da[4].substr(0,2);
|
var hh = dt.split(":")[0];
|
||||||
mi = da[4].substr(3,2);
|
var mm = dt.split(":")[1];
|
||||||
|
g.drawString(hh, 52, 65, true);
|
||||||
|
g.drawString(mm, 132, 65, true);
|
||||||
|
g.drawString(':', 93,65);
|
||||||
dd = ("0"+(new Date()).getDate()).substr(-2);
|
dd = ("0"+(new Date()).getDate()).substr(-2);
|
||||||
mo = ("0"+((new Date()).getMonth()+1)).substr(-2);
|
mo = ("0"+((new Date()).getMonth()+1)).substr(-2);
|
||||||
yy = ("0"+((new Date()).getFullYear())).substr(-2);
|
yy = ("0"+((new Date()).getFullYear())).substr(-2);
|
||||||
g.drawString(hh, 52, 65, true);
|
|
||||||
g.drawString(mi, 132, 65, true);
|
|
||||||
g.drawString(':', 93,65);
|
|
||||||
g.setFontCustom(font, 48, 8, 521);
|
g.setFontCustom(font, 48, 8, 521);
|
||||||
g.drawString(dd + ':' + mo + ':' + yy, 88, 120, true);
|
g.drawString(dd + ':' + mo + ':' + yy, 88, 120, true);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "MacWatch2",
|
"name": "MacWatch2",
|
||||||
"shortName":"MacWatch2",
|
"shortName":"MacWatch2",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"version":"0.03",
|
"version":"0.04",
|
||||||
"description": "Classic Mac Finder clock",
|
"description": "Classic Mac Finder clock",
|
||||||
"type": "clock",
|
"type": "clock",
|
||||||
"tags": "clock",
|
"tags": "clock",
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/
|
0.12: Improve pattern detection code readability by PaddeK http://forum.espruino.com/profiles/117930/
|
||||||
0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/
|
0.13: Improve pattern rendering by HughB http://forum.espruino.com/profiles/167235/
|
||||||
0.14: Update setUI to work with new Bangle.js 2v13 menu style
|
0.14: Update setUI to work with new Bangle.js 2v13 menu style
|
||||||
|
0.15: Update to support clocks in custom setUI mode
|
||||||
|
|
|
@ -76,13 +76,8 @@
|
||||||
var sui = Bangle.setUI;
|
var sui = Bangle.setUI;
|
||||||
Bangle.setUI = function (mode, cb) {
|
Bangle.setUI = function (mode, cb) {
|
||||||
sui(mode, cb);
|
sui(mode, cb);
|
||||||
if ("object"==typeof mode) mode = mode.mode;
|
if (typeof mode === "object") mode = (mode.clock ? "clock" : "") + mode.mode;
|
||||||
if (!mode) {
|
if (!mode || !mode.startsWith("clock")) {
|
||||||
Bangle.removeListener("drag", dragHandler);
|
|
||||||
storedPatterns = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!mode.startsWith("clock")) {
|
|
||||||
storedPatterns = {};
|
storedPatterns = {};
|
||||||
Bangle.removeListener("drag", dragHandler);
|
Bangle.removeListener("drag", dragHandler);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "ptlaunch",
|
"id": "ptlaunch",
|
||||||
"name": "Pattern Launcher",
|
"name": "Pattern Launcher",
|
||||||
"shortName": "Pattern Launcher",
|
"shortName": "Pattern Launcher",
|
||||||
"version": "0.14",
|
"version": "0.15",
|
||||||
"description": "Directly launch apps from the clock screen with custom patterns.",
|
"description": "Directly launch apps from the clock screen with custom patterns.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"manage_patterns_light.png"}],
|
"screenshots": [{"url":"manage_patterns_light.png"}],
|
||||||
|
|
|
@ -4,3 +4,6 @@
|
||||||
0.04: Fix #1445, display loading info, add icons to display service states
|
0.04: Fix #1445, display loading info, add icons to display service states
|
||||||
0.05: Fix LOW_MEMORY,MEMORY error on to big log size
|
0.05: Fix LOW_MEMORY,MEMORY error on to big log size
|
||||||
0.06: Reduced log size further to 750 entries
|
0.06: Reduced log size further to 750 entries
|
||||||
|
0.10: Complete rework off this app!
|
||||||
|
0.10beta: Add interface.html to view saved log data, add "View log" function for debugging log, send data for gadgetbridge, change caching for global getStats
|
||||||
|
0.11: Prevent module not found error
|
||||||
|
|
|
@ -1,176 +1,175 @@
|
||||||
# Sleep Log
|
# Sleep Log
|
||||||
|
|
||||||
This app logs and displays the four following states:
|
This app logs and displays the following states:
|
||||||
_unknown, not worn, awake, sleeping_
|
- sleepling status: _unknown, not worn, awake, light sleep, deep sleep_
|
||||||
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
|
- consecutive sleep status: _unknown, not consecutive, consecutive_
|
||||||
also provides a power saving mode using the built in movement calculation. The internal temperature is used to decide if the status is _sleeping_ or _not worn_.
|
|
||||||
|
It is using the built in movement calculation to decide your sleeping state. While charging it is assumed that you are not wearing the watch and if the status changes to _deep sleep_ the internal heartrate sensor is used to detect if you are wearing the watch.
|
||||||
|
|
||||||
|
Logfiles are not removed on un-/reinstall to prevent data loss.
|
||||||
|
|
||||||
|
| Filename (* _example_) | Content | Removeable in |
|
||||||
|
|------------------------------|-----------------|-------------------|
|
||||||
|
| `sleeplog.log (StorageFile)` | recent logfile | App Web Interface |
|
||||||
|
| `sleeplog_1234.log`* | old logfiles | App Web Interface |
|
||||||
|
| `sleeplog_123456.csv`* | debugging files | Web IDE |
|
||||||
|
|
||||||
#### 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 (__NoMo Thresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. The first time no movement is detected the actual timestamp is cached (in _sleeplog.firstnomodate_) for logging.
|
|
||||||
When the "no movement" counter reaches the sleep threshold the watch is considered as resting. (The sleep threshold is calculated from the __Min Duration__ setting, Example: _sleep threshold = Min Duration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_)
|
|
||||||
* __Power Saving Mode__
|
|
||||||
On power saving mode the movement value of bangle's build in health event is checked against the maximal movement threshold (__Max Move__). The event is only triggered every 10 minutes which decreases the battery impact but also reduces accurracy.
|
|
||||||
* ___Sleeping___ __or__ ___Not Worn___
|
|
||||||
To check if a resting watch indicates a sleeping status, the internal temperature must be greater than the temperature threshold (__Temp Thresh__). 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 (__Max Awake__). If this sum is lower than the minimal consecutive sleep duration (__Min Consec__) 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. The logged timestamp is matching the beginning of its measurement period.
|
|
||||||
When not on power saving mode a movement is detected nearly instantaneous and the detection of a no movement period is delayed by the minimal no movement duration. To match the beginning of the measurement period a cached timestamp (_sleeplog.firstnomodate_) is logged.
|
|
||||||
On power saving mode the measurement period is fixed to 10 minutes and all logged timestamps are also set back 10 minutes.
|
|
||||||
To prevent a LOW_MEMORY,MEMORY error the log size is limited to 750 entries, older entries will be overwritten.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
### Control
|
### App Usage
|
||||||
---
|
---
|
||||||
* __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.
|
|
||||||
|
|
||||||
---
|
#### On the main app screen:
|
||||||
### Settings
|
- __swipe left & right__
|
||||||
---
|
to change the displayed day
|
||||||
* __Break Tod__ | break at time of day
|
- __touch the "title"__ (e.g. `Night to Fri 20/05/2022`)
|
||||||
_0_ / _1_ / _..._ / __10__ / _..._ / _12_
|
to enter day selection prompt
|
||||||
Change time of day on wich the lower graph starts and the upper graph ends.
|
- __touch the info area__
|
||||||
* __Max Awake__ | maximal awake duration
|
to change the displayed information
|
||||||
_15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_
|
(by default: consecutive & true sleeping)
|
||||||
Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period.
|
- __touch the wrench__ (upper right corner)
|
||||||
* __Min Consec__ | minimal consecutive sleep duration
|
to enter the settings
|
||||||
_15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_
|
- __use back button widget__ (upper left corner)
|
||||||
Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value.
|
exit the app
|
||||||
* __Temp Thresh__ | temperature threshold
|
|
||||||
_20°C_ / _20.5°C_ / _..._ / __25°C__ / _..._ / _40°C_
|
#### Inside the settings:
|
||||||
The internal temperature must be greater than this threshold to log _sleeping_, otherwise it is _not worn_.
|
- __Thresholds__ submenu
|
||||||
* __Power Saving__
|
Changes take effect from now on, not retrospective!
|
||||||
_on_ / __off__
|
- __Max Awake__ | maximal awake duration
|
||||||
En-/Disable power saving mode. _Saves battery, but might decrease accurracy._
|
_10min_ / _20min_ / ... / __60min__ / ... / _120min_
|
||||||
In app icon showing that power saving mode is enabled: 
|
- __Min Consecutive__ | minimal consecutive sleep duration
|
||||||
* __Max Move__ | maximal movement threshold
|
_10min_ / _20min_ / ... / __30min__ / ... / _120min_
|
||||||
(only available when on power saving mode)
|
- __Deep Sleep__ | deep sleep threshold
|
||||||
_50_ / _51_ / _..._ / __100__ / _..._ / _200_
|
_30_ / _31_ / ... / __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.
|
- __Light Sleep__ | light sleep threshold
|
||||||
* __NoMo Thresh__ | no movement threshold
|
_100_ / _110_ / ... / __200__ / ... / _400_
|
||||||
(only available when not on power saving mode)
|
- __Reset to Default__ | reset to bold values above
|
||||||
_0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_
|
- __BreakToD__ | time of day to break view
|
||||||
The standard deviation over the measured values needs to be lower then this threshold to count as not moving.
|
_0:00_ / _1:00_ / ... / __12:00__ / ... / _23:00_
|
||||||
The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise.
|
- __App Timeout__ | app specific lock timeout
|
||||||
* __Min Duration__ | minimal no movement duration
|
__0s__ / _10s_ / ... / _120s_
|
||||||
(only available when not on power saving mode)
|
- __Enabled__ | completely en-/disables the background service
|
||||||
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
|
|
||||||
If no movement is detected for this duration, the watch is considered as resting.
|
|
||||||
* __Enabled__
|
|
||||||
__on__ / _off_
|
__on__ / _off_
|
||||||
En-/Disable the service (all background activities). _Saves the most battery, but might make this app useless._
|
- __Debugging__ submenu
|
||||||
In app icon showing that the service is disabled: 
|
- __View log__ | display logfile data
|
||||||
* __Logfile__
|
Select the logfile by its starting time.
|
||||||
__default__ / _off_
|
Thresholds are shown as line with its value.
|
||||||
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
|
- __swipe left & right__
|
||||||
If the logfile has been customized it is displayed with _custom_.
|
to change displayed duration
|
||||||
In app icon showing that logging is disabled: 
|
- __swipe up & down__
|
||||||
|
to change displayed value range
|
||||||
|
- __touch the graph__
|
||||||
|
to change between light & dark colors
|
||||||
|
- __use back button widget__ (upper left corner)
|
||||||
|
to go back to the logfile selection
|
||||||
|
- __Enabled__ | en-/disables debugging
|
||||||
|
_on_ / __off__
|
||||||
|
- __write File__ | toggles if a logfile is written
|
||||||
|
_on_ / __off__
|
||||||
|
- __Duration__ | duration for writing into logfile
|
||||||
|
_1h_ / _2h_ / ... / __12h__ / _96_
|
||||||
|
- The following data is logged to a csv-file:
|
||||||
|
_timestamp_ (in days since 1900-01-01 00:00 UTC used by office software) _, movement, status, consecutive, asleepSince, awakeSince, bpm, bpmConfidence_
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
### Global Object and Module Functions
|
### Web Interface Usage
|
||||||
---
|
---
|
||||||
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:
|
|
||||||
/ undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
|
||||||
firstnomodate: 1644435877595, // number / Date.now() from first recognised no movement, not available in power saving mode
|
|
||||||
stop: function () { ... }, // funct / stops the service until the next load()
|
|
||||||
start: function () { ... }, // funct / restarts the service
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
>require("sleeplog")
|
Available through the App Loader when your watch is connected.
|
||||||
={
|
|
||||||
setEnabled: function (enable, logfile, powersaving) { ... },
|
- __view data__
|
||||||
// restarts the service with changed settings
|
Display the data to each timestamp in a table.
|
||||||
// * enable / bool / new service status
|
- __save csv-file__
|
||||||
// * logfile / bool or string
|
Download a csv-file with the data to each timestamp.
|
||||||
// - true = enables logging to "sleeplog.log"
|
The time format is chooseable beneath the file list.
|
||||||
// - "some_file.log" = enables logging to "some_file.log"
|
- __delete file__
|
||||||
// - false = disables logging
|
Deletes the logfile from the watch. __Please backup your data first!__
|
||||||
// * (powersaving) / bool / new power saving status, default: false
|
|
||||||
// returns: true or undefined
|
---
|
||||||
// - true = service restart executed
|
### Timestamps and files
|
||||||
// - undefined = no global.sleeplog found
|
---
|
||||||
readLog: function (logfile, since, until) { ... },
|
|
||||||
// read the raw log data for a specific time period
|
1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps:
|
||||||
// * logfile / string / on no string uses logfile from global object or "sleeplog.log"
|
seconds since 1970-01-01 00:00 UTC
|
||||||
// * (since) / Date or number / startpoint of period, default: 0
|
2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution:
|
||||||
// * (until) / Date or number / endpoint of period, default: 1E14
|
10 minutes since 1970-01-01 00:00 UTC (`Bangle / (10 * 60 * 1000)`)
|
||||||
// returns: array
|
3. debug .csv file ID (`sleeplog_123456.csv`) has a hourly resolution:
|
||||||
// * [[number, int, string], [...], ... ] / sorting: latest first
|
hours since 1970-01-01 00:00 UTC (`Bangle / (60 * 60 * 1000)`)
|
||||||
// - number // timestamp in ms
|
4. logged timestamps inside the debug .csv file are formatted for office calculation software:
|
||||||
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
days since 1900-01-01 00:00 UTC (`Bangle / (24 * 60 * 60 * 1000) + 25569`)
|
||||||
// - string // additional information
|
5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app:
|
||||||
// * [] = no data available or global.sleeplog not found
|
fortnights since 1970-01-04 12:00 UTC (converted with `require("sleeplog").msToFn(Bangle)` and `require("sleeplog").fnToMs(fortnight)`)
|
||||||
writeLog: function (logfile, input) { ... },
|
|
||||||
// append or replace log depending on input
|
- __Logfiles from before 0.10:__
|
||||||
// * logfile / string / on no string uses logfile from global object or default
|
timestamps and sleeping status of old logfiles are automatically converted on your first consecutive sleep or manually by `require("sleeplog").convertOldLog()`
|
||||||
// * input / array
|
|
||||||
// - append input if array length >1 and element[0] >9E11
|
- __View logged data:__
|
||||||
// - replace log with input if at least one entry like above is inside another array
|
if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data
|
||||||
// returns: true or undefined
|
since & until in Bangle timestamp, e.g. `require("sleeplog").printLog(Date()-24*60*60*1000, Date())` for the last 24h
|
||||||
// - true = changest written to storage
|
|
||||||
// - undefined = wrong input
|
|
||||||
getReadableLog: function (printLog, since, until, logfile) { ... }
|
---
|
||||||
// read the log data as humanreadable string for a specific time period
|
### Access statistics (developer information)
|
||||||
// * (printLog) / bool / direct print output with additional information, default: false
|
---
|
||||||
// * (since) / Date or number / see readLog(..)
|
- Last Asleep Time [Date]:
|
||||||
// * (until) / Date or number / see readLog(..)
|
`Date(sleeplog.awakeSince)`
|
||||||
// * (logfile) / string / see readLog(..)
|
- Last Awake Duration [ms]:
|
||||||
// returns: string
|
`Date() - sleeplog.awakeSince`
|
||||||
// * "{substring of ISO date} - {status} for {duration}min\n...", sorting: latest last
|
- Last Statistics [object]:
|
||||||
// * undefined = no data available or global.sleeplog found
|
|
||||||
restoreLog: function (logfile) { ... }
|
|
||||||
// eliminate some errors inside a specific logfile
|
|
||||||
// * (logfile) / string / see readLog(..)
|
|
||||||
// returns: int / number of changes that were made
|
|
||||||
reinterpretTemp: function (logfile, tempthresh) { ... }
|
|
||||||
// reinterpret worn status based on given temperature threshold
|
|
||||||
// * (logfile) / string / see readLog(..)
|
|
||||||
// * (tempthresh) / float / new temperature threshold, on default uses tempthresh from global object or 27
|
|
||||||
// returns: int / number of changes that were made
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
// get stats of the last night (period as displayed inside the app)
|
||||||
|
// as this might be the mostly used function the data is cached inside the global object
|
||||||
|
sleeplog.getStats();
|
||||||
|
|
||||||
|
// get stats of the last 24h
|
||||||
|
require("sleeplog").getStats(0, 24*60*60*1000);
|
||||||
|
// same as
|
||||||
|
require("sleeplog").getStats(Date.now(), 24*60*60*1000);
|
||||||
|
// output as object, timestamps as UNIX timestamp, durations in minutes
|
||||||
|
={ calculatedAt: 1653123553810, deepSleep: 250, lightSleep: 150, awakeSleep: 10,
|
||||||
|
consecSleep: 320, awakeTime: 1030, notWornTime: 0, unknownTime: 0, logDuration: 1440,
|
||||||
|
firstDate: 1653036600000, lastDate: 1653111600000 }
|
||||||
|
|
||||||
|
// to get the start of a period defined by "Break TOD" of any date
|
||||||
|
var startOfBreak = require("sleeplog").getLastBreak();
|
||||||
|
// same as
|
||||||
|
var startOfBreak = require("sleeplog").getLastBreak(Date.now());
|
||||||
|
// output as date
|
||||||
|
=Date: Sat May 21 2022 12:00:00 GMT+0200
|
||||||
|
|
||||||
|
// get stats of this period as displayed inside the app
|
||||||
|
require("sleeplog").getStats(require("sleeplog").getLastBreak(), 24*60*60*1000);
|
||||||
|
// or any other day
|
||||||
|
require("sleeplog").getStats(require("sleeplog").getLastBreak(Date(2022,4,10)), 24*60*60*1000);
|
||||||
|
```
|
||||||
|
- Total Statistics [object]:
|
||||||
|
```
|
||||||
|
// use with caution, may take a long time !
|
||||||
|
require("sleeplog").getStats(0, 0, require("sleeplog").readLog());
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
### Worth Mentioning
|
### Worth Mentioning
|
||||||
---
|
---
|
||||||
#### To do list
|
#### To do list
|
||||||
* Send the logged information to Gadgetbridge.
|
- Check translations.
|
||||||
_(For now I have no idea how to achieve this, help is appreciated.)_
|
- Add more functionallities to interface.html.
|
||||||
* View, down- and upload log functions via App Loader.
|
- Enable recieving data on the Gadgetbridge side + testing.
|
||||||
* Calculate and display overall sleep statistics.
|
__Help appreciated!__
|
||||||
* Option to automatically change power saving mode depending on time of day.
|
|
||||||
|
|
||||||
#### Requests, Bugs and Feedback
|
#### 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).
|
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
|
#### Creator
|
||||||
Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
|
Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64))
|
||||||
|
|
||||||
#### Contributors
|
#### Contributors
|
||||||
nxdefiant ([github](https://github.com/nxdefiant))
|
myxor ([github](https://github.com/myxor))
|
||||||
|
|
||||||
#### Attributions
|
#### Attributions
|
||||||
* ESS calculation based on nxdefiant interpretation of the following publication by:
|
The app icon is downloaded from [https://icons8.com](https://icons8.com).
|
||||||
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
|
#### License
|
||||||
[MIT License](LICENSE)
|
[MIT License](LICENSE)
|
||||||
|
|
|
@ -1,234 +1,354 @@
|
||||||
// set storage and define settings
|
// touch listener for specific areas
|
||||||
var storage = require("Storage");
|
function touchListener(b, c) {
|
||||||
var breaktod, maxawake, minconsec;
|
// check if inside any area
|
||||||
|
for (var i = 0; i < aaa.length; i++) {
|
||||||
// read required settings from storage
|
if (!(c.x < aaa[i].x0 || c.x > aaa[i].x1 || c.y < aaa[i].y0 || c.y > aaa[i].y1)) {
|
||||||
function readSettings(settings) {
|
// check if drawing ongoing
|
||||||
breaktod = settings.breaktod || (settings.breaktod === 0 ? 0 : 10); // time of day when to start/end graphs
|
if (drawingID > 0) {
|
||||||
maxawake = settings.maxawake || 36E5; // 60min in ms
|
// check if interrupt is set
|
||||||
minconsec = settings.minconsec || 18E5; // 30min in ms
|
if (aaa[i].interrupt) {
|
||||||
}
|
// stop ongoing drawing
|
||||||
|
drawingID++;
|
||||||
// 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(0, 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 + 1, 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 function to draw the analysis
|
|
||||||
function drawAnalysis(toDate) {
|
|
||||||
//var t0 = Date.now();
|
|
||||||
|
|
||||||
// get width
|
|
||||||
var width = g.getWidth();
|
|
||||||
|
|
||||||
// define variable for sleep calculation
|
|
||||||
var outputs = [0, 0]; // [estimated, true]
|
|
||||||
|
|
||||||
// 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(144, Date(toDate.valueOf() - 432E5)).forEach(
|
|
||||||
(value, index) => outputs[index] += value);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
//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 states: service, loggging and powersaving
|
|
||||||
if (!sleeplog.enabled) {
|
|
||||||
// draw disabled service icon
|
|
||||||
g.setColor(1, 0, 0)
|
|
||||||
.drawImage(atob("FBSBAAH4AH/gH/+D//w/n8f5/nud7znP85z/f+/3/v8/z/P895+efGPj4Hw//8H/+Af+AB+A"), 2, 36);
|
|
||||||
} else if (!sleeplog.logfile) {
|
|
||||||
// draw disabled log icon
|
|
||||||
g.reset().drawImage(atob("EA6BAM//z/8AAAAAz//P/wAAAADP/8//AAAAAM//z/8="), 4, 40)
|
|
||||||
.setColor(1, 0, 0).fillPoly([2, 38, 4, 36, 22, 54, 20, 56]);
|
|
||||||
}
|
|
||||||
// draw power saving icon
|
|
||||||
if (sleeplog.powersaving) g.setColor(0, 1, 0)
|
|
||||||
.drawImage(atob("FBSBAAAAcAD/AH/wP/4P/+H//h//4//+fv/nj/7x/88//Of/jH/4j/8I/+Af+AH+AD8AA4AA"), width - 22, 36);
|
|
||||||
|
|
||||||
// draw headline
|
|
||||||
g.reset().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);
|
if (ATID) ATID = clearTimeout(ATID);
|
||||||
ATID = setTimeout(drawAnalysis, 100, toDate);
|
} else {
|
||||||
|
// do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// give feedback
|
||||||
|
Bangle.buzz(25);
|
||||||
|
// execute action
|
||||||
|
aaa[i].funct();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// define function to draw and setup UI
|
// swipe listener for switching the displayed day
|
||||||
function startApp() {
|
function swipeListener(h, v) {
|
||||||
readSettings(storage.readJSON("sleeplog.json", true) || {});
|
// give feedback
|
||||||
drawNightTo(prevDays);
|
Bangle.buzz(25);
|
||||||
Bangle.setUI("leftright", (cb) => {
|
// set previous or next day
|
||||||
if (!cb) {
|
prevDays += h;
|
||||||
eval(storage.read("sleeplog.settings.js"))(startApp);
|
if (prevDays < -1) prevDays = -1;
|
||||||
} else if (prevDays + cb >= -1) {
|
// redraw
|
||||||
drawNightTo((prevDays += cb));
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// day selection
|
||||||
|
function daySelection() {
|
||||||
|
var date = Date(startDate - prevDays * 864E5).toString().split(" ");
|
||||||
|
E.showPrompt(date.slice(0, 3).join(" ") + "\n" + date[3] + "\n" +
|
||||||
|
prevDays + /*LANG*/" days before today", {
|
||||||
|
title: /*LANG*/"Select Day",
|
||||||
|
buttons: {
|
||||||
|
"<<7": 7,
|
||||||
|
"<1": 1,
|
||||||
|
"Ok": 0,
|
||||||
|
"1>": -1,
|
||||||
|
"7>>": -7
|
||||||
|
}
|
||||||
|
}).then(function(v) {
|
||||||
|
if (v) {
|
||||||
|
prevDays += v;
|
||||||
|
if (prevDays < -1) prevDays = -1;
|
||||||
|
daySelection();
|
||||||
|
} else {
|
||||||
|
fromMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// define day to display and analysis timeout id
|
// open settings menu
|
||||||
var prevDays = 0;
|
function openSettings() {
|
||||||
var ATID;
|
// disable back behaviour to prevent bouncing on return
|
||||||
|
backListener = () => {};
|
||||||
|
// open settings menu
|
||||||
|
eval(require("Storage").read("sleeplog.settings.js"))(fromMenu);
|
||||||
|
}
|
||||||
|
|
||||||
// setup app
|
// draw progress as bar, increment on undefined percentage
|
||||||
g.clear();
|
function drawProgress(progress) {
|
||||||
|
g.fillRect(19, 147, 136 * progress + 19, 149);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (re)draw info data
|
||||||
|
function drawInfo() {
|
||||||
|
// set active info type
|
||||||
|
var info = infoData[infoType % infoData.length];
|
||||||
|
// draw info
|
||||||
|
g.clearRect(0, 69, 175, 105).reset()
|
||||||
|
.setFont("6x8").setFontAlign(-1, -1)
|
||||||
|
.drawString(info[0][0], 10, 70)
|
||||||
|
.drawString(info[1][0], 10, 90)
|
||||||
|
.setFont("12x20").setFontAlign(1, -1)
|
||||||
|
.drawString((info[0][1] / 60 | 0) + "h " + (info[0][1] % 60) + "min", 166, 70)
|
||||||
|
.drawString((info[1][1] / 60 | 0) + "h " + (info[1][1] % 60) + "min", 166, 90);
|
||||||
|
// free ram
|
||||||
|
info = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// draw graph for log segment
|
||||||
|
function drawGraph(log, date, pos) {
|
||||||
|
// set y position
|
||||||
|
var y = pos ? 144 : 110;
|
||||||
|
// clear area
|
||||||
|
g.reset().clearRect(0, y, width, y + 33);
|
||||||
|
// draw x axis
|
||||||
|
g.drawLine(0, y + 19, width, y + 19);
|
||||||
|
// draw x label
|
||||||
|
var stepWidth = width / 12;
|
||||||
|
var startHour = date.getHours() + (pos ? 0 : 12);
|
||||||
|
for (var x = 0; x < 12; x++) {
|
||||||
|
g.fillRect(x * stepWidth, y + 20, x * stepWidth, y + 22);
|
||||||
|
g.setFontAlign(-1, -1).setFont("6x8")
|
||||||
|
.drawString((startHour + x) % 24, x * stepWidth + 1, y + 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set height and color values:
|
||||||
|
// status: unknown, not worn, awake, light sleep, deep sleep, consecutive
|
||||||
|
// color: black, red, green, cyan, blue, violet
|
||||||
|
var heights = [0, 0.4, 0.6, 0.8, 1];
|
||||||
|
var colors = [0, 63488, 2016, 2047, 31, 32799];
|
||||||
|
|
||||||
|
// cycle through log
|
||||||
|
log.forEach((entry, index, log) => {
|
||||||
|
// calculate positions
|
||||||
|
var x1 = Math.ceil((entry[0] - log[0][0]) / 72 * width);
|
||||||
|
var x2 = Math.floor(((log[index + 1] || [date / 6E5])[0] - log[0][0]) / 72 * width);
|
||||||
|
// calculate y2 position
|
||||||
|
var y2 = y + 18 * (1 - heights[entry[1]]);
|
||||||
|
// set color depending on status and consecutive sleep
|
||||||
|
g.setColor(colors[entry[2] === 2 ? 5 : entry[1]]);
|
||||||
|
// clear area, draw bar and top line
|
||||||
|
g.clearRect(x1, y, x2, y + 18);
|
||||||
|
g.fillRect(x1, y + 18, x2, y2).reset();
|
||||||
|
if (y + 18 !== y2) g.fillRect(x1, y2, x2, y2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw information in an interruptable cycle
|
||||||
|
function drawingCycle(calcDate, thisID, cycle, log) {
|
||||||
|
// reset analysis timeout ID
|
||||||
|
ATID = undefined;
|
||||||
|
|
||||||
|
// check drawing ID to continue
|
||||||
|
if (thisID !== drawingID) return;
|
||||||
|
|
||||||
|
// check cycle
|
||||||
|
if (!cycle) {
|
||||||
|
/* read log on first cycle */
|
||||||
|
// set initial cycle
|
||||||
|
cycle = 1;
|
||||||
|
|
||||||
|
// read log
|
||||||
|
log = slMod.readLog(calcDate - 864E5, calcDate);
|
||||||
|
|
||||||
|
// draw progress
|
||||||
|
drawProgress(0.6);
|
||||||
|
} else if (cycle === 2) {
|
||||||
|
/* draw stats on second cycle */
|
||||||
|
|
||||||
|
// read stats and process into info data
|
||||||
|
infoData = slMod.getStats(calcDate, 0, log);
|
||||||
|
infoData = [
|
||||||
|
[
|
||||||
|
[ /*LANG*/"consecutive\nsleeping", infoData.consecSleep],
|
||||||
|
[ /*LANG*/"true\nsleeping", infoData.deepSleep + infoData.lightSleep]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[ /*LANG*/"deep\nsleep", infoData.deepSleep],
|
||||||
|
[ /*LANG*/"light\nsleep", infoData.lightSleep]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[ /*LANG*/"awake", infoData.awakeTime],
|
||||||
|
[ /*LANG*/"not worn", infoData.notWornTime]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
// draw info
|
||||||
|
drawInfo();
|
||||||
|
|
||||||
|
// draw progress
|
||||||
|
drawProgress(0.9);
|
||||||
|
} else if (cycle === 3) {
|
||||||
|
/* segment log on third cycle */
|
||||||
|
// calculate segmentation date in 10min steps and index of the segmentation
|
||||||
|
var segmDate = calcDate / 6E5 - 72;
|
||||||
|
var segmIndex = log.findIndex(entry => entry[0] >= segmDate);
|
||||||
|
|
||||||
|
// check if segmentation neccessary
|
||||||
|
if (segmIndex > 0) {
|
||||||
|
// split log
|
||||||
|
log = [log.slice(segmIndex), log.slice(0, segmIndex)];
|
||||||
|
// add entry at segmentation point
|
||||||
|
if (log[0][0] !== segmDate)
|
||||||
|
log[0].unshift([segmDate, log[1][segmIndex - 1][1], log[1][segmIndex - 1][2]]);
|
||||||
|
} else if (segmIndex < 0) {
|
||||||
|
// set log as second log entry
|
||||||
|
log = [
|
||||||
|
[], log
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// add entry at segmentation point
|
||||||
|
if (log[0] !== segmDate) log.unshift([segmDate, 0, 0]);
|
||||||
|
// set log as first log entry
|
||||||
|
log = [log, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw progress
|
||||||
|
drawProgress(1);
|
||||||
|
} else if (cycle === 4) {
|
||||||
|
/* draw upper graph on fourth cycle */
|
||||||
|
drawGraph(log[0], calcDate, 0);
|
||||||
|
} else if (cycle === 5) {
|
||||||
|
/* draw upper graph on fifth cycle */
|
||||||
|
drawGraph(log[1], calcDate, 1);
|
||||||
|
} else {
|
||||||
|
/* stop cycle and set drawing finished */
|
||||||
|
drawingID = 0;
|
||||||
|
// give feedback
|
||||||
|
Bangle.buzz(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
// initiate next cycle if defined
|
||||||
|
if (thisID === drawingID) ATID = setTimeout(drawingCycle, 10, calcDate, drawingID, ++cycle, log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return from a menu
|
||||||
|
function fromMenu() {
|
||||||
|
// reset UI to custom mode
|
||||||
|
Bangle.setUI(customUI);
|
||||||
|
// enable back behaviour delayed to prevent bouncing
|
||||||
|
setTimeout(() => backListener = load, 500);
|
||||||
|
// redraw app
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw app
|
||||||
|
function draw() {
|
||||||
|
// stop ongoing drawing
|
||||||
|
drawingID++;
|
||||||
|
if (ATID) ATID = clearTimeout(ATID);
|
||||||
|
|
||||||
|
// clear app area
|
||||||
|
g.reset().clearRect(0, 24, width, width);
|
||||||
|
|
||||||
|
// set date to calculate data for
|
||||||
|
var calcDate = new Date(startDate - prevDays * 864E5);
|
||||||
|
|
||||||
|
// draw title
|
||||||
|
g.setFont("12x20").setFontAlign(0, -1)
|
||||||
|
.drawString( /*LANG*/"Night to " + require('locale').dow(calcDate, 1) + "\n" +
|
||||||
|
require('locale').date(calcDate, 1), 87, 28);
|
||||||
|
|
||||||
|
// reset graphics and define image string
|
||||||
|
g.reset();
|
||||||
|
var imgStr = "";
|
||||||
|
// check which icon to set
|
||||||
|
if (!global.sleeplog || sleeplog.conf.enabled !== true) {
|
||||||
|
// set color and disabled service icon
|
||||||
|
g.setColor(1, 0, 0);
|
||||||
|
imgStr = "FBSBAOAAfwAP+AH3wD4+B8Hw+A+fAH/gA/wAH4AB+AA/wAf+APnwHw+D4Hx8A++AH/AA/gAH";
|
||||||
|
} else if (sleeplog.debug) {
|
||||||
|
// set debugging icon
|
||||||
|
imgStr = typeof sleeplog.debug === "object" ?
|
||||||
|
"FBSBAB/4AQDAF+4BfvAX74F+CBf+gX/oFJKBf+gUkoF/6BSSgX/oFJ6Bf+gX/oF/6BAAgf/4" : // file
|
||||||
|
"FBSBAP//+f/V///4AAGAABkAAZgAGcABjgAYcAGDgBhwAY4AGcABmH+ZB/mAABgAAYAAH///"; // console
|
||||||
|
}
|
||||||
|
// draw service and settings icon
|
||||||
|
if (imgStr) g.drawImage(atob(imgStr), 2, 36);
|
||||||
|
g.reset().drawImage(atob("FBSBAAAeAAPgAHwAB4AA8AAPAwDwcA+PAP/wH/4D/8B/8A/gAfwAP4AH8AD+AA/AAPgABwAA"), width - 22, 36);
|
||||||
|
|
||||||
|
// show loading info with progresss bar
|
||||||
|
g.reset().drawRect(7, 117, width - 8, 157)
|
||||||
|
.setFont("6x8").setFontAlign(0, 0)
|
||||||
|
.drawString( /*LANG*/ "calculating data ...\nplease be patient :)", 87, 133)
|
||||||
|
.drawRect(17, 145, 157, 151);
|
||||||
|
|
||||||
|
// draw first progress
|
||||||
|
drawProgress(0.1);
|
||||||
|
|
||||||
|
// initiate drawing cycle
|
||||||
|
ATID = setTimeout(drawingCycle, 10, calcDate, drawingID, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// define sleeplog module
|
||||||
|
var slMod = require("sleeplog");
|
||||||
|
|
||||||
|
// read app timeout from settings
|
||||||
|
var appTimeout = (require("Storage").readJSON("sleeplog.json", true) || {}).appTimeout;
|
||||||
|
|
||||||
|
// set listener for back button
|
||||||
|
var backListener = load;
|
||||||
|
// define custom UI mode
|
||||||
|
var customUI = {
|
||||||
|
mode: "custom",
|
||||||
|
back: backListener,
|
||||||
|
touch: touchListener,
|
||||||
|
swipe: swipeListener
|
||||||
|
};
|
||||||
|
|
||||||
|
// define start values
|
||||||
|
var startDate = slMod.getLastBreak(); // date to start from
|
||||||
|
var prevDays = 0; // number of previous days to display
|
||||||
|
var infoType = 0; // type of info data to display
|
||||||
|
var infoData; // storage for info data
|
||||||
|
var ATID; // analysis timeout ID
|
||||||
|
var drawingID = 0; // drawing ID for ongoing process
|
||||||
|
// get screen width and center (zero based)
|
||||||
|
var width = g.getWidth() - 1;
|
||||||
|
var center = width / 2 - 1;
|
||||||
|
|
||||||
|
// set areas and actions array
|
||||||
|
var aaa = [
|
||||||
|
// day selection
|
||||||
|
{
|
||||||
|
x0: 26,
|
||||||
|
x1: width - 26,
|
||||||
|
y0: 24,
|
||||||
|
y1: 68,
|
||||||
|
interrupt: true,
|
||||||
|
funct: () => daySelection()
|
||||||
|
},
|
||||||
|
// open settings
|
||||||
|
{
|
||||||
|
x0: width - 26,
|
||||||
|
x1: width,
|
||||||
|
y0: 24,
|
||||||
|
y1: 68,
|
||||||
|
interrupt: true,
|
||||||
|
funct: () => openSettings()
|
||||||
|
},
|
||||||
|
// change info type
|
||||||
|
{
|
||||||
|
x0: 0,
|
||||||
|
x1: width,
|
||||||
|
y0: 69,
|
||||||
|
y1: 105,
|
||||||
|
funct: () => {
|
||||||
|
// change info type
|
||||||
|
infoType++;
|
||||||
|
// redraw info
|
||||||
|
drawInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// clear and reset screen
|
||||||
|
g.clear(true);
|
||||||
|
|
||||||
|
// load and draw widgets
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
// start app
|
// set UI in custom mode
|
||||||
startApp();
|
Bangle.setUI(customUI);
|
||||||
|
|
||||||
|
// set app timeout if defined
|
||||||
|
if (appTimeout) Bangle.setOptions({
|
||||||
|
lockTimeout: appTimeout,
|
||||||
|
backlightTimeout: appTimeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// draw app
|
||||||
|
draw();
|
||||||
|
|
|
@ -1,169 +1,330 @@
|
||||||
// Sleep/Wake detection with Estimation of Stationary Sleep-segments (ESS):
|
// sleeplog.status values:
|
||||||
// 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.
|
// undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep
|
||||||
// https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en
|
// sleeplog.consecutive values:
|
||||||
|
// undefined = service stopped, 0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep
|
||||||
|
|
||||||
// sleeplog.status values: undefined = service stopped, 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
// create global object with settings
|
||||||
|
global.sleeplog = {
|
||||||
// load settings into global object
|
conf: Object.assign({
|
||||||
global.sleeplog = Object.assign({
|
// main settings
|
||||||
enabled: true, // en-/disable completely
|
enabled: true, // en-/disable completely
|
||||||
logfile: "sleeplog.log", // logfile
|
// threshold settings
|
||||||
powersaving: false, // disables ESS and uses build in movement detection
|
maxAwake: 36E5, // [ms] maximal awake time to count for consecutive sleep
|
||||||
winwidth: 13, // 13 values, read with 12.5Hz = every 1.04s
|
minConsec: 18E5, // [ms] minimal time to count for consecutive sleep
|
||||||
nomothresh: 0.012, // values lower than 0.008 getting triggert by noise
|
deepTh: 100, // threshold for deep sleep
|
||||||
sleepthresh: 577, // 577 times no movement * 1.04s window width > 10min
|
lightTh: 200, // threshold for light sleep
|
||||||
maxmove: 100, // movement threshold on power saving mode
|
}, require("Storage").readJSON("sleeplog.json", true) || {})
|
||||||
tempthresh: 27, // every temperature above ist registered as worn
|
};
|
||||||
}, require("Storage").readJSON("sleeplog.json", true) || {});
|
|
||||||
|
|
||||||
// delete app settings
|
|
||||||
["breaktod", "maxawake", "minconsec"].forEach(property => delete sleeplog[property]);
|
|
||||||
|
|
||||||
// check if service enabled
|
|
||||||
if (sleeplog.enabled) {
|
|
||||||
|
|
||||||
// add always used values and functions to global object
|
|
||||||
sleeplog = Object.assign(sleeplog, {
|
|
||||||
// set cached values
|
|
||||||
resting: undefined,
|
|
||||||
status: undefined,
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
// write log with undefined sleeping status
|
|
||||||
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
|
|
||||||
// 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 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();
|
|
||||||
},
|
|
||||||
|
|
||||||
|
// check if service is enabled
|
||||||
|
if (sleeplog.conf.enabled) {
|
||||||
|
// assign functions to global object
|
||||||
|
global.sleeplog = Object.assign({
|
||||||
// define function to initialy start or restart the service
|
// define function to initialy start or restart the service
|
||||||
start: function() {
|
start: function() {
|
||||||
// add kill listener
|
// add kill and health listener
|
||||||
E.on('kill', sleeplog.stopHandler);
|
E.on('kill', sleeplog.saveStatus);
|
||||||
// add health listener if defined and
|
Bangle.on('health', sleeplog.health);
|
||||||
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);
|
|
||||||
// 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];
|
|
||||||
// update resting according to status
|
|
||||||
sleeplog.resting = sleeplog.status % 2;
|
|
||||||
// write restored status to log
|
|
||||||
require("sleeplog").writeLog(0, [Math.floor(Date.now()), sleeplog.status]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// check for power saving mode
|
// restore saved status
|
||||||
if (sleeplog.powersaving) {
|
this.restoreStatus();
|
||||||
// power saving mode using build in movement detection
|
|
||||||
// delete unused settings
|
|
||||||
["winwidth", "nomothresh", "sleepthresh"].forEach(property => delete sleeplog[property]);
|
|
||||||
// add cached values and functions to global object
|
|
||||||
sleeplog = Object.assign(sleeplog, {
|
|
||||||
// define health listener function
|
|
||||||
health: function(data) {
|
|
||||||
// set global object and check for existence
|
|
||||||
var gObj = global.sleeplog;
|
|
||||||
if (!gObj) return;
|
|
||||||
|
|
||||||
// calculate timestamp for this measurement
|
|
||||||
var timestamp = Math.floor(Date.now() - 6E5);
|
|
||||||
|
|
||||||
// check for non-movement according to the threshold
|
|
||||||
if (data.movement <= gObj.maxmove) {
|
|
||||||
// check resting state
|
|
||||||
if (gObj.resting !== true) {
|
|
||||||
// change resting state
|
|
||||||
gObj.resting = true;
|
|
||||||
// set status to sleeping or worn
|
|
||||||
gObj.status = E.getTemperature() > gObj.tempthresh ? 3 : 1;
|
|
||||||
// write status to log,
|
|
||||||
require("sleeplog").writeLog(0, [timestamp, gObj.status, E.getTemperature()]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check resting state
|
|
||||||
if (gObj.resting !== false) {
|
|
||||||
// change resting state, set status and write status to log
|
|
||||||
gObj.resting = false;
|
|
||||||
gObj.status = 2;
|
|
||||||
require("sleeplog").writeLog(0, [timestamp, 2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// full ESS calculation
|
|
||||||
// add cached values and functions to global object
|
|
||||||
sleeplog = Object.assign(sleeplog, {
|
|
||||||
// set cached values
|
|
||||||
ess_values: [],
|
|
||||||
nomocount: 0,
|
|
||||||
firstnomodate: undefined,
|
|
||||||
|
|
||||||
// define acceleration listener function
|
|
||||||
accel: function(xyz) {
|
|
||||||
// save acceleration magnitude and start calculation on enough saved data
|
|
||||||
if (global.sleeplog && sleeplog.ess_values.push(xyz.mag) >= sleeplog.winwidth) sleeplog.calc();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// define calculator function
|
// define function to stop the service, it will be restarted on reload if enabled
|
||||||
calc: function() {
|
stop: function() {
|
||||||
// exit on wrong this
|
// remove all listeners
|
||||||
if (this.enabled === undefined) return;
|
Bangle.removeListener('health', sleeplog.health);
|
||||||
// calculate standard deviation over
|
E.removeListener('kill', sleeplog.saveStatus);
|
||||||
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
|
// save active values
|
||||||
if (stddev < this.nomothresh) {
|
this.saveStatus();
|
||||||
// increment non-movement sections count, set date of first non-movement
|
// reset active values
|
||||||
if (++this.nomocount == 1) this.firstnomodate = Math.floor(Date.now());
|
this.status = undefined;
|
||||||
// check resting state and non-movement count against threshold
|
this.consecutive = undefined;
|
||||||
if (this.resting !== true && this.nomocount >= this.sleepthresh) {
|
},
|
||||||
// change resting state
|
|
||||||
this.resting = true;
|
// define function to restore active values on a restart or reload
|
||||||
// set status to sleeping or worn
|
restoreStatus: function() {
|
||||||
this.status = E.getTemperature() > this.tempthresh ? 3 : 1;
|
// define restore objects with default values
|
||||||
// write status to log, with first no movement timestamp
|
var restore = {
|
||||||
require("sleeplog").writeLog(0, [this.firstnomodate, this.status, E.getTemperature()]);
|
status: 0,
|
||||||
}
|
consecutive: 0,
|
||||||
} else {
|
info: {}
|
||||||
// reset non-movement sections count
|
};
|
||||||
this.nomocount = 0;
|
|
||||||
// check resting state
|
// open log file
|
||||||
if (this.resting !== false) {
|
var file = require("Storage").open("sleeplog.log", "r");
|
||||||
// change resting state and set status
|
// read last 55 chars from log
|
||||||
this.resting = false;
|
file.read(file.getLength() - 52);
|
||||||
this.status = 2;
|
file = file.read(52);
|
||||||
// write status to log
|
|
||||||
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 2]);
|
// check if the log contains data
|
||||||
}
|
if (file) {
|
||||||
}
|
// remove unneeded data
|
||||||
|
file = file.trim().split("\n").reverse().filter((e, i) => i < 2);
|
||||||
|
// convert file into accessable array
|
||||||
|
file = file.map(e => e.split(",").map(e => parseInt(e)));
|
||||||
|
|
||||||
|
// add default data if no previous status is available
|
||||||
|
if (file.length < 2 || file[1].length !== 3) file.push([0, 0, 0]);
|
||||||
|
|
||||||
|
// check if data to restore has been saved
|
||||||
|
if (file[0].length > 3) {
|
||||||
|
// read data into restore object
|
||||||
|
restore = {
|
||||||
|
status: file[1][1],
|
||||||
|
consecutive: file[1][2],
|
||||||
|
info: {
|
||||||
|
lastChange: file[1][0] * 6E5,
|
||||||
|
lastCheck: file[0][1] * 6E5,
|
||||||
|
awakeSince: file[0][2] * 6E5,
|
||||||
|
asleepSince: file[0][3] * 6E5
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// add debug if set
|
||||||
|
if (file[0].length === 6)
|
||||||
|
restore = Object.assign(restore, {
|
||||||
|
debug: file[0][4] ? {
|
||||||
|
writeUntil: file[0][4] * 6E5,
|
||||||
|
fileid: file[0][5]
|
||||||
|
} : true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// calculate timestamp in 10min steps, corrected to 10min ago
|
||||||
|
var timestamp = (Date.now() / 6E5 | 0) - 1;
|
||||||
|
|
||||||
|
// check if restored status not unknown and lastCheck was 20min before timestamp
|
||||||
|
if (restore.status && restore.info.lastCheck + 12E5 < timestamp) {
|
||||||
|
// set status and consecutive to unknown
|
||||||
|
restore.status = 0;
|
||||||
|
restore.consecutive = 0;
|
||||||
|
restore.info.lastChange = restore.info.lastCheck + 6E5;
|
||||||
|
restore.info.lastCheck = timestamp;
|
||||||
|
// write undefined status 10min after restored lastCheck
|
||||||
|
this.appendStatus(restore.info.lastChange, 0, 0);
|
||||||
|
} else {
|
||||||
|
// set saveUpToDate
|
||||||
|
restore.info.saveUpToDate = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write restored values into global object
|
||||||
|
global.sleeplog = Object.assign(this, restore);
|
||||||
|
},
|
||||||
|
|
||||||
|
// define function to save active values on a stop or kill event
|
||||||
|
// - called by event listener: "this"-reference points to global
|
||||||
|
saveStatus: function(force) {
|
||||||
|
// check if global variable accessable
|
||||||
|
if (!global.sleeplog) return new Error("sleeplog: Can't save status, global object missing!");
|
||||||
|
|
||||||
|
// check saveUpToDate is not set or forced
|
||||||
|
if (!sleeplog.info.saveUpToDate || force) {
|
||||||
|
// save status, consecutive status and info timestamps to restore on reload
|
||||||
|
var save = [sleeplog.info.lastCheck, sleeplog.info.awakeSince, sleeplog.info.asleepSince];
|
||||||
|
// add debuging status if active
|
||||||
|
if (sleeplog.debug) save.push(sleeplog.debug.writeUntil, sleeplog.debug.fileid);
|
||||||
|
|
||||||
|
// stringify entries
|
||||||
|
save = "," + save.map((entry, index) => {
|
||||||
|
if (index < 4) entry /= 6E5; // store in 10min steps
|
||||||
|
return entry | 0; // sanitize
|
||||||
|
}).join(",") + "\n";
|
||||||
|
|
||||||
|
// add present status if forced
|
||||||
|
if (force) save = (sleeplog.info.lastChange / 6E5) + "," +
|
||||||
|
sleeplog.status + "," + sleeplog.consecutive + "\n" + save;
|
||||||
|
|
||||||
|
// append saved data to StorageFile
|
||||||
|
require("Storage").open("sleeplog.log", "a").write(save);
|
||||||
|
|
||||||
|
// clear save string to free ram
|
||||||
|
save = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// define health listener function
|
||||||
|
// - called by event listener: "this"-reference points to global
|
||||||
|
health: function(data) {
|
||||||
|
// check if global variable accessable
|
||||||
|
if (!global.sleeplog) return new Error("sleeplog: Can't process health event, global object missing!");
|
||||||
|
|
||||||
|
// check if movement is available
|
||||||
|
if (!data.movement) return;
|
||||||
|
|
||||||
|
// add timestamp rounded to 10min, corrected to 10min ago
|
||||||
|
data.timestamp = data.timestamp || ((Date.now() / 6E5 | 0) - 1) * 6E5;
|
||||||
|
|
||||||
|
// add preliminary status depending on charging and movement thresholds
|
||||||
|
data.status = Bangle.isCharging() ? 1 :
|
||||||
|
data.movement <= sleeplog.conf.deepTh ? 4 :
|
||||||
|
data.movement <= sleeplog.conf.lightTh ? 3 : 2;
|
||||||
|
|
||||||
|
// check if changing to deep sleep from non sleepling
|
||||||
|
if (data.status === 4 && sleeplog.status <= 2) {
|
||||||
|
// check wearing status
|
||||||
|
sleeplog.checkIsWearing((isWearing, data) => {
|
||||||
|
// correct status
|
||||||
|
if (!isWearing) data.status = 1;
|
||||||
|
// set status
|
||||||
|
sleeplog.setStatus(data);
|
||||||
|
}, data);
|
||||||
|
} else {
|
||||||
|
// set status
|
||||||
|
sleeplog.setStatus(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// define function to check if the bangle is worn by using the hrm
|
||||||
|
checkIsWearing: function(returnFn, data) {
|
||||||
|
// create a temporary object to store data and functions
|
||||||
|
global.tmpWearingCheck = {
|
||||||
|
// define temporary hrm listener function to read the wearing status
|
||||||
|
hrmListener: hrm => tmpWearingCheck.isWearing = hrm.isWearing,
|
||||||
|
// set default wearing status
|
||||||
|
isWearing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// enable HRM
|
||||||
|
Bangle.setHRMPower(true, "wearingCheck");
|
||||||
|
// wait until HRM is initialised
|
||||||
|
setTimeout((returnFn, data) => {
|
||||||
|
// add HRM listener
|
||||||
|
Bangle.on('HRM-raw', tmpWearingCheck.hrmListener);
|
||||||
|
// wait for two cycles (HRM working on 60Hz)
|
||||||
|
setTimeout((returnFn, data) => {
|
||||||
|
// remove listener and disable HRM
|
||||||
|
Bangle.removeListener('HRM-raw', tmpWearingCheck.hrmListener);
|
||||||
|
Bangle.setHRMPower(false, "wearingCheck");
|
||||||
|
// cache wearing status
|
||||||
|
var isWearing = tmpWearingCheck.isWearing;
|
||||||
|
// clear temporary object
|
||||||
|
delete global.tmpWearingCheck;
|
||||||
|
// call return function with status
|
||||||
|
returnFn(isWearing, data);
|
||||||
|
}, 34, returnFn, data);
|
||||||
|
}, 2500, returnFn, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// define function to set the status
|
||||||
|
setStatus: function(data) {
|
||||||
|
// update lastCheck
|
||||||
|
this.info.lastCheck = data.timestamp;
|
||||||
|
|
||||||
|
// correct light sleep status to awake if
|
||||||
|
// previous status not deep sleep and not too long awake (asleepSince unset)
|
||||||
|
if (data.status === 3 && this.status !== 4 && !this.info.asleepSince) {
|
||||||
|
data.status = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache consecutive status to check for changes later on
|
||||||
|
data.consecutive = this.consecutive;
|
||||||
|
|
||||||
|
// check if changing to deep sleep from non sleepling
|
||||||
|
if (data.status === 4 && this.status <= 2) {
|
||||||
|
// set asleepSince if undefined
|
||||||
|
this.info.asleepSince = this.info.asleepSince || data.timestamp;
|
||||||
|
// reset consecutive status
|
||||||
|
data.consecutive = 0;
|
||||||
|
// check if changing to awake
|
||||||
|
} else if (data.status === 2 && this.status > 2) {
|
||||||
|
// set awakeSince if undefined
|
||||||
|
this.info.awakeSince = this.info.awakeSince || data.timestamp;
|
||||||
|
// reset consecutive status
|
||||||
|
data.consecutive = 0;
|
||||||
|
}
|
||||||
|
// check if consecutive unknown
|
||||||
|
if (!this.consecutive) {
|
||||||
|
// check if long enough asleep or too long awake
|
||||||
|
if (data.status === 4 && this.info.asleepSince &&
|
||||||
|
this.info.asleepSince + this.conf.minConsec <= data.timestamp) {
|
||||||
|
// set consecutive sleep
|
||||||
|
data.consecutive = 2;
|
||||||
|
// reset awakeSince
|
||||||
|
this.info.awakeSince = 0;
|
||||||
|
} else if (data.status <= 2 && this.info.awakeSince &&
|
||||||
|
this.info.awakeSince + this.conf.maxAwake <= data.timestamp) {
|
||||||
|
// set non consecutive sleep
|
||||||
|
data.consecutive = 1;
|
||||||
|
// reset asleepSince
|
||||||
|
this.info.asleepSince = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache change into a known consecutive state
|
||||||
|
var changeIntoConsec = data.consecutive;
|
||||||
|
|
||||||
|
// check if the status has changed
|
||||||
|
if (data.status !== this.status || data.consecutive !== this.consecutive) {
|
||||||
|
// append status
|
||||||
|
this.appendStatus(data.timestamp, data.status, data.consecutive);
|
||||||
|
|
||||||
|
// set new states and update lastChange
|
||||||
|
this.status = data.status;
|
||||||
|
this.consecutive = data.consecutive;
|
||||||
|
this.info.lastChange = data.timestamp;
|
||||||
|
// reset saveUpToDate status
|
||||||
|
delete this.info.saveUpToDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send status to gadgetbridge
|
||||||
|
var gb_kinds = "unknown,not_worn,activity,light_sleep,deep_sleep";
|
||||||
|
Bluetooth.println(JSON.stringify({
|
||||||
|
t: "act",
|
||||||
|
act: gb_kinds.split(",")[data.status],
|
||||||
|
ts: data.timestamp
|
||||||
|
}));
|
||||||
|
|
||||||
|
// call debugging function if set
|
||||||
|
if (this.debug) require("sleeplog").debug(data);
|
||||||
|
|
||||||
|
// check if changed into known consecutive state
|
||||||
|
if (changeIntoConsec) {
|
||||||
|
// check if change is to consecutive sleep or not
|
||||||
|
if (changeIntoConsec === 2) {
|
||||||
|
// call move log function
|
||||||
|
require("sleeplog").moveLog();
|
||||||
|
} else {
|
||||||
|
// update stats cache if available
|
||||||
|
if (this.statsCache) this.statsCache = require("sleeplog").getStats();
|
||||||
|
}
|
||||||
|
// remove module from cache if cached
|
||||||
|
if (Modules.getCached().includes("sleeplog")) Modules.removeCached("sleeplog");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// define function to append the status to the StorageFile log
|
||||||
|
appendStatus: function(timestamp, status, consecutive) {
|
||||||
|
// exit on missing timestamp
|
||||||
|
if (!timestamp) return;
|
||||||
|
// reduce timestamp to 10min step
|
||||||
|
timestamp = timestamp / 6E5 | 0;
|
||||||
|
// append to StorageFile
|
||||||
|
require("Storage").open("sleeplog.log", "a").write(
|
||||||
|
[timestamp, status || 0, consecutive || 0].join(",") + "\n"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// define function to access stats of the last night
|
||||||
|
getStats: function() {
|
||||||
|
// check if stats cache is not defined or older than 12h
|
||||||
|
// if stats cache is set it will be updated on every change to non consecutive sleep
|
||||||
|
if (this.statsCache === undefined || this.statsCache.calculatedAt + 432E5 < Date.now()) {
|
||||||
|
// read stats of the last night into cache and remove module from cache
|
||||||
|
this.statsCache = require("sleeplog").getStats();
|
||||||
|
// remove module from cache if cached
|
||||||
|
if (Modules.getCached().includes("sleeplog")) Modules.removeCached("sleeplog");
|
||||||
|
}
|
||||||
|
// return stats cache
|
||||||
|
return this.statsCache;
|
||||||
|
}
|
||||||
|
}, sleeplog);
|
||||||
|
|
||||||
// initial starting
|
// initial starting
|
||||||
global.sleeplog.start();
|
global.sleeplog.start();
|
||||||
|
} else {
|
||||||
|
// clear global object from ram
|
||||||
|
delete global.sleeplog;
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 642 B |
Before Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 635 B |
|
@ -0,0 +1,185 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
|
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="table"></div>
|
||||||
|
<div id="view"></div>
|
||||||
|
<script src="../../core/lib/interface.js"></script>
|
||||||
|
<script>
|
||||||
|
var domTable = document.getElementById("table");
|
||||||
|
var domView = document.getElementById("view");
|
||||||
|
|
||||||
|
function viewLog(logData, filename) {
|
||||||
|
domView.innerHTML = "";
|
||||||
|
var html = `
|
||||||
|
<div class="container">
|
||||||
|
<h4><b>Viewing data of:</b> ` + filename + `</h4>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Sleep</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`;
|
||||||
|
logData.forEach((entry, index, log) => {
|
||||||
|
var duration = ((log[index + 1] || [Math.floor(Date.now() / 6E5)])[0] - entry[0]) * 10;
|
||||||
|
html += `
|
||||||
|
<tr style="text-align: center">
|
||||||
|
<td style="text-align: right">` +
|
||||||
|
new Date(entry[0] * 6E5).toLocaleDateString(undefined) + `
|
||||||
|
</td>
|
||||||
|
<td>` +
|
||||||
|
new Date(entry[0] * 6E5).toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}) + `
|
||||||
|
</td>
|
||||||
|
<td style="text-align: right"><div class="container"` +
|
||||||
|
(duration >= 60 ? ` style="background-color: hsl(192, 50%, ` +
|
||||||
|
(duration > 480 ? 50 : 100 - Math.floor(duration / 60) * 50 / 8) +
|
||||||
|
`%)">` : `>`) +
|
||||||
|
duration + ` min
|
||||||
|
</td>
|
||||||
|
<td><div class="container" style="background-color: ` +
|
||||||
|
["orange", "lightcoral", "lightgreen", "lightcyan", "lightskyblue"][entry[1]] + `">` +
|
||||||
|
["unknown", "not worn", "awake", "light sleep", "deep sleep"][entry[1]] + `
|
||||||
|
</div></td>
|
||||||
|
<td><div class="container" style="background-color: ` +
|
||||||
|
["orange", "lightgreen", "lightskyblue"][entry[2]] + `">` +
|
||||||
|
["unknown", "non consecutive", "consecutive"][entry[2]] + `
|
||||||
|
</div></td>
|
||||||
|
</tr>`
|
||||||
|
});
|
||||||
|
html += `
|
||||||
|
</tbody>
|
||||||
|
</table>`;
|
||||||
|
domView.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCSV(logData, date0, date1) {
|
||||||
|
var csvTime = document.getElementById("csvTime").selectedIndex;
|
||||||
|
var filename = "sleeplog_" +
|
||||||
|
new Date(date0).toISOString().substr(0, 10) + "_" +
|
||||||
|
new Date(date1).toISOString().substr(5, 5);
|
||||||
|
logData = logData.map(entry => {
|
||||||
|
entry[0] *= 6E5;
|
||||||
|
if (csvTime === 1) entry[0] /= 1E3;
|
||||||
|
if (csvTime === 2) entry[0] = entry[0] / 864E5 + 25569;
|
||||||
|
return entry.join(",");
|
||||||
|
}).join("\n");
|
||||||
|
Util.saveCSV(filename, "time,sleep,consecutive\n" + logData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLog(date, callback) {
|
||||||
|
Util.showModal("Downloading logged data...");
|
||||||
|
Puck.eval(`require("sleeplog").readLog(` + date + `, ` + date + ` + 12096E5)`, logData => {
|
||||||
|
Util.hideModal();
|
||||||
|
callback(logData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile(filename, callback) {
|
||||||
|
if (window.confirm("Do you really want to remove " + filename)) {
|
||||||
|
Util.showModal("Deleting...");
|
||||||
|
if (filename.endsWith(" (StorageFile)")) {
|
||||||
|
Util.eraseStorageFile(filename, () => {
|
||||||
|
Util.hideModal();
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Util.eraseStorage(filename, () => {
|
||||||
|
Util.hideModal();
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewFiles() {
|
||||||
|
Util.showModal("Loading...");
|
||||||
|
domTable.innerHTML = "";
|
||||||
|
Puck.eval(`require("Storage").list(/^sleeplog_\\d\\d\\d\\d\\.log$/)`, files => {
|
||||||
|
// add active log
|
||||||
|
files.push("" + Math.floor(Date.now() / 12096E5 - 0.25));
|
||||||
|
files = files.map(file => { return {
|
||||||
|
filename: file.length === 4 ? "sleeplog.log (StorageFile)" : file,
|
||||||
|
date: (parseInt(file.match(/\d{4}/)[0]) + 0.25) * 12096E5
|
||||||
|
}});
|
||||||
|
files = files.sort((a, b) => a.date - b.date);
|
||||||
|
var html = `
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>from</th>
|
||||||
|
<th>to</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`;
|
||||||
|
files.forEach(file => { html += `
|
||||||
|
<tr>
|
||||||
|
<td>${file.filename}</td>
|
||||||
|
<td>${new Date(file.date).toLocaleDateString(undefined)}</td>
|
||||||
|
<td>${new Date(file.date + 12096E5).toLocaleDateString(undefined)}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm tooltip" data-tooltip="view data" task="view" filename="${file.filename}" date="${file.date}">
|
||||||
|
<i class="icon icon-caret"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm tooltip btn-primary" data-tooltip="save csv-file" task="csv" filename="${file.filename}" date="${file.date}">
|
||||||
|
<i class="icon icon-download"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm tooltip btn-error" data-tooltip="delete file" task="del" filename="${file.filename}" date="${file.date}">
|
||||||
|
<i class="icon icon-delete"></i>
|
||||||
|
</button>`;
|
||||||
|
html += `
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="container">
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-3 col-sm-12">
|
||||||
|
<label class="form-label"><b>csv time format</b></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-9 col-sm-12">
|
||||||
|
<select class="form-select" id="csvTime">
|
||||||
|
<option>JavaScript (milliseconds since 1970)</option>
|
||||||
|
<option>UNIX (seconds since 1970)</option>
|
||||||
|
<option>Office (days since 1900)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>`;
|
||||||
|
domTable.innerHTML = html;
|
||||||
|
Util.hideModal();
|
||||||
|
var buttons = domTable.querySelectorAll("button");
|
||||||
|
for (var i = 0; i < buttons.length; i++) {
|
||||||
|
buttons[i].addEventListener("click", event => {
|
||||||
|
var button = event.currentTarget;
|
||||||
|
var task = button.getAttribute("task");
|
||||||
|
var filename = button.getAttribute("filename");
|
||||||
|
var date = button.getAttribute("date") - 0;
|
||||||
|
if (!task || !filename || !date) return;
|
||||||
|
if (task === "view") readLog(date, logData => viewLog(logData, filename));
|
||||||
|
else if (task === "csv") readLog(date, logData => saveCSV(logData, date, date + 12096E5));
|
||||||
|
else if (task === "del") deleteFile(filename, () => viewFiles());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
viewFiles();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,199 +1,465 @@
|
||||||
|
// define accessable functions
|
||||||
exports = {
|
exports = {
|
||||||
// define en-/disable function, restarts the service to make changes take effect
|
// define en-/disable function, restarts the service to make changes take effect
|
||||||
setEnabled: function(enable, logfile, powersaving) {
|
setEnabled: function(enable) {
|
||||||
// check if sleeplog is available
|
|
||||||
if (typeof global.sleeplog !== "object") return;
|
|
||||||
|
|
||||||
// set default logfile
|
|
||||||
if ((typeof logfile !== "string" || !logfile.endsWith(".log")) &&
|
|
||||||
logfile !== false) logfile = "sleeplog.log";
|
|
||||||
|
|
||||||
// stop if enabled
|
// stop if enabled
|
||||||
if (global.sleeplog.enabled) global.sleeplog.stop();
|
if (global.sleeplog && sleeplog.enabled) sleeplog.stop();
|
||||||
|
|
||||||
// define storage and filename
|
// define settings filename
|
||||||
var storage = require("Storage");
|
var settings = "sleeplog.json";
|
||||||
var filename = "sleeplog.json";
|
|
||||||
|
|
||||||
// change enabled value in settings
|
// change enabled value in settings
|
||||||
storage.writeJSON(filename, Object.assign(storage.readJSON(filename, true) || {}, {
|
require("Storage").writeJSON(settings, Object.assign(
|
||||||
enabled: enable,
|
require("Storage").readJSON(settings, true) || {}, {
|
||||||
logfile: logfile,
|
enabled: enable
|
||||||
powersaving: powersaving || false
|
}
|
||||||
}));
|
));
|
||||||
|
|
||||||
// force changes to take effect by executing the boot script
|
// force changes to take effect by executing the boot script
|
||||||
eval(storage.read("sleeplog.boot.js"));
|
eval(require("Storage").read("sleeplog.boot.js"));
|
||||||
|
|
||||||
// clear variables
|
|
||||||
storage = undefined;
|
|
||||||
filename = undefined;
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
// define read log function
|
// define read log function, returns log array
|
||||||
// sorting: latest first, format:
|
// sorting: ascending (latest first), format:
|
||||||
// [[number, int, float, string], [...], ... ]
|
// [[number, int, int], [...], ... ]
|
||||||
// - number // timestamp in ms
|
// - number // timestamp in 10min
|
||||||
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = sleeping
|
// - int // status: 0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep
|
||||||
// - float // internal temperature
|
// - int // consecutive: 0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep
|
||||||
// - string // additional information
|
readLog: function(since, until) {
|
||||||
readLog: function(logfile, since, until) {
|
// set now and check if now is before since
|
||||||
// check/set logfile
|
var now = Date.now();
|
||||||
if (typeof logfile !== "string" || !logfile.endsWith(".log")) {
|
if (now < since) return [];
|
||||||
logfile = (global.sleeplog || {}).logfile || "sleeplog.log";
|
|
||||||
|
// set defaults and convert since, until and now to 10min steps
|
||||||
|
since = Math.floor((since || 0) / 6E5);
|
||||||
|
until = Math.ceil((until || now) / 6E5);
|
||||||
|
now = Math.ceil(now / 6E5);
|
||||||
|
|
||||||
|
// define output log
|
||||||
|
var log = [];
|
||||||
|
|
||||||
|
// open StorageFile
|
||||||
|
var file = require("Storage").open("sleeplog.log", "r");
|
||||||
|
// cache StorageFile size
|
||||||
|
var storageFileSize = file.getLength();
|
||||||
|
// check if a Storage File needs to be read
|
||||||
|
if (storageFileSize) {
|
||||||
|
// define previous line cache
|
||||||
|
var prevLine;
|
||||||
|
// loop through StorageFile entries
|
||||||
|
while (true) {
|
||||||
|
// cache new line
|
||||||
|
var line = file.readLine();
|
||||||
|
// exit loop if all lines are read
|
||||||
|
if (!line) break;
|
||||||
|
// skip lines starting with ","
|
||||||
|
if (line.startsWith(",")) continue;
|
||||||
|
// parse line
|
||||||
|
line = line.trim().split(",").map(e => parseInt(e));
|
||||||
|
// exit loop if new line timestamp is not before until
|
||||||
|
if (line[0] >= until) break;
|
||||||
|
// check if new line timestamp is 24h before since or not after since
|
||||||
|
if (line[0] + 144 < since) {
|
||||||
|
// skip roughly the next 10 lines
|
||||||
|
file.read(118);
|
||||||
|
file.readLine();
|
||||||
|
} else if (line[0] <= since) {
|
||||||
|
// cache line for next cycle
|
||||||
|
prevLine = line;
|
||||||
|
} else {
|
||||||
|
// add previous line if it was cached
|
||||||
|
if (prevLine) log.push(prevLine);
|
||||||
|
// add new line at the end of log
|
||||||
|
log.push(line);
|
||||||
|
// clear previous line cache
|
||||||
|
prevLine = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add previous line if it was cached
|
||||||
|
if (prevLine) log.push(prevLine);
|
||||||
|
// set unknown consecutive statuses
|
||||||
|
log = log.reverse().map((entry, index) => {
|
||||||
|
if (entry[2] === 0) entry[2] = (log[index - 1] || [])[2] || 0;
|
||||||
|
return entry;
|
||||||
|
}).reverse();
|
||||||
|
// remove duplicates
|
||||||
|
log = log.filter((entry, index) =>
|
||||||
|
!(index > 0 && entry[1] === log[index - 1][1] && entry[2] === log[index - 1][2])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if since is in the future
|
// check if log empty or first entry is after since
|
||||||
if (since > Date()) return [];
|
if (!log[0] || log[0][0] > since) {
|
||||||
|
// look for all needed storage files
|
||||||
|
var files = require("Storage").list(/^sleeplog_\d\d\d\d\.log$/, {
|
||||||
|
sf: false
|
||||||
|
});
|
||||||
|
|
||||||
// read logfile
|
// check if any file available
|
||||||
var log = require("Storage").read(logfile);
|
if (files.length) {
|
||||||
// return empty log
|
// generate start and end times in 10min steps
|
||||||
if (!log) return [];
|
files = files.map(file => {
|
||||||
// decode data if needed
|
var start = this.fnToMs(parseInt(file.substr(9, 4))) / 6E5;
|
||||||
if (log[0] !== "[") log = atob(log);
|
return {
|
||||||
// do a simple check before parsing
|
name: file,
|
||||||
if (!log.startsWith("[[") || !log.endsWith("]]")) return [];
|
start: start,
|
||||||
log = JSON.parse(log) || [];
|
end: start + 2016
|
||||||
|
};
|
||||||
|
}).sort((a, b) => b.start - a.start);
|
||||||
|
|
||||||
// check if filtering is needed
|
// read all neccessary files
|
||||||
if (since || until) {
|
var filesLog = [];
|
||||||
// search for latest entry befor since
|
files.some(file => {
|
||||||
if (since) since = (log.find(element => element[0] <= since) || [0])[0];
|
// exit loop if since after end
|
||||||
// filter selected time period
|
if (since >= file.end) return true;
|
||||||
log = log.filter(element => (element[0] >= since) && (element[0] <= (until || 1E14)));
|
// read file if until after start and since before end
|
||||||
|
if (until > file.start || since < file.end) {
|
||||||
|
var thisLog = require("Storage").readJSON(file.name, 1) || [];
|
||||||
|
if (thisLog.length) filesLog = thisLog.concat(filesLog);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// free ram
|
||||||
|
files = undefined;
|
||||||
|
|
||||||
|
// check if log from files is available
|
||||||
|
if (filesLog.length) {
|
||||||
|
// remove unwanted entries
|
||||||
|
filesLog = filesLog.filter((entry, index, filesLog) => (
|
||||||
|
(filesLog[index + 1] || [now])[0] >= since && entry[0] <= until
|
||||||
|
));
|
||||||
|
// add to log as previous entries
|
||||||
|
log = filesLog.concat(log);
|
||||||
|
}
|
||||||
|
// free ram
|
||||||
|
filesLog = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// output log
|
// define last index
|
||||||
|
var lastIndex = log.length - 1;
|
||||||
|
// set timestamp of first entry to since if first entry before since
|
||||||
|
if (log[0] && log[0][0] < since) log[0][0] = since;
|
||||||
|
// add timestamp at now with unknown status if until after now
|
||||||
|
if (until > now) log.push([now, 0, 0]);
|
||||||
|
|
||||||
return log;
|
return log;
|
||||||
},
|
},
|
||||||
|
|
||||||
// define write log function, append or replace log depending on input
|
// define move log function, move StorageFile content into files seperated by fortnights
|
||||||
// append input if array length >1 and element[0] >9E11
|
moveLog: function(force) {
|
||||||
// replace log with input if at least one entry like above is inside another array
|
/** convert old logfile (< v0.10) if present **/
|
||||||
writeLog: function(logfile, input) {
|
if (require("Storage").list("sleeplog.log", {
|
||||||
// check/set logfile
|
sf: false
|
||||||
if (typeof logfile !== "string" || !logfile.endsWith(".log")) {
|
}).length) {
|
||||||
if (!global.sleeplog || sleeplog.logfile === false) return;
|
convertOldLog();
|
||||||
logfile = sleeplog.logfile || "sleeplog.log";
|
}
|
||||||
|
/** may be removed in later versions **/
|
||||||
|
|
||||||
|
// first day of this fortnight period
|
||||||
|
var thisFirstDay = this.fnToMs(this.msToFn(Date.now()));
|
||||||
|
|
||||||
|
// read timestamp of the first StorageFile entry
|
||||||
|
var firstDay = (require("Storage").open("sleeplog.log", "r").read(47) || "").match(/\n\d*/);
|
||||||
|
// calculate the first day of the fortnight period
|
||||||
|
if (firstDay) firstDay = this.fnToMs(this.msToFn(parseInt(firstDay[0].trim()) * 6E5));
|
||||||
|
|
||||||
|
// check if moving is neccessary or forced
|
||||||
|
if (force || firstDay && firstDay < thisFirstDay) {
|
||||||
|
// read log for each fortnight period
|
||||||
|
while (firstDay) {
|
||||||
|
// calculate last day
|
||||||
|
var lastDay = firstDay + 12096E5;
|
||||||
|
// read log of the fortnight period
|
||||||
|
var log = require("sleeplog").readLog(firstDay, lastDay);
|
||||||
|
|
||||||
|
// check if before this fortnight period
|
||||||
|
if (firstDay < thisFirstDay) {
|
||||||
|
// write log in seperate file
|
||||||
|
require("Storage").writeJSON("sleeplog_" + this.msToFn(firstDay) + ".log", log);
|
||||||
|
// set last day as first
|
||||||
|
firstDay = lastDay;
|
||||||
|
} else {
|
||||||
|
// rewrite StorageFile
|
||||||
|
require("Storage").open("sleeplog.log", "w").write(log.map(e => e.join(",")).join("\n"));
|
||||||
|
// clear first day to exit loop
|
||||||
|
firstDay = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if input is an array
|
// free ram
|
||||||
if (typeof input !== "object" || typeof input.length !== "number") return;
|
log = undefined;
|
||||||
|
|
||||||
// check for entry plausibility
|
|
||||||
if (input.length > 1 && input[0] * 1 > 9E11) {
|
|
||||||
// read log
|
|
||||||
var log = this.readLog(logfile);
|
|
||||||
|
|
||||||
// remove last state if it was unknown and less then 5min ago
|
|
||||||
if (log.length > 0 && log[0][1] === 0 &&
|
|
||||||
Math.floor(Date.now()) - log[0][0] < 3E5) log.shift();
|
|
||||||
|
|
||||||
// add entry at the first position if it has changed
|
|
||||||
if (log.length === 0 || input.some((e, index) => index > 0 && input[index] !== log[0][index])) log.unshift(input);
|
|
||||||
|
|
||||||
// map log as input
|
|
||||||
input = log;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check and if neccessary reduce logsize to prevent low mem
|
|
||||||
if (input.length > 750) input = input.slice(-750);
|
|
||||||
|
|
||||||
// simple check for log plausibility
|
|
||||||
if (input[0].length > 1 && input[0][0] * 1 > 9E11) {
|
|
||||||
// write log to storage
|
|
||||||
require("Storage").write(logfile, btoa(JSON.stringify(input)));
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// define log to humanreadable string function
|
// define function to return stats from the last date [ms] for a specific duration [ms] or for the complete log
|
||||||
// sorting: latest last, format:
|
getStats: function(until, duration, log) {
|
||||||
// "{substring of ISO date} - {status} for {duration}min\n..."
|
// define stats variable
|
||||||
getReadableLog: function(printLog, since, until, logfile) {
|
var stats = {
|
||||||
// read log and check
|
calculatedAt: // [date] timestamp of the calculation
|
||||||
var log = this.readLog(logfile, since, until);
|
Math.round(Date.now()),
|
||||||
if (!log.length) return;
|
deepSleep: 0, // [min] deep sleep duration
|
||||||
// reverse array to set last timestamp to the end
|
lightSleep: 0, // [min] light sleep duration
|
||||||
log.reverse();
|
awakeSleep: 0, // [min] awake duration inside consecutive sleep
|
||||||
|
consecSleep: 0, // [min] consecutive sleep duration
|
||||||
// define status description and log string
|
awakeTime: 0, // [min] awake duration outside consecutive sleep
|
||||||
var statusText = ["unknown ", "not worn", "awake ", "sleeping"];
|
notWornTime: 0, // [min] duration of not worn status
|
||||||
var logString = [];
|
unknownTime: 0, // [min] duration of unknown status
|
||||||
|
logDuration: 0, // [min] duration of all entries taken into account
|
||||||
// rewrite each entry
|
firstDate: undefined, // [date] first entry taken into account
|
||||||
log.forEach((element, index) => {
|
lastDate: undefined // [date] last entry taken into account
|
||||||
logString[index] = "" +
|
};
|
||||||
Date(element[0] - Date().getTimezoneOffset() * 6E4).toISOString().substr(0, 19).replace("T", " ") + " - " +
|
|
||||||
statusText[element[1]] +
|
// set default inputs
|
||||||
(index === log.length - 1 ?
|
until = until || stats.calculatedAt;
|
||||||
element.length < 3 ? "" : " ".repeat(12) :
|
if (!duration) duration = 864E5;
|
||||||
" for " + ("" + Math.round((log[index + 1][0] - element[0]) / 60000)).padStart(4) + "min"
|
|
||||||
) +
|
// read log for the specified duration or complete log if not handed over
|
||||||
(element[2] ? " | Temp: " + ("" + element[2]).padEnd(5) + "°C" : "") +
|
if (!log) log = this.readLog(duration ? until - duration : 0, until);
|
||||||
(element[3] ? " | " + element[3] : "");
|
|
||||||
});
|
// check if log not empty or corrupted
|
||||||
logString = logString.join("\n");
|
if (log && log.length && log[0] && log[0].length === 3) {
|
||||||
|
// calculate and set first log date from 10min steps
|
||||||
// if set print and return string
|
stats.firstDate = log[0][0] * 6E5;
|
||||||
if (printLog) {
|
stats.lastDate = log[log.length - 1][0] * 6E5;
|
||||||
print(logString);
|
|
||||||
print("- first", Date(log[0][0]));
|
// cycle through log to calculate sums til end or duration is exceeded
|
||||||
print("- last", Date(log[log.length - 1][0]));
|
log.forEach((entry, index, log) => {
|
||||||
var period = log[log.length - 1][0] - log[0][0];
|
// calculate duration of this entry from 10min steps to minutes
|
||||||
print("- period= " + Math.floor(period / 864E5) + "d " + Math.floor(period % 864E5 / 36E5) + "h " + Math.floor(period % 36E5 / 6E4) + "min");
|
var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10;
|
||||||
}
|
|
||||||
return logString;
|
// check if duration greater 0
|
||||||
},
|
if (duration) {
|
||||||
|
// calculate sums
|
||||||
// define function to eliminate some errors inside the log
|
if (entry[1] === 4) stats.deepSleep += duration;
|
||||||
restoreLog: function(logfile) {
|
else if (entry[1] === 3) stats.lightSleep += duration;
|
||||||
// read log and check
|
else if (entry[1] === 2) {
|
||||||
var log = this.readLog(logfile);
|
if (entry[2] === 2) stats.awakeSleep += duration;
|
||||||
if (!log.length) return;
|
else if (entry[2] === 1) stats.awakeTime += duration;
|
||||||
|
}
|
||||||
// define output variable to show number of changes
|
if (entry[2] === 2) stats.consecSleep += duration;
|
||||||
var output = log.length;
|
if (entry[1] === 1) stats.notWornTime += duration;
|
||||||
|
if (entry[1] === 0) stats.unknownTime += duration;
|
||||||
// remove non decremental entries
|
stats.logDuration += duration;
|
||||||
log = log.filter((element, index) => log[index][0] >= (log[index + 1] || [0])[0]);
|
}
|
||||||
|
});
|
||||||
// write log
|
}
|
||||||
this.writeLog(logfile, log);
|
|
||||||
|
// free ram
|
||||||
// return difference in length
|
log = undefined;
|
||||||
return output - log.length;
|
|
||||||
},
|
// return stats of the last day
|
||||||
|
return stats;
|
||||||
// define function to reinterpret worn status based on given temperature threshold
|
},
|
||||||
reinterpretTemp: function(logfile, tempthresh) {
|
|
||||||
// read log and check
|
// define function to return last break time of day from date or now (default: 12 o'clock)
|
||||||
var log = this.readLog(logfile);
|
getLastBreak: function(date, ToD) {
|
||||||
if (!log.length) return;
|
// set default date or correct date type if needed
|
||||||
|
if (!date || !date.getDay) date = date ? new Date(date) : new Date();
|
||||||
// set default tempthresh
|
// set default ToD as set in sleeplog.conf or settings if available
|
||||||
tempthresh = tempthresh || (global.sleeplog ? sleeplog.tempthresh : 27);
|
if (ToD === undefined) ToD = (global.sleeplog && sleeplog.conf ? sleeplog.conf.breakToD :
|
||||||
|
(require("Storage").readJSON("sleeplog.json", true) || {}).breakToD) || 12;
|
||||||
// define output variable to show number of changes
|
// calculate last break time and return
|
||||||
var output = 0;
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), ToD);
|
||||||
|
},
|
||||||
// remove non decremental entries
|
|
||||||
log = log.map(element => {
|
// define functions to convert ms to the number of fortnights since the first Sunday at noon: 1970-01-04T12:00
|
||||||
if (element[2]) {
|
fnToMs: function(no) {
|
||||||
var tmp = element[1];
|
return (no + 0.25) * 12096E5;
|
||||||
element[1] = element[2] > tempthresh ? 3 : 1;
|
},
|
||||||
if (tmp !== element[1]) output++;
|
msToFn: function(ms) {
|
||||||
}
|
return (ms / 12096E5 - 0.25) | 0;
|
||||||
return element;
|
},
|
||||||
});
|
|
||||||
|
// define set debug function, options:
|
||||||
// write log
|
// enable as boolean, start/stop debugging
|
||||||
this.writeLog(logfile, log);
|
// duration in hours, generate csv log if set, max: 96h
|
||||||
|
setDebug: function(enable, duration) {
|
||||||
// return output
|
// check if global variable accessable
|
||||||
return output;
|
if (!global.sleeplog) return new Error("sleeplog: Can't set debugging, global object missing!");
|
||||||
}
|
|
||||||
|
// check if nothing has to be changed
|
||||||
|
if (!duration &&
|
||||||
|
(enable && sleeplog.debug === true) ||
|
||||||
|
(!enable && !sleeplog.debug)) return;
|
||||||
|
|
||||||
|
// check if en- or disable debugging
|
||||||
|
if (enable) {
|
||||||
|
// define debug object
|
||||||
|
sleeplog.debug = {};
|
||||||
|
|
||||||
|
// check if a file should be generated
|
||||||
|
if (typeof duration === "number") {
|
||||||
|
// check duration boundaries, 0 => 8
|
||||||
|
duration = duration > 96 ? 96 : duration || 12;
|
||||||
|
// calculate and set writeUntil in 10min steps
|
||||||
|
sleeplog.debug.writeUntil = ((Date.now() / 6E5 | 0) + duration * 6) * 6E5;
|
||||||
|
// set fileid to "{hours since 1970}"
|
||||||
|
sleeplog.debug.fileid = Date.now() / 36E5 | 0;
|
||||||
|
// write csv header on empty file
|
||||||
|
var file = require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a");
|
||||||
|
if (!file.getLength()) file.write(
|
||||||
|
"timestamp,movement,status,consecutive,asleepSince,awakeSince,bpm,bpmConfidence\n"
|
||||||
|
);
|
||||||
|
// free ram
|
||||||
|
file = undefined;
|
||||||
|
} else {
|
||||||
|
// set debug as active
|
||||||
|
sleeplog.debug = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// disable debugging
|
||||||
|
delete sleeplog.debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save status forced
|
||||||
|
sleeplog.saveStatus(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
// define debugging function, called after logging if debug is set
|
||||||
|
debug: function(data) {
|
||||||
|
// check if global variable accessable and debug active
|
||||||
|
if (!global.sleeplog || !sleeplog.debug) return;
|
||||||
|
|
||||||
|
// set functions to convert timestamps
|
||||||
|
function localTime(timestamp) {
|
||||||
|
return timestamp ? Date(timestamp).toString().split(" ")[4].substr(0, 5) : "- - -";
|
||||||
|
}
|
||||||
|
function officeTime(timestamp) {
|
||||||
|
// days since 30.12.1899
|
||||||
|
return timestamp / 864E5 + 25569;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate console output
|
||||||
|
var console = "sleeplog: " +
|
||||||
|
localTime(data.timestamp) + " > " +
|
||||||
|
"movement: " + ("" + data.movement).padStart(4) + ", " +
|
||||||
|
"unknown ,non consec.,consecutive".split(",")[sleeplog.consecutive] + " " +
|
||||||
|
"unknown,not worn,awake,light sleep,deep sleep".split(",")[data.status].padEnd(12) + ", " +
|
||||||
|
"asleep since: " + localTime(sleeplog.info.asleepSince) + ", " +
|
||||||
|
"awake since: " + localTime(sleeplog.info.awakeSince);
|
||||||
|
// add bpm if set
|
||||||
|
if (data.bpm) console += ", " +
|
||||||
|
"bpm: " + ("" + data.bpm).padStart(3) + ", " +
|
||||||
|
"confidence: " + data.bpmConfidence;
|
||||||
|
// output to console
|
||||||
|
print(console);
|
||||||
|
|
||||||
|
// check if debug is set as object with a file id and it is not past writeUntil
|
||||||
|
if (typeof sleeplog.debug === "object" && sleeplog.debug.fileid &&
|
||||||
|
Date.now() < sleeplog.debug.writeUntil) {
|
||||||
|
// generate next csv line
|
||||||
|
var csv = [
|
||||||
|
officeTime(data.timestamp),
|
||||||
|
data.movement,
|
||||||
|
data.status,
|
||||||
|
sleeplog.consecutive,
|
||||||
|
sleeplog.info.asleepSince ? officeTime(sleeplog.info.asleepSince) : "",
|
||||||
|
sleeplog.info.awakeSince ? officeTime(sleeplog.info.awakeSince) : "",
|
||||||
|
data.bpm || "",
|
||||||
|
data.bpmConfidence || ""
|
||||||
|
].join(",");
|
||||||
|
// write next line to log if set
|
||||||
|
require("Storage").open("sleeplog_" + sleeplog.debug.fileid + ".csv", "a").write(csv + "\n");
|
||||||
|
} else {
|
||||||
|
// clear file setting in debug
|
||||||
|
sleeplog.debug = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// print log as humanreadable output similar to debug output
|
||||||
|
printLog: function(since, until) {
|
||||||
|
// set default until
|
||||||
|
until = until || Date.now();
|
||||||
|
// print each entry inside log
|
||||||
|
this.readLog(since, until).forEach((entry, index, log) => {
|
||||||
|
// calculate duration of this entry from 10min steps to minutes
|
||||||
|
var duration = ((log[index + 1] || [until / 6E5 | 0])[0] - entry[0]) * 10;
|
||||||
|
// print this entry
|
||||||
|
print((index + ")").padStart(4) + " " +
|
||||||
|
Date(entry[0] * 6E5).toString().substr(0, 21) + " > " +
|
||||||
|
"unknown ,non consec.,consecutive".split(",")[entry[2]] + " " +
|
||||||
|
"unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) +
|
||||||
|
"for" + (duration + "min").padStart(8));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/** convert old (< v0.10) to new logfile data **/
|
||||||
|
convertOldLog: function() {
|
||||||
|
// read old logfile
|
||||||
|
var oldLog = require("Storage").read("sleeplog.log") || "";
|
||||||
|
// decode data if needed
|
||||||
|
if (!oldLog.startsWith("[")) oldLog = atob(oldLog);
|
||||||
|
// delete old logfile and return if it is empty or corrupted
|
||||||
|
if (!oldLog.startsWith("[[") || !oldLog.endsWith("]]")) {
|
||||||
|
require("Storage").erase("sleeplog.log");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform into StorageFile and clear oldLog to have more free ram accessable
|
||||||
|
require("Storage").open("sleeplog_old.log", "w").write(JSON.parse(oldLog).reverse().join("\n"));
|
||||||
|
oldLog = undefined;
|
||||||
|
|
||||||
|
// calculate fortnight from now
|
||||||
|
var fnOfNow = this.msToFn(Date.now());
|
||||||
|
|
||||||
|
// open StorageFile with old log data
|
||||||
|
var file = require("Storage").open("sleeplog_old.log", "r");
|
||||||
|
// define active fortnight and file cache
|
||||||
|
var activeFn = true;
|
||||||
|
var fileCache = [];
|
||||||
|
// loop through StorageFile entries
|
||||||
|
while (activeFn) {
|
||||||
|
// define fortnight for this entry
|
||||||
|
var thisFn = false;
|
||||||
|
// cache new line
|
||||||
|
var line = file.readLine();
|
||||||
|
// check if line is filled
|
||||||
|
if (line) {
|
||||||
|
// parse line
|
||||||
|
line = line.substr(0, 15).split(",").map(e => parseInt(e));
|
||||||
|
// calculate fortnight for this entry
|
||||||
|
thisFn = this.msToFn(line[0]);
|
||||||
|
// convert timestamp into 10min steps
|
||||||
|
line[0] = line[0] / 6E5 | 0;
|
||||||
|
// set consecutive to unknown
|
||||||
|
line.push(0);
|
||||||
|
}
|
||||||
|
// check if active fortnight and file cache is set, fortnight has changed and
|
||||||
|
// active fortnight is not fortnight from now
|
||||||
|
if (activeFn && fileCache.length && activeFn !== thisFn && activeFn !== fnOfNow) {
|
||||||
|
// write file cache into new file according to fortnight
|
||||||
|
require("Storage").writeJSON("sleeplog_" + activeFn + ".log", fileCache);
|
||||||
|
// clear file cache
|
||||||
|
fileCache = [];
|
||||||
|
}
|
||||||
|
// add line to file cache if it is filled
|
||||||
|
if (line) fileCache.push(line);
|
||||||
|
// set active fortnight
|
||||||
|
activeFn = thisFn;
|
||||||
|
}
|
||||||
|
// check if entries are leftover
|
||||||
|
if (fileCache.length) {
|
||||||
|
// format fileCache entries into a string
|
||||||
|
fileCache = fileCache.map(e => e.join(",")).join("\n");
|
||||||
|
// read complete new log StorageFile as string
|
||||||
|
file = require("Storage").open("sleeplog.log", "r");
|
||||||
|
var newLogString = file.read(file.getLength());
|
||||||
|
// add entries at the beginning of the new log string
|
||||||
|
newLogString = fileCache + "\n" + newLogString;
|
||||||
|
// rewrite new log StorageFile
|
||||||
|
require("Storage").open("sleeplog.log", "w").write(newLogString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// free ram
|
||||||
|
file = undefined;
|
||||||
|
fileCache = undefined;
|
||||||
|
|
||||||
|
// clean up old files
|
||||||
|
require("Storage").erase("sleeplog.log");
|
||||||
|
require("Storage").open("sleeplog_old.log", "w").erase();
|
||||||
|
}
|
||||||
|
/** may be removed in later versions **/
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
"id":"sleeplog",
|
"id":"sleeplog",
|
||||||
"name":"Sleep Log",
|
"name":"Sleep Log",
|
||||||
"shortName": "SleepLog",
|
"shortName": "SleepLog",
|
||||||
"version": "0.06",
|
"version": "0.11",
|
||||||
"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.",
|
"description": "Log and view your sleeping habits. This app is using the built in movement calculation.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"type": "app",
|
"type": "app",
|
||||||
"tags": "tool,boot",
|
"tags": "tool,boot",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
|
"interface": "interface.html",
|
||||||
"storage": [
|
"storage": [
|
||||||
{"name": "sleeplog.app.js", "url": "app.js"},
|
{"name": "sleeplog.app.js", "url": "app.js"},
|
||||||
{"name": "sleeplog.img", "url": "app-icon.js", "evaluate": true},
|
{"name": "sleeplog.img", "url": "app-icon.js", "evaluate": true},
|
||||||
|
@ -17,12 +18,16 @@
|
||||||
{"name": "sleeplog.settings.js", "url": "settings.js"}
|
{"name": "sleeplog.settings.js", "url": "settings.js"}
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
{"name": "sleeplog.json"},
|
{"name": "sleeplog.json"}
|
||||||
{"name": "sleeplog.log"}
|
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
{"url": "screenshot1.png"},
|
{"url": "screenshot-1_app_light.png"},
|
||||||
{"url": "screenshot2.png"},
|
{"url": "screenshot-2_day_light.png"},
|
||||||
{"url": "screenshot3.png"}
|
{"url": "screenshot-3_graph_light.png"},
|
||||||
|
{"url": "screenshot-4_graph2_light.png"},
|
||||||
|
{"url": "screenshot-5_app_dark.png"},
|
||||||
|
{"url": "screenshot-6_day_dark.png"},
|
||||||
|
{"url": "screenshot-7_graph_dark.png"},
|
||||||
|
{"url": "screenshot-8_graph2_dark.png"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 650 B |
Before Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 26 KiB |
|
@ -1,144 +1,431 @@
|
||||||
(function(back) {
|
(function(back) {
|
||||||
|
// define settings filename
|
||||||
var filename = "sleeplog.json";
|
var filename = "sleeplog.json";
|
||||||
|
// define logging prompt display status
|
||||||
|
var thresholdsPrompt = true;
|
||||||
|
|
||||||
// set storage and load settings
|
// define default vaules
|
||||||
var storage = require("Storage");
|
var defaults = {
|
||||||
var settings = Object.assign({
|
// main settings
|
||||||
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
|
|
||||||
powersaving: false, // disables ESS and uses build in movement detection
|
|
||||||
maxmove: 100, // movement threshold on power saving mode
|
|
||||||
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
|
enabled: true, // en-/disable completely
|
||||||
logfile: "sleeplog.log", // logfile
|
// threshold settings
|
||||||
}, storage.readJSON(filename, true) || {});
|
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
|
||||||
|
// app settings
|
||||||
|
breakToD: 12, // [h] time of day when to start/end graphs
|
||||||
|
appTimeout: 0 // lock and backlight timeouts for the app
|
||||||
|
};
|
||||||
|
|
||||||
// write change to global.sleeplog and storage
|
// assign loaded settings to default values
|
||||||
function writeSetting(key, value) {
|
var settings = Object.assign(defaults, require("Storage").readJSON(filename, true) || {});
|
||||||
// change key in global.sleeplog
|
|
||||||
if (typeof global.sleeplog === "object") global.sleeplog[key] = value;
|
// write change to storage
|
||||||
// reread settings to only change key
|
function writeSetting() {
|
||||||
settings = Object.assign(settings, storage.readJSON(filename, true) || {});
|
require("Storage").writeJSON(filename, settings);
|
||||||
// change the value of key
|
|
||||||
settings[key] = value;
|
|
||||||
// write to storage
|
|
||||||
storage.writeJSON(filename, settings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// define function to change values that need a restart of the service
|
// plot a debug file
|
||||||
function changeRestart() {
|
function plotDebug(filename) {
|
||||||
require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving);
|
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate sleepthresh factor
|
// read required entries
|
||||||
var stFactor = settings.winwidth / 12.5 / 60;
|
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 ? sleeplog.conf.deepTh : defaults.deepTh;
|
||||||
|
var lightTh = global.sleeplog ? 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 = !!sleeplog.debug;
|
||||||
|
var file = typeof sleeplog.debug === "object";
|
||||||
|
var duration = 0;
|
||||||
|
// setup debugging menu
|
||||||
|
var debugMenu = {
|
||||||
|
"": {
|
||||||
|
title: /*LANG*/"Debugging"
|
||||||
|
},
|
||||||
|
/*LANG*/"< Back": () => {
|
||||||
|
// check if some value has changed
|
||||||
|
if (enabled !== !!sleeplog.debug || file !== (typeof 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 ? (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"
|
||||||
|
},
|
||||||
|
/*LANG*/"< 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*/"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
|
// show main menu
|
||||||
function showMain(selected) {
|
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 sleeplog.debug === "object" ?
|
||||||
|
"FBSBAB/4AQDAF+4BfvAX74F+CBf+gX/oFJKBf+gUkoF/6BSSgX/oFJ6Bf+gX/oF/6BAAgf/4" : // file
|
||||||
|
sleeplog.debug ?
|
||||||
|
"FBSBAP//+f/V///4AAGAABkAAZgAGcABjgAYcAGDgBhwAY4AGcABmH+ZB/mAABgAAYAAH///" : // console
|
||||||
|
0; // off
|
||||||
|
debugImg = debugImg ? "\0" + atob(debugImg) : false;
|
||||||
|
// set menu
|
||||||
var mainMenu = {
|
var mainMenu = {
|
||||||
"": {
|
"": {
|
||||||
title: "Sleep Log",
|
title: "Sleep Log",
|
||||||
selected: selected
|
selected: selected
|
||||||
},
|
},
|
||||||
"Exit": () => load(),
|
/*LANG*/"< Back": () => back(),
|
||||||
"< Back": () => back(),
|
/*LANG*/"Thresholds": () => showThresholds(),
|
||||||
"Break Tod": {
|
/*LANG*/"Break ToD": {
|
||||||
value: settings.breaktod,
|
value: settings.breakToD,
|
||||||
step: 1,
|
step: 1,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 23,
|
max: 23,
|
||||||
wrap: true,
|
wrap: true,
|
||||||
onchange: v => writeSetting("breaktod", v),
|
noList: true,
|
||||||
},
|
format: v => v + ":00",
|
||||||
"Max Awake": {
|
onchange: v => {
|
||||||
value: settings.maxawake / 6E4,
|
settings.breakToD = v;
|
||||||
step: 5,
|
writeSetting();
|
||||||
min: 15,
|
|
||||||
max: 120,
|
|
||||||
wrap: true,
|
|
||||||
format: v => v + "min",
|
|
||||||
onchange: v => writeSetting("maxawake", v * 6E4),
|
|
||||||
},
|
|
||||||
"Min Consec": {
|
|
||||||
value: settings.minconsec / 6E4,
|
|
||||||
step: 5,
|
|
||||||
min: 15,
|
|
||||||
max: 120,
|
|
||||||
wrap: true,
|
|
||||||
format: v => v + "min",
|
|
||||||
onchange: v => writeSetting("minconsec", v * 6E4),
|
|
||||||
},
|
|
||||||
"Temp Thresh": {
|
|
||||||
value: settings.tempthresh,
|
|
||||||
step: 0.5,
|
|
||||||
min: 20,
|
|
||||||
max: 40,
|
|
||||||
wrap: true,
|
|
||||||
format: v => v + "°C",
|
|
||||||
onchange: v => writeSetting("tempthresh", v),
|
|
||||||
},
|
|
||||||
"Power Saving": {
|
|
||||||
value: settings.powersaving,
|
|
||||||
format: v => v ? "on" : "off",
|
|
||||||
onchange: function(v) {
|
|
||||||
settings.powersaving = v;
|
|
||||||
changeRestart();
|
|
||||||
// redraw menu with changed entries subsequent to onchange
|
|
||||||
// https://github.com/espruino/Espruino/issues/2149
|
|
||||||
setTimeout(showMain, 1, 6);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Max Move": {
|
/*LANG*/"App Timeout": {
|
||||||
value: settings.maxmove,
|
value: settings.appTimeout / 1E3,
|
||||||
step: 1,
|
step: 10,
|
||||||
min: 50,
|
min: 0,
|
||||||
max: 200,
|
max: 120,
|
||||||
wrap: true,
|
wrap: true,
|
||||||
onchange: v => writeSetting("maxmove", v),
|
noList: true,
|
||||||
|
format: v => v ? v + "s" : "-",
|
||||||
|
onchange: v => {
|
||||||
|
settings.appTimeout = v * 1E3;
|
||||||
|
writeSetting();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"NoMo Thresh": {
|
/*LANG*/"Enabled": {
|
||||||
value: settings.nomothresh,
|
|
||||||
step: 0.001,
|
|
||||||
min: 0.006,
|
|
||||||
max: 0.02,
|
|
||||||
wrap: true,
|
|
||||||
format: v => ("" + v).padEnd(5, "0"),
|
|
||||||
onchange: v => writeSetting("nomothresh", v),
|
|
||||||
},
|
|
||||||
"Min Duration": {
|
|
||||||
value: Math.floor(settings.sleepthresh * stFactor),
|
|
||||||
step: 1,
|
|
||||||
min: 5,
|
|
||||||
max: 15,
|
|
||||||
wrap: true,
|
|
||||||
format: v => v + "min",
|
|
||||||
onchange: v => writeSetting("sleepthresh", Math.ceil(v / stFactor)),
|
|
||||||
},
|
|
||||||
"Enabled": {
|
|
||||||
value: settings.enabled,
|
value: settings.enabled,
|
||||||
format: v => v ? "on" : "off",
|
onchange: v => {
|
||||||
onchange: function(v) {
|
|
||||||
settings.enabled = v;
|
settings.enabled = v;
|
||||||
changeRestart();
|
require("sleeplog").setEnabled(v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Logfile ": {
|
/*LANG*/"Debugging": {
|
||||||
value: settings.logfile === "sleeplog.log" ? true : (settings.logfile || "").endsWith(".log") ? "custom" : false,
|
value: debugImg,
|
||||||
format: v => v === true ? "default" : v ? "custom" : "off",
|
onchange: () => setTimeout(showDebug, 10)
|
||||||
onchange: function(v) {
|
|
||||||
if (v !== "custom") {
|
|
||||||
settings.logfile = v ? "sleeplog.log" : false;
|
|
||||||
changeRestart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// check power saving mode to delete unused entries
|
|
||||||
(settings.powersaving ? ["NoMo Thresh", "Min Duration"] : ["Max Move"]).forEach(property => delete mainMenu[property]);
|
|
||||||
var menu = E.showMenu(mainMenu);
|
var menu = E.showMenu(mainMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
Update every 10s, average last 5 readings
|
Update every 10s, average last 5 readings
|
||||||
Changes based on #1092
|
Changes based on #1092
|
||||||
0.06: Minor tweaks for stability. Update every 5 seconds
|
0.06: Minor tweaks for stability. Update every 5 seconds
|
||||||
|
0.07: Add back button
|
||||||
|
|
|
@ -24,7 +24,7 @@ function onTemperature(p) {
|
||||||
// Gets the temperature in the most accurate way (pressure sensor or inbuilt thermistor)
|
// Gets the temperature in the most accurate way (pressure sensor or inbuilt thermistor)
|
||||||
function drawTemperature() {
|
function drawTemperature() {
|
||||||
if (Bangle.getPressure) {
|
if (Bangle.getPressure) {
|
||||||
Bangle.getPressure().then(p =>{if (p) onTemperature(p)});
|
Bangle.getPressure().then(p =>{if (p) onTemperature(p);});
|
||||||
} else {
|
} else {
|
||||||
onTemperature({
|
onTemperature({
|
||||||
temperature : E.getTemperature()
|
temperature : E.getTemperature()
|
||||||
|
@ -36,6 +36,9 @@ setInterval(function() {
|
||||||
drawTemperature();
|
drawTemperature();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.setUI({
|
||||||
|
mode : "custom",
|
||||||
|
back : function() {load();}
|
||||||
|
});
|
||||||
E.showMessage("Reading temperature...");
|
E.showMessage("Reading temperature...");
|
||||||
drawTemperature();
|
drawTemperature();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "thermom",
|
"id": "thermom",
|
||||||
"name": "Thermometer",
|
"name": "Thermometer",
|
||||||
"version": "0.06",
|
"version": "0.07",
|
||||||
"description": "Displays the current temperature in degree Celsius/Fahrenheit (depending on locale), updates every 10 seconds with average of last 5 readings.",
|
"description": "Displays the current temperature in degree Celsius/Fahrenheit (depending on locale), updates every 10 seconds with average of last 5 readings.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool",
|
"tags": "tool",
|
||||||
|
|
|
@ -6,3 +6,4 @@
|
||||||
0.06: Periodically update so the always on display does show current GPS fix
|
0.06: Periodically update so the always on display does show current GPS fix
|
||||||
0.07: Alternative marker icon (configurable via settings)
|
0.07: Alternative marker icon (configurable via settings)
|
||||||
0.08: Add ability to hide the icon when GPS is off, for a cleaner appearance.
|
0.08: Add ability to hide the icon when GPS is off, for a cleaner appearance.
|
||||||
|
0.09: Do not take widget space if icon is hidden
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "widgps",
|
"id": "widgps",
|
||||||
"name": "GPS Widget",
|
"name": "GPS Widget",
|
||||||
"version": "0.08",
|
"version": "0.09",
|
||||||
"description": "Tiny widget to show the power and fix status of the GPS/GNSS",
|
"description": "Tiny widget to show the power and fix status of the GPS/GNSS",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
"type": "widget",
|
"type": "widget",
|
||||||
|
|
|
@ -11,13 +11,14 @@ var interval;
|
||||||
var oldSetGPSPower = Bangle.setGPSPower;
|
var oldSetGPSPower = Bangle.setGPSPower;
|
||||||
Bangle.setGPSPower = function(on, id) {
|
Bangle.setGPSPower = function(on, id) {
|
||||||
var isGPSon = oldSetGPSPower(on, id);
|
var isGPSon = oldSetGPSPower(on, id);
|
||||||
WIDGETS.gps.draw();
|
WIDGETS.gps.width = !isGPSon && settings.hideWhenGpsOff ? 0 : 24;
|
||||||
|
Bangle.drawWidgets();
|
||||||
return isGPSon;
|
return isGPSon;
|
||||||
};
|
};
|
||||||
|
|
||||||
WIDGETS.gps = {
|
WIDGETS.gps = {
|
||||||
area : "tr",
|
area : "tr",
|
||||||
width : 24,
|
width : !Bangle.isGPSOn() && settings.hideWhenGpsOff ? 0 : 24,
|
||||||
draw : function() {
|
draw : function() {
|
||||||
g.reset();
|
g.reset();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Initial Medical Alert Widget!
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Medical Alert Widget
|
||||||
|
|
||||||
|
Shows a medical alert logo in the top right widget area, and a medical alert message in the bottom widget area.
|
||||||
|
|
||||||
|
**Note:** this is not a replacement for a medical alert band but hopefully a useful addition.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Implemented:
|
||||||
|
|
||||||
|
- Basic medical alert logo and message
|
||||||
|
- Only display bottom widget on clocks
|
||||||
|
- High contrast colours depending on theme
|
||||||
|
|
||||||
|
Future:
|
||||||
|
|
||||||
|
- Configure when to show bottom widget (always/never/clocks)
|
||||||
|
- Configure medical alert text
|
||||||
|
- Show details when touched
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
James Taylor ([jt-nti](https://github.com/jt-nti))
|
|
@ -0,0 +1,15 @@
|
||||||
|
{ "id": "widmeda",
|
||||||
|
"name": "Medical Alert Widget",
|
||||||
|
"shortName":"Medical Alert",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Display a medical alert in the bottom widget section.",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "health,medical,tools,widget",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"screenshots": [{"url":"screenshot_light.png"}],
|
||||||
|
"storage": [
|
||||||
|
{"name":"widmeda.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,30 @@
|
||||||
|
(() => {
|
||||||
|
// Top right star of life logo
|
||||||
|
WIDGETS["widmedatr"]={
|
||||||
|
area: "tr",
|
||||||
|
width: 24,
|
||||||
|
draw: function() {
|
||||||
|
g.reset();
|
||||||
|
g.setColor("#f00");
|
||||||
|
g.drawImage(atob("FhYBAAAAA/AAD8AAPwAc/OD/P8P8/x/z/n+/+P5/wP58A/nwP5/x/v/n/P+P8/w/z/Bz84APwAA/AAD8AAAAAA=="), this.x + 1, this.y + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bottom medical alert message
|
||||||
|
WIDGETS["widmedabl"]={
|
||||||
|
area: "bl",
|
||||||
|
width: Bangle.CLOCK?Bangle.appRect.w:0,
|
||||||
|
draw: function() {
|
||||||
|
// Only show the widget on clocks
|
||||||
|
if (!Bangle.CLOCK) return;
|
||||||
|
|
||||||
|
g.reset();
|
||||||
|
g.setBgColor(g.theme.dark ? "#fff" : "#f00");
|
||||||
|
g.setColor(g.theme.dark ? "#f00" : "#fff");
|
||||||
|
g.setFont("Vector",18);
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.clearRect(this.x, this.y, this.x + this.width - 1, this.y + 23);
|
||||||
|
g.drawString("MEDICAL ALERT", this.width / 2, this.y + ( 23 / 2 ));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
0.01: New Widget!
|
||||||
|
0.02: Fix calling null on draw
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Twenties
|
||||||
|
|
||||||
|
Follow the [20-20-20 rule](https://www.aoa.org/AOA/Images/Patients/Eye%20Conditions/20-20-20-rule.pdf) with discrete reminders. Your BangleJS will buzz every 20 minutes for you to look away from your screen, and then buzz 20 seconds later to look back. Additionally, alternate between standing and sitting every 20 minutes to be standing for [more than 30 minutes](https://uwaterloo.ca/kinesiology-health-sciences/how-long-should-you-stand-rather-sit-your-work-station) per hour.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Download this widget and, as long as your watch-face supports widgets, it will automatically run in the background.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Vibrate to remind you to stand up and look away for healthy living.
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
[@splch](https://github.com/splch/)
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"id": "widtwenties",
|
||||||
|
"name": "Twenties",
|
||||||
|
"shortName": "twenties",
|
||||||
|
"version": "0.02",
|
||||||
|
"description": "Buzzes every 20m to stand / sit and look 20ft away for 20s.",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"type": "widget",
|
||||||
|
"tags": "widget,tools",
|
||||||
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [{ "name": "widtwenties.wid.js", "url": "widget.js" }]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// WIDGETS = {}; // <-- for development only
|
||||||
|
|
||||||
|
/* run widgets in their own function scope so
|
||||||
|
they don't interfere with currently-running apps */
|
||||||
|
(() => {
|
||||||
|
const move = 20 * 60 * 1000; // 20 minutes
|
||||||
|
const look = 20 * 1000; // 20 seconds
|
||||||
|
|
||||||
|
buzz = _ => {
|
||||||
|
Bangle.buzz().then(_ => {
|
||||||
|
setTimeout(Bangle.buzz, look);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// add widget
|
||||||
|
WIDGETS.twenties = {
|
||||||
|
buzz: buzz,
|
||||||
|
draw: _ => { return null; },
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(WIDGETS.twenties.buzz, move); // buzz to stand / sit
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Bangle.drawWidgets(); // <-- for development only
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/nodejs
|
#!/usr/bin/env nodejs
|
||||||
/* Simple Command-line app loader for Node.js
|
/* Simple Command-line app loader for Node.js
|
||||||
===============================================
|
===============================================
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/nodejs
|
#!/usr/bin/env nodejs
|
||||||
/* Quick hack to add proper 'supports' field to apps.json
|
/* Quick hack to add proper 'supports' field to apps.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# apps.json used to contain the metadata for every app. Now the
|
# apps.json used to contain the metadata for every app. Now the
|
||||||
# metadata is stored in each apps's directory - app/yourapp/metadata.js
|
# metadata is stored in each apps's directory - app/yourapp/metadata.js
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/nodejs
|
#!/usr/bin/env nodejs
|
||||||
/*
|
/*
|
||||||
Mashes together a bunch of different apps to make
|
Mashes together a bunch of different apps to make
|
||||||
a single firmware JS file which can be uploaded.
|
a single firmware JS file which can be uploaded.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/node
|
#!/usr/bin/env node
|
||||||
/*
|
/*
|
||||||
Mashes together a bunch of different apps into a big binary blob.
|
Mashes together a bunch of different apps into a big binary blob.
|
||||||
We then store this *inside* the Bangle.js firmware and can use it
|
We then store this *inside* the Bangle.js firmware and can use it
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cd `dirname $0`/..
|
cd `dirname $0`/..
|
||||||
nodejs bin/sanitycheck.js || exit 1
|
nodejs bin/sanitycheck.js || exit 1
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/node
|
#!/usr/bin/env node
|
||||||
/* Checks for any obvious problems in apps.json
|
/* Checks for any obvious problems in apps.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -30,7 +30,11 @@ function ERROR(msg, opt) {
|
||||||
function WARN(msg, opt) {
|
function WARN(msg, opt) {
|
||||||
// file=app.js,line=1,col=5,endColumn=7
|
// file=app.js,line=1,col=5,endColumn=7
|
||||||
opt = opt||{};
|
opt = opt||{};
|
||||||
|
if (KNOWN_WARNINGS.includes(msg)) {
|
||||||
|
console.log(`Known warning : ${msg}`);
|
||||||
|
} else {
|
||||||
console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`);
|
console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`);
|
||||||
|
}
|
||||||
warningCount++;
|
warningCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +90,10 @@ const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD
|
||||||
'waypoints' : ['waypoints'],
|
'waypoints' : ['waypoints'],
|
||||||
// notify?
|
// notify?
|
||||||
};
|
};
|
||||||
|
/* These are warnings we know about but don't want in our output */
|
||||||
|
var KNOWN_WARNINGS = [
|
||||||
|
"App gpsrec data file wildcard .gpsrc? does not include app ID"
|
||||||
|
];
|
||||||
|
|
||||||
function globToRegex(pattern) {
|
function globToRegex(pattern) {
|
||||||
const ESCAPE = '.*+-?^${}()|[]\\';
|
const ESCAPE = '.*+-?^${}()|[]\\';
|
||||||
|
@ -225,6 +233,13 @@ apps.forEach((app,appIdx) => {
|
||||||
console.log("=====================================================");
|
console.log("=====================================================");
|
||||||
ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`, {file:appDirRelative+file.url});
|
ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`, {file:appDirRelative+file.url});
|
||||||
}
|
}
|
||||||
|
// clock app checks
|
||||||
|
if (app.type=="clock") {
|
||||||
|
var a = fileContents.indexOf("Bangle.loadWidgets()");
|
||||||
|
var b = fileContents.indexOf("Bangle.setUI(");
|
||||||
|
if (a>=0 && b>=0 && a<b)
|
||||||
|
WARN(`Clock ${app.id} file calls loadWidgets before setUI (clock widget/etc won't be aware a clock app is running)`, {file:appDirRelative+file.url, line : fileContents.substr(0,a).split("\n").length});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const key in file) {
|
for (const key in file) {
|
||||||
if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`, {file:appDirRelative+file.url});
|
if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`, {file:appDirRelative+file.url});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/*
|
/*
|
||||||
var EMULATOR = "banglejs2";
|
var EMULATOR = "banglejs2";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cd `dirname $0`/..
|
cd `dirname $0`/..
|
||||||
ls tests/*.js | xargs -I{} bin/runtest.sh {}
|
ls tests/*.js | xargs -I{} bin/runtest.sh {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/bash
|
#!/usr/bin/env bash
|
||||||
# Requires Linux x64 (for ./espruino)
|
# Requires Linux x64 (for ./espruino)
|
||||||
# Also imagemagick for display
|
# Also imagemagick for display
|
||||||
|
|
||||||
|
|