1
0
Fork 0

Merge branch 'espruino:master' into master

master
eleanor 2022-09-06 15:29:01 -05:00 committed by GitHub
commit fb41861376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 3199 additions and 1142 deletions

3
.gitignore vendored
View File

@ -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*

View File

@ -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

View File

@ -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();
})();

View File

@ -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();
})(); })();

View File

@ -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

View File

@ -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;
}

View File

@ -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}
] ]
} }

View File

@ -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());
}) })

View File

@ -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

View File

@ -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);

View File

@ -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"}],

View File

@ -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"

View File

@ -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
``` ```

View File

@ -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");

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

2
apps/calclock/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Initial version
0.02: More compact rendering & app icon

9
apps/calclock/README.md Normal file
View File

@ -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
![](screenshot.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgpm5gAB4AVRhgWCAAQWWDCARC/4ACJR4uB54WDAAP8DBotFGIgXLFwv4GAouQC4gwMLooXF/gXJOowXGJBIXBCIgXQxgXLMAIXXMAmIC5OIx4XJhH/wAXIxnIC78IxGIHoIABI44MBC4wQBEQIDB5gXGPAJgEC6IxBC5oABC4wwDa4YTCxAWD5nPDAzvGFYgAB5AXWJBK+GcAq5CGBIuBC5X4GBIJBdoQXB/GIx4CDPJAuEC5JoCDAgWBFwYXJxCBIFwYXKYwoACCwZ3IPQoWIC5YABGYIABCwpHKAQYMBCwwX/C5QAMC8R3/R/4XNhAXNwAXHgGIABgWIAFwA=="))

119
apps/calclock/calclock.js Normal file
View File

@ -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");

BIN
apps/calclock/calclock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
apps/calclock/location.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

View File

@ -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"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

2
apps/chimer/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: Initial Creation
0.02: Fixed some sleep bugs. Added a sleep mode toggle

11
apps/chimer/README.MD Normal file
View File

@ -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.

2
apps/chimer/icon.txt Normal file
View File

@ -0,0 +1,2 @@
widget.png: "https://icons8.com/icon/114436/alarm"

16
apps/chimer/metadata.json Normal file
View File

@ -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" }]
}

94
apps/chimer/settings.js Normal file
View File

@ -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();
});

134
apps/chimer/widget.js Normal file
View File

@ -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();
})();

BIN
apps/chimer/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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 += `(&#9888; update required)`; version += `(&#9888; update required)`;
} }

View File

@ -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

View File

@ -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.
![](first_screen.jpg) ![](first_screen.jpg)
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.
![](waypoint_screen.jpg) ![](waypoint_screen.jpg)
@ -18,7 +21,7 @@ The app lets you mark your current location as follows. There are vacant slots i
![](select_screen.jpg) ![](select_screen.jpg)
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].
![](marked_screen.jpg) ![](marked_screen.jpg)

268
apps/gpsnav/app_b2.js Normal file
View File

@ -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();

View File

@ -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}
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -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

View File

@ -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();

View File

@ -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();
},
},
}); });
}) })

View File

@ -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.",

View File

@ -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

View File

@ -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);

View File

@ -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",

View File

@ -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

View File

@ -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;

View File

@ -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"}],

View File

@ -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

View File

@ -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: ![](powersaving.png) - __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: ![](disabled.png) - __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: ![](nolog.png) - __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)

View File

@ -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();

View File

@ -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;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

View File

@ -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>

View File

@ -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 **/
}; };

View File

@ -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"}
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
apps/sleeplog/off_20x20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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);
} }

View File

@ -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

View File

@ -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();

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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();

1
apps/widmeda/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Initial Medical Alert Widget!

23
apps/widmeda/README.md Normal file
View File

@ -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))

View File

@ -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"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

30
apps/widmeda/widget.js Normal file
View File

@ -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 ));
}
};
})();

BIN
apps/widmeda/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,2 @@
0.01: New Widget!
0.02: Fix calling null on draw

View File

@ -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/)

View File

@ -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" }]
}

View File

@ -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

BIN
apps/widtwenties/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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
=============================================== ===============================================

View File

@ -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
*/ */

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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});

View File

@ -1,4 +1,4 @@
#!/usr/bin/node #!/usr/bin/env node
/* /*
var EMULATOR = "banglejs2"; var EMULATOR = "banglejs2";

View File

@ -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 {}

View File

@ -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