Merge branch 'espruino:master' into master
|
|
@ -1,11 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Make overriding the HRM event optional
|
||||
Emit BTHRM event for external sensor
|
||||
Add recorder app plugin
|
||||
0.03: Prevent readings from internal sensor mixing into BT values
|
||||
Mark events with src property
|
||||
Show actual source of event in app
|
||||
0.04: Allow reading additional data if available: HRM battery and position
|
||||
Better caching of scanned BT device properties
|
||||
New setting for not starting the BTHRM together with HRM
|
||||
Save some RAM by not definining functions if disabled in settings
|
||||
0.02: Write available data on reset or kill
|
||||
|
|
|
|||
|
|
@ -11,34 +11,30 @@ var currentSlot = 0;
|
|||
var hrvSlots = [10,20,30,60,120,300];
|
||||
var hrvValues = {};
|
||||
var rrRmsProgress;
|
||||
var saved = false;
|
||||
|
||||
var rrNumberOfValues = 0;
|
||||
var rrSquared = 0;
|
||||
var rrLastValue
|
||||
var rrLastValue;
|
||||
var rrMax;
|
||||
var rrMin;
|
||||
|
||||
function calcHrv(rr){
|
||||
//Calculate HRV with RMSSD method: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5624990/
|
||||
for (currentRr of rr){
|
||||
for (var currentRr of rr){
|
||||
if (!rrMax) rrMax = currentRr;
|
||||
if (!rrMin) rrMin = currentRr;
|
||||
rrMax = Math.max(rrMax, currentRr);
|
||||
rrMin = Math.min(rrMin, currentRr);
|
||||
//print("Calc for: " + currentRr);
|
||||
rrNumberOfValues++;
|
||||
if (!rrLastValue){
|
||||
rrLastValue = currentRr;
|
||||
continue;
|
||||
}
|
||||
rrSquared += (rrLastValue - currentRr)*(rrLastValue - currentRr);
|
||||
|
||||
//print("rr²: " + rrSquared);
|
||||
|
||||
rrLastValue = currentRr;
|
||||
}
|
||||
var rms = Math.sqrt(rrSquared / rrNumberOfValues);
|
||||
//print("rms: " + rms);
|
||||
return rms;
|
||||
}
|
||||
|
||||
|
|
@ -56,17 +52,36 @@ function draw(y, hrv) {
|
|||
if (hrvValues[hrvSlots[i]]) str += hrvValues[hrvSlots[i]].toFixed(1) + "ms";
|
||||
g.setFontVector(16).drawString(str,px,y+44+(i*17));
|
||||
}
|
||||
|
||||
|
||||
g.setRotation(3);
|
||||
g.setFontVector(12).drawString("Reset",g.getHeight()/2, g.getWidth()-10);
|
||||
g.setRotation(0);
|
||||
}
|
||||
|
||||
function write(){
|
||||
if (!hrvValues[hrvSlots[0]]){
|
||||
return;
|
||||
}
|
||||
|
||||
var file = require('Storage').open("bthrv.csv", "a");
|
||||
var data = new Date(startingTime).toISOString();
|
||||
for (var i = 0; i < hrvSlots.length; i++ ){
|
||||
data += ",";
|
||||
if (hrvValues[hrvSlots[i]]){
|
||||
data += hrvValues[hrvSlots[i]];
|
||||
}
|
||||
}
|
||||
|
||||
data += "," + rrMax + "," + rrMin + ","+rrNumberOfValues;
|
||||
data += "\n";
|
||||
file.write(data);
|
||||
Bangle.buzz(500);
|
||||
}
|
||||
|
||||
function onBtHrm(e) {
|
||||
if (e.rr && !startingTime) Bangle.buzz(500);
|
||||
if (e.rr && !startingTime) startingTime=Date.now();
|
||||
//print("Event:" + e.rr);
|
||||
|
||||
|
||||
var hrv = calcHrv(e.rr);
|
||||
if (hrv){
|
||||
if (currentSlot <= hrvSlots.length && (Date.now() - startingTime) > (hrvSlots[currentSlot] * 1000) && !hrvValues[hrvSlots[currentSlot]]){
|
||||
|
|
@ -74,35 +89,25 @@ function onBtHrm(e) {
|
|||
currentSlot++;
|
||||
}
|
||||
}
|
||||
if (!saved && currentSlot == hrvSlots.length){
|
||||
var file = require('Storage').open("bthrv.csv", "a");
|
||||
var data = new Date(startingTime).toISOString();
|
||||
for (var c of hrvSlots){
|
||||
data+=","+hrvValues[c];
|
||||
}
|
||||
data+="," + rrMax + "," + rrMin + ","+rrNumberOfValues;
|
||||
data+="\n";
|
||||
file.write(data);
|
||||
saved = true;
|
||||
Bangle.buzz(500);
|
||||
}
|
||||
|
||||
if (hrv){
|
||||
if (!ui){
|
||||
if (!ui){
|
||||
Bangle.setUI("leftright", ()=>{
|
||||
resetHrv();
|
||||
clear(30);
|
||||
});
|
||||
ui = true;
|
||||
}
|
||||
|
||||
draw(30, hrv);
|
||||
}
|
||||
}
|
||||
|
||||
function resetHrv(){
|
||||
write();
|
||||
hrvValues={};
|
||||
startingTime=undefined;
|
||||
currentSlot=0;
|
||||
saved=false;
|
||||
rrNumberOfValues = 0;
|
||||
rrSquared = 0;
|
||||
rrLastValue = undefined;
|
||||
|
|
@ -117,7 +122,6 @@ g.clear();
|
|||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
|
||||
if (Bangle.setBTHRMPower){
|
||||
Bangle.on('BTHRM', onBtHrm);
|
||||
Bangle.setBTHRMPower(1,'bthrv');
|
||||
|
|
@ -133,6 +137,11 @@ if (Bangle.setBTHRMPower){
|
|||
file.write(data);
|
||||
}
|
||||
|
||||
E.on('kill', ()=>{
|
||||
write();
|
||||
Bangle.setBTHRMPower(0,'bthrv');
|
||||
});
|
||||
|
||||
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
|
||||
} else {
|
||||
|
|
@ -140,4 +149,3 @@ if (Bangle.setBTHRMPower){
|
|||
g.drawString("Missing BT HRM",g.getWidth()/2,g.getHeight()/2 - 16);
|
||||
}
|
||||
|
||||
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrv'));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrv",
|
||||
"name": "Bluetooth Heart Rate variance calculator",
|
||||
"shortName": "BT HRV",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Calculates HRV from a a BT HRM with interval data",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
|||
|
|
@ -19,3 +19,4 @@
|
|||
Colors of circles can be configured
|
||||
Color depending on value (green -> red, red -> green) option
|
||||
Good HRM value will not be overwritten so fast anymore
|
||||
0.10: Use roboto font for time, date and day of week and center align them
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"minHR": 40,
|
||||
"maxHR": 200,
|
||||
"confidence": 0,
|
||||
"stepGoal": 10000,
|
||||
"stepDistanceGoal": 8000,
|
||||
"stepLength": 0.8,
|
||||
"batteryWarn": 30,
|
||||
"showWidgets": false,
|
||||
"weatherCircleData": "humidity",
|
||||
"circleCount": 3,
|
||||
"circle1": "hr",
|
||||
"circle2": "steps",
|
||||
"circle3": "battery",
|
||||
"circle4": "weather",
|
||||
"circle1color": "green-red",
|
||||
"circle2color": "#0000ff",
|
||||
"circle3color": "red-green",
|
||||
"circle4color": "#ffff00",
|
||||
"circle1colorizeIcon": true,
|
||||
"circle2colorizeIcon": true,
|
||||
"circle3colorizeIcon": true,
|
||||
"circle4colorizeIcon": false,
|
||||
"hrmValidity": 60
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "circlesclock",
|
||||
"name": "Circles clock",
|
||||
"shortName":"Circles clock",
|
||||
"version":"0.09",
|
||||
"version":"0.10",
|
||||
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
"storage": [
|
||||
{"name":"circlesclock.app.js","url":"app.js"},
|
||||
{"name":"circlesclock.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"circlesclock.settings.js","url":"settings.js"}
|
||||
{"name":"circlesclock.settings.js","url":"settings.js"},
|
||||
{"name":"circlesclock.default.json","url":"default.json"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"circlesclock.json"}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = "circlesclock.json";
|
||||
const storage = require('Storage');
|
||||
let settings = storage.readJSON(SETTINGS_FILE, 1) || {};
|
||||
let settings = Object.assign(
|
||||
storage.readJSON("circlesclock.default.json", true) || {},
|
||||
storage.readJSON(SETTINGS_FILE, true) || {}
|
||||
);
|
||||
|
||||
function save(key, value) {
|
||||
settings[key] = value;
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
|
|
@ -10,8 +14,8 @@
|
|||
const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"];
|
||||
const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"];
|
||||
|
||||
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"];
|
||||
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"];
|
||||
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"];
|
||||
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"];
|
||||
|
||||
const weatherData = ["empty", "humidity", "wind"];
|
||||
|
||||
|
|
@ -20,7 +24,7 @@
|
|||
'': { 'title': 'Circles clock' },
|
||||
/*LANG*/'< Back': back,
|
||||
/*LANG*/'circle count': {
|
||||
value: "circleCount" in settings ? settings.circleCount : 3,
|
||||
value: settings.circleCount,
|
||||
min: 3,
|
||||
max : 4,
|
||||
step: 1,
|
||||
|
|
@ -33,7 +37,7 @@
|
|||
/*LANG*/'heartrate': ()=>showHRMenu(),
|
||||
/*LANG*/'steps': ()=>showStepMenu(),
|
||||
/*LANG*/'battery warn': {
|
||||
value: "batteryWarn" in settings ? settings.batteryWarn : 30,
|
||||
value: settings.batteryWarn,
|
||||
min: 10,
|
||||
max : 100,
|
||||
step: 10,
|
||||
|
|
@ -43,12 +47,12 @@
|
|||
onchange: x => save('batteryWarn', x),
|
||||
},
|
||||
/*LANG*/'show widgets': {
|
||||
value: "showWidgets" in settings ? settings.showWidgets : false,
|
||||
value: !!settings.showWidgets,
|
||||
format: () => (settings.showWidgets ? 'Yes' : 'No'),
|
||||
onchange: x => save('showWidgets', x),
|
||||
},
|
||||
/*LANG*/'weather circle': {
|
||||
value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 1,
|
||||
/*LANG*/'weather data': {
|
||||
value: weatherData.indexOf(settings.weatherCircleData),
|
||||
min: 0, max: 2,
|
||||
format: v => weatherData[v],
|
||||
onchange: x => save('weatherCircleData', weatherData[x]),
|
||||
|
|
@ -62,7 +66,7 @@
|
|||
'': { 'title': /*LANG*/'Heartrate' },
|
||||
/*LANG*/'< Back': ()=>showMainMenu(),
|
||||
/*LANG*/'minimum': {
|
||||
value: "minHR" in settings ? settings.minHR : 40,
|
||||
value: settings.minHR,
|
||||
min: 0,
|
||||
max : 250,
|
||||
step: 5,
|
||||
|
|
@ -72,7 +76,7 @@
|
|||
onchange: x => save('minHR', x),
|
||||
},
|
||||
/*LANG*/'maximum': {
|
||||
value: "maxHR" in settings ? settings.maxHR : 200,
|
||||
value: settings.maxHR,
|
||||
min: 20,
|
||||
max : 250,
|
||||
step: 5,
|
||||
|
|
@ -82,7 +86,7 @@
|
|||
onchange: x => save('maxHR', x),
|
||||
},
|
||||
/*LANG*/'min. confidence': {
|
||||
value: "confidence" in settings ? settings.confidence : 0,
|
||||
value: settings.confidence,
|
||||
min: 0,
|
||||
max : 100,
|
||||
step: 10,
|
||||
|
|
@ -92,7 +96,7 @@
|
|||
onchange: x => save('confidence', x),
|
||||
},
|
||||
/*LANG*/'valid period': {
|
||||
value: "hrmValidity" in settings ? settings.hrmValidity : 30,
|
||||
value: settings.hrmValidity,
|
||||
min: 10,
|
||||
max : 600,
|
||||
step: 10,
|
||||
|
|
@ -110,7 +114,7 @@
|
|||
'': { 'title': /*LANG*/'Steps' },
|
||||
/*LANG*/'< Back': ()=>showMainMenu(),
|
||||
/*LANG*/'goal': {
|
||||
value: "stepGoal" in settings ? settings.stepGoal : 10000,
|
||||
value: settings.stepGoal,
|
||||
min: 2000,
|
||||
max : 50000,
|
||||
step: 2000,
|
||||
|
|
@ -120,7 +124,7 @@
|
|||
onchange: x => save('stepGoal', x),
|
||||
},
|
||||
/*LANG*/'distance goal': {
|
||||
value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000,
|
||||
value: settings.stepDistanceGoal,
|
||||
min: 2000,
|
||||
max : 30000,
|
||||
step: 1000,
|
||||
|
|
@ -130,7 +134,7 @@
|
|||
onchange: x => save('stepDistanceGoal', x),
|
||||
},
|
||||
/*LANG*/'step length': {
|
||||
value: "stepLength" in settings ? settings.stepLength : 0.8,
|
||||
value: settings.stepLength,
|
||||
min: 0.1,
|
||||
max : 1.5,
|
||||
step: 0.01,
|
||||
|
|
@ -142,9 +146,6 @@
|
|||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
|
||||
|
||||
function showCircleMenu(circleId) {
|
||||
const circleName = "circle" + circleId;
|
||||
const colorKey = circleName + "color";
|
||||
|
|
@ -154,19 +155,19 @@
|
|||
'': { 'title': /*LANG*/'Circle ' + circleId },
|
||||
/*LANG*/'< Back': ()=>showMainMenu(),
|
||||
/*LANG*/'data': {
|
||||
value: settings[circleName]!=undefined ? valuesCircleTypes.indexOf(settings[circleName]) : valuesCircleTypes.indexOf(defaultCircleTypes[circleId -1]),
|
||||
value: valuesCircleTypes.indexOf(settings[circleName]),
|
||||
min: 0, max: valuesCircleTypes.length - 1,
|
||||
format: v => namesCircleTypes[v],
|
||||
onchange: x => save(circleName, valuesCircleTypes[x]),
|
||||
},
|
||||
/*LANG*/'color': {
|
||||
value: settings[colorKey] ? valuesColors.indexOf(settings[colorKey]) : 0,
|
||||
value: valuesColors.indexOf(settings[colorKey]) || 0,
|
||||
min: 0, max: valuesColors.length - 1,
|
||||
format: v => namesColors[v],
|
||||
onchange: x => save(colorKey, valuesColors[x]),
|
||||
},
|
||||
/*LANG*/'colorize icon': {
|
||||
value: colorizeIconKey in settings ? settings[colorizeIconKey] : false,
|
||||
value: settings[colorizeIconKey] || false,
|
||||
format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'),
|
||||
onchange: x => save(colorizeIconKey, x),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: Release
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Info
|
||||
|
||||
A very simple app that shows information on 3 different screens.
|
||||
Go to the next screen via tab right, go to the previous screen
|
||||
via tab left and reload the data via tab in the middle of the
|
||||
screen. Very useful if combined with pattern launcher ;)
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## Contributors
|
||||
- [David Peer](https://github.com/peerdavid).
|
||||
|
||||
## Thanks To
|
||||
<a href="https://www.flaticon.com/free-icons/info" title="info icons">Info icons created by Freepik - Flaticon</a>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
var s = require("Storage");
|
||||
const locale = require('locale');
|
||||
var ENV = process.env;
|
||||
var W = g.getWidth(), H = g.getHeight();
|
||||
var screen = 0;
|
||||
const maxScreen = 2;
|
||||
|
||||
function getVersion(file) {
|
||||
var j = s.readJSON(file,1);
|
||||
var v = ("object"==typeof j)?j.version:false;
|
||||
return v?((v?"v"+v:"Unknown")):"NO ";
|
||||
}
|
||||
|
||||
|
||||
function drawData(name, value, y){
|
||||
g.drawString(name, 5, y);
|
||||
g.drawString(value, 100, y);
|
||||
}
|
||||
|
||||
function getSteps(){
|
||||
try{
|
||||
return Bangle.getHealthStatus("day").steps;
|
||||
} catch(e) {
|
||||
return ">= 2v12";
|
||||
}
|
||||
}
|
||||
|
||||
function getBpm(){
|
||||
try{
|
||||
return Math.round(Bangle.getHealthStatus("day").bpm) + "bpm";
|
||||
} catch(e) {
|
||||
return ">= 2v12";
|
||||
}
|
||||
}
|
||||
|
||||
function drawInfo() {
|
||||
g.reset().clearRect(Bangle.appRect);
|
||||
var h=18, y = h;//-h;
|
||||
|
||||
// Header
|
||||
g.setFont("Vector", h+2).setFontAlign(0,-1);
|
||||
g.drawString("--==|| INFO ||==--", W/2, 0);
|
||||
g.setFont("Vector",h).setFontAlign(-1,-1);
|
||||
|
||||
// Dynamic data
|
||||
if(screen == 0){
|
||||
drawData("Steps", getSteps(), y+=h);
|
||||
drawData("HRM", getBpm(), y+=h);
|
||||
drawData("Battery", E.getBattery() + "%", y+=h);
|
||||
drawData("Voltage", E.getAnalogVRef().toFixed(2) + "V", y+=h);
|
||||
drawData("IntTemp.", locale.temp(parseInt(E.getTemperature())), y+=h);
|
||||
}
|
||||
|
||||
if(screen == 1){
|
||||
drawData("Charging?", Bangle.isCharging() ? "Yes" : "No", y+=h);
|
||||
drawData("Bluetooth", NRF.getSecurityStatus().connected ? "Conn." : "Disconn.", y+=h);
|
||||
drawData("GPS", Bangle.isGPSOn() ? "On" : "Off", y+=h);
|
||||
drawData("Compass", Bangle.isCompassOn() ? "On" : "Off", y+=h);
|
||||
drawData("HRM", Bangle.isHRMOn() ? "On" : "Off", y+=h);
|
||||
}
|
||||
|
||||
// Static data
|
||||
if(screen == 2){
|
||||
drawData("Firmw.", ENV.VERSION, y+=h);
|
||||
drawData("Boot.", getVersion("boot.info"), y+=h);
|
||||
drawData("Settings", getVersion("setting.info"), y+=h);
|
||||
drawData("Storage", "", y+=h);
|
||||
drawData(" Total", ENV.STORAGE>>10, y+=h);
|
||||
drawData(" Free", require("Storage").getFree()>>10, y+=h);
|
||||
}
|
||||
|
||||
if(Bangle.isLocked()){
|
||||
g.setFont("Vector",h-2).setFontAlign(-1,-1);
|
||||
g.drawString("Locked", 0, H-h+2);
|
||||
}
|
||||
|
||||
g.setFont("Vector",h-2).setFontAlign(1,-1);
|
||||
g.drawString((screen+1) + "/3", W, H-h+2);
|
||||
}
|
||||
|
||||
drawInfo();
|
||||
setWatch(_=>load(), BTN1);
|
||||
|
||||
Bangle.on('touch', function(btn, e){
|
||||
var left = parseInt(g.getWidth() * 0.3);
|
||||
var right = g.getWidth() - left;
|
||||
var isLeft = e.x < left;
|
||||
var isRight = e.x > right;
|
||||
|
||||
if(isRight){
|
||||
screen = (screen + 1) % (maxScreen+1);
|
||||
}
|
||||
|
||||
if(isLeft){
|
||||
screen -= 1;
|
||||
screen = screen < 0 ? maxScreen : screen;
|
||||
}
|
||||
|
||||
drawInfo();
|
||||
});
|
||||
|
||||
Bangle.on('lock', function(isLocked) {
|
||||
drawInfo();
|
||||
});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
// Bangle.drawWidgets();
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcBkmSpICDBwcJBYwCDpAhFggRJGg8SCI+ABgU//gSDCI4JBj//AAX4JRAIBg4QDAAPgBIJWGgIQFAAI+BLglAgEPCI/wEgJoEgYQHAAPANwhWFAApcBCIWQgAQJAAMAgSMDCJiSCwB6GQA6eCn5TFL4q5BUgIRF/wuBv4RGkCeGO4IREUgMBCJCVGCISwIWw0BYRLIICLBHHCJRrGCIQIFR44I5LIoRaPpARcdIwRJfYMBCJuACKUkgE/a5f8gEJCJD7FCIeAg78FAAvggFJCIMACJZOBCIOQCJsCCIOSgEfCBP4gESCIZTFOIwRDoDIGaguSCIVIgCkFTwcAggRDpIYBQAx6BgAOCAQYIBLghWBTwQRFFgIABXIIFDBwgCDBYQAENAYCFLgIAEKwpKIIhA="))
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id": "info",
|
||||
"name": "Info",
|
||||
"version": "0.01",
|
||||
"description": "An application that displays information such as battery level, steps etc.",
|
||||
"icon": "info.png",
|
||||
"type": "app",
|
||||
"tags": "tool",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"screenshots": [
|
||||
{"url":"screenshot_1.png"},
|
||||
{"url":"screenshot_2.png"},
|
||||
{"url":"screenshot_3.png"}],
|
||||
"storage": [
|
||||
{"name":"info.app.js","url":"info.app.js"},
|
||||
{"name":"info.img","url":"info.icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
|
@ -1 +1,2 @@
|
|||
0.01: Initial release
|
||||
0.02: Optional fullscreen mode
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
|---------------------------------|--------------------------------------|
|
||||
| <center>Neon X</center> | <center>Neon IO X</center> |
|
||||
|
||||
This is a clock based on Pebble's Neon X and Neon IO X watchfaces by Sam Jerichow.
|
||||
Can be switched between in the Settings menu, which can be accessed through
|
||||
This is a clock based on Pebble's Neon X and Neon IO X watchfaces by Sam Jerichow.
|
||||
Can be switched between in the Settings menu, which can be accessed through
|
||||
the app/widget settings menu of the Bangle.js
|
||||
|
||||
## Settings
|
||||
|
|
@ -14,7 +14,11 @@ the app/widget settings menu of the Bangle.js
|
|||
Activate the Neon IO X clock look, a bit hard to read until one gets used to it.
|
||||
|
||||
### Thickness
|
||||
The thickness of watch lines, from 1 to 5.
|
||||
The thickness of watch lines, from 1 to 6.
|
||||
|
||||
### Date on touch
|
||||
Shows the current date as DD MM on touch and reverts back to time after 5 seconds or with another touch.
|
||||
|
||||
### Fullscreen
|
||||
Shows the watchface in fullscreen mode.
|
||||
Note: In fullscreen mode, widgets are hidden, but still loaded.
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "neonx",
|
||||
"name": "Neon X & IO X Clock",
|
||||
"shortName": "Neon X Clock",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Pebble Neon X & Neon IO X for Bangle.js",
|
||||
"icon": "neonx.png",
|
||||
"type": "clock",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const colors = {
|
|||
|
||||
const is12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false;
|
||||
const screenWidth = g.getWidth();
|
||||
const screenHeight = g.getHeight();
|
||||
const halfWidth = screenWidth / 2;
|
||||
const scale = screenWidth / 240;
|
||||
const REFRESH_RATE = 10E3;
|
||||
|
|
@ -58,16 +59,19 @@ function drawLine(poly, thickness){
|
|||
}
|
||||
}
|
||||
|
||||
let settings = require('Storage').readJSON('neonx.json', 1);
|
||||
|
||||
if (!settings) {
|
||||
settings = {
|
||||
thickness: 4,
|
||||
io: 0,
|
||||
showDate: 1
|
||||
};
|
||||
let settings = {
|
||||
thickness: 4,
|
||||
io: 0,
|
||||
showDate: 1,
|
||||
fullscreen: false,
|
||||
};
|
||||
let saved_settings = require('Storage').readJSON('neonx.json', 1) || settings;
|
||||
for (const key in saved_settings) {
|
||||
settings[key] = saved_settings[key]
|
||||
}
|
||||
|
||||
|
||||
|
||||
function drawClock(num){
|
||||
let tx, ty;
|
||||
|
||||
|
|
@ -79,13 +83,15 @@ function drawClock(num){
|
|||
g.setColor(colors[settings.io ? 'io' : 'x'][y][x]);
|
||||
|
||||
if (!settings.io) {
|
||||
tx = (x * 100 + 18) * newScale;
|
||||
ty = (y * 100 + 32) * newScale;
|
||||
newScale *= settings.fullscreen ? 1.18 : 1.0;
|
||||
let dx = settings.fullscreen ? 0 : 18
|
||||
tx = (x * 100 + dx) * newScale;
|
||||
ty = (y * 100 + dx*2) * newScale;
|
||||
} else {
|
||||
newScale = 0.33 + current * 0.4;
|
||||
newScale = 0.33 + current * (settings.fullscreen ? 0.48 : 0.4);
|
||||
|
||||
tx = (halfWidth - 139) * newScale + halfWidth;
|
||||
ty = (halfWidth - 139) * newScale + halfWidth + 12;
|
||||
tx = (halfWidth - 139) * newScale + halfWidth + (settings.fullscreen ? 2 : 0);
|
||||
ty = (halfWidth - 139) * newScale + halfWidth + (settings.fullscreen ? 2 : 12);
|
||||
}
|
||||
|
||||
for (let i = 0; i < digits[num[y][x]].length; i++) {
|
||||
|
|
@ -116,7 +122,11 @@ function draw(date){
|
|||
l2 = ('0' + d.getMinutes()).substr(-2);
|
||||
}
|
||||
|
||||
g.clearRect(0,24,240,240);
|
||||
if(settings.fullscreen){
|
||||
g.clearRect(0,0,screenWidth,screenHeight);
|
||||
} else {
|
||||
g.clearRect(0,24,240,240);
|
||||
}
|
||||
|
||||
drawClock([l1, l2]);
|
||||
}
|
||||
|
|
@ -150,4 +160,9 @@ Bangle.on('lcdPower', function(on){
|
|||
});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
if(settings.fullscreen){
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
} else {
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@
|
|||
neonXSettings = {
|
||||
thickness: 4,
|
||||
io: 0,
|
||||
showDate: 1
|
||||
showDate: 1,
|
||||
fullscreen: false,
|
||||
};
|
||||
|
||||
updateSettings();
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
|
||||
if (!neonXSettings) resetSettings();
|
||||
|
||||
let thicknesses = [1, 2, 3, 4, 5];
|
||||
let thicknesses = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
const menu = {
|
||||
"" : { "title":"Neon X & IO"},
|
||||
|
|
@ -48,7 +49,15 @@
|
|||
neonXSettings.showDate = v;
|
||||
updateSettings();
|
||||
}
|
||||
}
|
||||
},
|
||||
'Fullscreen': {
|
||||
value: false | neonXSettings.fullscreen,
|
||||
format: () => (neonXSettings.fullscreen ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
neonXSettings.fullscreen = !neonXSettings.fullscreen;
|
||||
updateSettings();
|
||||
},
|
||||
},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@
|
|||
0.14: incorporated lazybones idle timer, configuration settings to come
|
||||
0.15: fixed tendancy for mylocation to default to London
|
||||
added setting to enable/disable idle timer warning
|
||||
0.16: make check_idle boolean setting work properly with new B2 menu
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "pastel",
|
||||
"name": "Pastel Clock",
|
||||
"shortName": "Pastel",
|
||||
"version": "0.15",
|
||||
"version": "0.16",
|
||||
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times",
|
||||
"icon": "pastel.png",
|
||||
"dependencies": {"mylocation":"app","weather":"app"},
|
||||
|
|
|
|||
|
|
@ -38,38 +38,28 @@
|
|||
},
|
||||
},
|
||||
'Show Grid': {
|
||||
value: s.grid,
|
||||
format: () => (s.grid ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.grid = !s.grid;
|
||||
value: !!s.grid,
|
||||
format: v => v ? /*LANG*/"Yes":/*LANG*/"No",
|
||||
onchange: v => {
|
||||
s.grid = v;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Show Weather': {
|
||||
value: s.weather,
|
||||
format: () => (s.weather ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.weather = !s.weather;
|
||||
value: !!s.weather,
|
||||
format: v => v ? /*LANG*/"Yes":/*LANG*/"No",
|
||||
onchange: v => {
|
||||
s.weather = v;
|
||||
save();
|
||||
},
|
||||
},
|
||||
// for use when the new menu system goes live
|
||||
/*
|
||||
'Idle Warning': {
|
||||
value: s.idle_check,
|
||||
onchange : v => {
|
||||
value: !!s.idle_check,
|
||||
format: v => v ? /*LANG*/"Yes":/*LANG*/"No",
|
||||
onchange: v => {
|
||||
s.idle_check = v;
|
||||
save();
|
||||
},
|
||||
},
|
||||
*/
|
||||
'Idle Warning': {
|
||||
value: s.idle_check,
|
||||
format: () => (s.idle_check ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.idle_check = !s.idle_check;
|
||||
save();
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
0.01: Initial Release
|
||||
0.02: Minor tweaks for light theme
|
||||
0.03: Made images 2 bit and fixed theme honoring
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Rolex
|
||||
|
||||

|
||||
|
||||
Created with the aid of the Espruino documentation and looking through many of the wonderful exising watchfaces that have been made.
|
||||
This has not been tested on a watch yet as I haven't aquired one but has been tested in the emulator.
|
||||
The hands don't rotate dead on center but they're as close as I could get them to.
|
||||
|
||||
Special thanks to:
|
||||
* rozek (for his updated widget draw code for utilization with background images)
|
||||
* Gordon Williams (Bangle.js, watchapps for reference code and documentation)
|
||||
* The community (for helping drive such a wonderful project)
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkEBxURiIlUgMxiUQC6cCiMhmAqPmQEDkUhF4cjGhUD/4FDn/zAof/GhYMEC4kCEQgAHl/xGoYcDj/yC5ZIFJgnwPBglIn6rNBpEBXp8QiQSBiMQFpMCS4sDXgMjgMhgYXFEgIDBh//kA/EiEhiURiMBBYs/FYSwB+YdBCQIBBkAYBiUQkACBCwTOEUYKaBkUhAAIXCDYMRkYxBIILNDAAMTHgZxBiBFBFQKOCgMvbRUBgUxIYJ3BSYUBmYJBU5QsCkIDBgQIBkcyYJwAFkczeBoAGiYWVgYWTbQMCmchfojgBDxc/+f/mUjC4abBkEf/4ABDY0C+cvmQKFgcxkTyBC47pBC4LgDAAUPmMyh4IEiUQiUyiJHBIwJ9GmMxC4kBmXyGYcBdQMykIPEcIIlBFgMikMzIAxiBkSPEIYqSKmX/mLWTgEimRiGAB0T+bwTVocCMBEAj51GAA4aGif/+AXNh//FAcC//ziEBgEhiCxBiADCXAIDBCIYdFgLCBaIMCkKNBkQIBkQTBgZBDgRdEiIsBGoMBAoLoDLQRxIkMhewMigMyiESiRrNWqpMB+QJHl4hMh/zBI//a5IlDYQcBFQcf+IWKgLJEj4cDgY5IBgf/AoYXEEQp2HHggXEKQIXKAAoXFACIXkA"))
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
var imgBg = {
|
||||
width : 176, height : 176, bpp : 1,
|
||||
transparent : 0,
|
||||
buffer : require("heatshrink").decompress(atob("ABMBwAVpmfMCqdxxwVpmOMCiUDmOIoAVRg8xzHwCqUZCqcDCoPAYBwDDzOZYx0DB4QVGF5UPComRAoZbKn4hD7uzGof4CpN/8AVI/wVO5+3T4YVKn5NDt/2Cqd/CAcP/gVJj5jDCogJECpc/EwYVLG4gVEJYgAGMYgVVLgqjDAA0D/5GDRAgVPg4VD/77DAA0B/+ABBwAEEQ4VVJQgAIMg4VVUQgAIn4VUj5kGgbfDABEeBA7fDABEMBA84CpYAIiAVUAHVAh4USgf4j+HVgPg4AVOxlw44DBuLMGcgQVFg1gs0DgE7JowVCGognB4AVB0DiFgPgAYMPAYRXD8AVB+EOHooVDfQkDjBBDj4VEh/wAYMf/wVEhkw40Tg0zNok/Cof/BQcDxyZB+f8sJjE/49Cn//Uh0B//8FYYwCwEgAYMQVQ4VDh4ECgfSufOvF548c8+CuQmGgf//4eBv3BxFMtuN23DmVzCAN//6rDCoPAg1suEMsAVBmgVBrYPDPwZuCCoMZhhBBx84CoPyNgSqETQUDpnZhFIsODmGDiX7KIRsCOYeAgOAuDsBgKhB8UMwPAFYJsCT4avNX41/Cpz2EgEeAYU+jl8vwHBnl8BQV+DgpSBAAMjmkQoEDsVmnAKCvA2JkcmCocweocwCpMhm0QvECsccCoYAKkd+FYNCscGCp0ysQVBs0iAgIVNn1DPYM8AAIVOhACBnkYhEYChoA/AH4A/AH4A/AC1///+CtH/AAIVVAAIuQCqkBCv4V/CozERCqrxZCtF/QCAA/AH4A/AH4A/AH4ArhgONkAGFnwVNuAGFvwVN/gFEgP/CpoOFg4VNEgOAAwcfAwoVJ8AGDn//4AVLgf//BHEFZoVB/wFIIJZnDh//RQMYhGICxN/KIZWB+EAmEfzsN4Ew8PMw3JnYMBPoIDBNgk6u8XjmA8OT2XJ/8zvg8ECoRsBh1/jscwdhj/ztO7iV4NAQVCj5sCh1Tzseydhy/jnO3icwSgSaCh4DCnOBwviwcw+Msm0BidINwRXCh4DCABsfCIUHbJgADg7yCg4IDhlmjEgAgPssOGsDHDCoQAEiXMs9yt8R5VrzmwBoY9HCoNjuVGiHOpOcyBKLiWMsd6s0R51LjgVMjkIjEPukM5UIjiQMhkAjEDmEMMoMGNoYA0jAVUjgIHuAVLn4IH/AVLv7OGgf8Cpj6Gg4VM/4rGg/+CsEB/+AK43/CpQMIDwIVVGgxONMA4VIj/wCp0PUwYVEXA4VZj7+DCok/AgYAGn4VID4gVHCAgVPJogVEMIgVRXJClGCojPJFYQVIgYVKj79DCoptKh4VIgKvKgYwECohLDABYVEACAV/Cv4V7"))
|
||||
};
|
||||
|
||||
/* Set hour hand image */
|
||||
|
||||
var imgHour = {
|
||||
width : 19, height : 62, bpp : 2,
|
||||
transparent : 0,
|
||||
buffer : E.toArrayBuffer(atob("AAP/wAAA///wAA////AA////8A/////A/////8P/////D/////w/////8P/////D/////w/////8D////8AP////AA////AAD///AAAP//AAAA//AAAAPXwAAAD18AAAA9fAAAAPXwAAAD18AAAA9fAAAAPXwAAAD18AAAA9fAAAAPXwAAAD18AAAA//AAAA//wAAA///wAD/X1/AD/V9X8A/VfVfw/VX1V8PVV9VXD1VfVV89VX1VfPVf/1Xz1f//V89/9f/fP/1Vf/A/VVVfwP1VVXwA/VVX8AD/VfwAAP//wAAA//wAAAD18AAAA9fAAAAPXAAAAD1wAAAAN8AAAAD8AAAAA/AAAAAPwAAAAA8AAAAAMAAAAADAAAAAAwAAAAAAAAAA=="))
|
||||
};
|
||||
|
||||
/* Set minute hand image */
|
||||
|
||||
var imgMin = {
|
||||
width : 10, height : 80, bpp : 2,
|
||||
transparent : 0,
|
||||
buffer : E.toArrayBuffer(atob("AAAADwAP/wP//D//z/////////8//8P//A//AD/AA/wAP8AD/AA/8A/8AP/wD/8A//AP/wD/8A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A9fAPXwD18A//AP/wA/wAP8AD/AA/wAP8AA/AAPAADwAA8AAPAADwAAMAAAAAAAA="))
|
||||
};
|
||||
|
||||
/* Set second hand image */
|
||||
|
||||
var imgSec = {
|
||||
width : 8, height : 116, bpp : 2,
|
||||
transparent : 2,
|
||||
buffer : E.toArrayBuffer(atob("v/q//r/+v/qv+q/qq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qv+v/6/D/wD8PDw8PwD/w///6v+qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqr+r//v///X/1X/Vf9V/1X/1///+//q/qq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq+qr6qvqq6qrqg=="))
|
||||
};
|
||||
|
||||
/* Set variables to get screen width, height and center points */
|
||||
|
||||
let W = g.getWidth();
|
||||
let H = g.getHeight();
|
||||
let cx = W/2;
|
||||
let cy = H/2;
|
||||
let Timeout;
|
||||
|
||||
/* set font */
|
||||
|
||||
require("Font4x5Numeric").add(Graphics);
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
||||
/* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */
|
||||
|
||||
Bangle.drawWidgets = function () {
|
||||
var w = g.getWidth(), h = g.getHeight();
|
||||
|
||||
var pos = {
|
||||
tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left
|
||||
tr:{x:w-1, y:0, r:1, c:0},
|
||||
bl:{x:0, y:h-24, r:0, c:0},
|
||||
br:{x:w-1, y:h-24, r:1, c:0}
|
||||
};
|
||||
|
||||
if (global.WIDGETS) {
|
||||
for (var wd of WIDGETS) {
|
||||
var p = pos[wd.area];
|
||||
if (!p) continue;
|
||||
|
||||
wd.x = p.x - p.r*wd.width;
|
||||
wd.y = p.y;
|
||||
|
||||
p.x += wd.width*(1-2*p.r);
|
||||
p.c++;
|
||||
}
|
||||
|
||||
g.reset(); // also loads the current theme
|
||||
|
||||
try {
|
||||
for (var wd of WIDGETS) {
|
||||
g.setClipRect(wd.x,wd.y, wd.x+wd.width-1,23);
|
||||
wd.draw(wd);
|
||||
}
|
||||
} catch (e) { print(e); }
|
||||
|
||||
g.reset(); // clears the clipping rectangle!
|
||||
}
|
||||
};
|
||||
|
||||
/* Draws the clock hands and date */
|
||||
|
||||
function drawHands() {
|
||||
let d = new Date();
|
||||
|
||||
let hour = d.getHours() % 12;
|
||||
let min = d.getMinutes();
|
||||
let sec = d.getSeconds();
|
||||
|
||||
let twoPi = 2*Math.PI;
|
||||
let Pi = Math.PI;
|
||||
let halfPi = Math.PI/2;
|
||||
|
||||
let hourAngle = (hour+(min/60))/12 * twoPi - Pi;
|
||||
let minAngle = (min/60) * twoPi - Pi;
|
||||
let secAngle = (sec/60) * twoPi - Pi;
|
||||
|
||||
let hourSin = Math.sin(hourAngle);
|
||||
let hourCos = Math.cos(hourAngle);
|
||||
let minSin = Math.sin(minAngle);
|
||||
let minCos = Math.cos(minAngle);
|
||||
let secSin = Math.sin(secAngle);
|
||||
let secCos = Math.cos(secAngle);
|
||||
|
||||
g.drawImage(imgHour,cx-22*hourSin,cy+22*hourCos,{rotate:hourAngle});
|
||||
g.drawImage(imgMin,cx-34*minSin,cy+34*minCos,{rotate:minAngle});
|
||||
g.drawImage(imgSec,cx-25*secSin,cy+25*secCos,{rotate:secAngle});
|
||||
g.setFont("4x5Numeric:3");
|
||||
g.setColor(g.theme.bg);
|
||||
g.drawString(d.getDate(),157,81);
|
||||
}
|
||||
|
||||
function drawBackground() {
|
||||
g.clear(1);
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawImage(imgBg,0,0);
|
||||
g.reset();
|
||||
}
|
||||
|
||||
/* Refresh the display every second */
|
||||
|
||||
function displayRefresh() {
|
||||
g.clear(true);
|
||||
drawBackground();
|
||||
drawHands();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
let Pause = 1000 - (Date.now() % 1000);
|
||||
Timeout = setTimeout(displayRefresh,Pause);
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
if (on) {
|
||||
if (Timeout != null) { clearTimeout(Timeout); Timeout = undefined;}
|
||||
displayRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.setUI("clock");
|
||||
// load widgets after 'setUI' so they're aware there is a clock active
|
||||
Bangle.loadWidgets();
|
||||
displayRefresh();
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{ "id": "rolex",
|
||||
"name": "rolex",
|
||||
"shortName":"rolex",
|
||||
"icon": "rolex.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version":"0.03",
|
||||
"description": "A rolex like watch face",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"supports":["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"rolex.app.js","url":"app.js"},
|
||||
{"name":"rolex.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -44,3 +44,4 @@
|
|||
0.39: Fix misbehaving debug info option
|
||||
0.40: Moved off into Utils, put System after Apps
|
||||
0.41: Stop users disabling all wake-up methods and locking themselves out (fix #1272)
|
||||
0.42: Fix theme customizer on new Bangle 2 firmware
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "setting",
|
||||
"name": "Settings",
|
||||
"version": "0.41",
|
||||
"version": "0.42",
|
||||
"description": "A menu for setting up Bangle.js",
|
||||
"icon": "settings.png",
|
||||
"tags": "tool,system",
|
||||
|
|
|
|||
|
|
@ -243,12 +243,11 @@ function showThemeMenu() {
|
|||
});
|
||||
|
||||
function showCustomThemeMenu() {
|
||||
function cv(x) { return g.setColor(x).getColor(); }
|
||||
function setT(t, v) {
|
||||
let th = g.theme;
|
||||
th[t] = v;
|
||||
if (t==="bg") {
|
||||
th['dark'] = (v===cv("#000"));
|
||||
th['dark'] = (v===cl("#000"));
|
||||
}
|
||||
upd(th);
|
||||
}
|
||||
|
|
@ -260,11 +259,7 @@ function showThemeMenu() {
|
|||
let colors = [], names = [];
|
||||
for(const c in rgb) {
|
||||
names.push(c);
|
||||
colors.push(cv(rgb[c]));
|
||||
}
|
||||
function cn(v) {
|
||||
const i = colors.indexOf(v);
|
||||
return i!== -1 ? names[i] : v; // another color: just show value
|
||||
colors.push(cl(rgb[c]));
|
||||
}
|
||||
let menu = {
|
||||
'':{title:'Custom Theme'},
|
||||
|
|
@ -277,14 +272,11 @@ function showThemeMenu() {
|
|||
};
|
||||
["fg", "bg", "fg2", "bg2", "fgH", "bgH"].forEach(t => {
|
||||
menu[labels[t]] = {
|
||||
value: colors.indexOf(g.theme[t]),
|
||||
format: () => cn(g.theme[t]),
|
||||
min : 0, max : colors.length-1, wrap : true,
|
||||
value: Math.max(colors.indexOf(g.theme[t]),0),
|
||||
format: v => names[v],
|
||||
onchange: function(v) {
|
||||
// wrap around
|
||||
if (v>=colors.length) {v = 0;}
|
||||
if (v<0) {v = colors.length-1;}
|
||||
this.value = v;
|
||||
const c = colors[v];
|
||||
var c = colors[v];
|
||||
// if we select the same fg and bg: set the other to the old color
|
||||
// e.g. bg=black;fg=white, user selects fg=black -> bg changes to white automatically
|
||||
// so users don't end up with a black-on-black menu
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Fix crash on start
|
||||
0.03: Added power saving mode, move all read/write log actions into lib/module, fix #1445
|
||||
0.02: Fix crash on start #1423
|
||||
0.03: Added power saving mode, move all read/write log actions into lib/module
|
||||
0.04: Fix #1445, display loading info, add icons to display service states
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ also provides a power saving mode using the built in movement calculation. The i
|
|||
#### Operating Principle
|
||||
* __ESS calculation__
|
||||
The accelerometer polls values with 12.5Hz. On each poll the magnitude value is saved. When 13 values are collected, every 1.04 seconds, the standard deviation over this values is calculated.
|
||||
Is the calculated standard deviation lower than the "no movement" threshold (__NoMoThresh__) a "no movement" counter is incremented. Each time the "no movement" threshold is reached the "no movement" counter will be reset. 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 __MinDuration__ setting, Example: _sleep threshold = MinDuration * 60 / calculation interval => 10min * 60s/min / 1.04s ~= 576,9 rounded up to 577_)
|
||||
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 (__MaxMove__). The event is only triggered every 10 minutes which decreases the battery impact but also reduces accurracy.
|
||||
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 (__TempThresh__). Otherwise the watch is considered as 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 (__MaxAwake__). If this sum is lower than the minimal consecutive sleep duration (__MinConsec__) it is not considered, otherwise it will be added to the consecutive sleep value.
|
||||
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.
|
||||
|
|
@ -34,41 +34,44 @@ also provides a power saving mode using the built in movement calculation. The i
|
|||
---
|
||||
### Settings
|
||||
---
|
||||
* __BreakTod__ | break at time of day
|
||||
* __Break Tod__ | break at time of day
|
||||
_0_ / _1_ / _..._ / __10__ / _..._ / _12_
|
||||
Change time of day on wich the lower graph starts and the upper graph ends.
|
||||
* __MaxAwake__ | maximal awake duration
|
||||
* __Max Awake__ | maximal awake duration
|
||||
_15min_ / _20min_ / _..._ / __60min__ / _..._ / _120min_
|
||||
Adjust the maximal awake duration upon the exceeding of which aborts the consecutive sleep period.
|
||||
* __MinConsec__ | minimal consecutive sleep duration
|
||||
* __Min Consec__ | minimal consecutive sleep duration
|
||||
_15min_ / _20min_ / _..._ / __30min__ / _..._ / _120min_
|
||||
Adjust the minimal consecutive sleep duration that will be considered for the consecutive sleep value.
|
||||
* __TempThresh__ | temperature threshold
|
||||
* __Temp Thresh__ | temperature threshold
|
||||
_20°C_ / _20.5°C_ / _..._ / __25°C__ / _..._ / _40°C_
|
||||
The internal temperature must be greater than this threshold to log _sleeping_, otherwise it is _not worn_.
|
||||
* __PowerSaving__
|
||||
* __Power Saving__
|
||||
_on_ / __off__
|
||||
En-/Disable power saving mode. _Saves battery, but might decrease accurracy._
|
||||
* __MaxMove__ | maximal movement threshold
|
||||
En-/Disable power saving mode. _Saves battery, but might decrease accurracy._
|
||||
In app icon showing that power saving mode is enabled: 
|
||||
* __Max Move__ | maximal movement threshold
|
||||
(only available when on power saving mode)
|
||||
_50_ / _51_ / _..._ / __100__ / _..._ / _200_
|
||||
On power saving mode the watch is considered resting if this threshold is lower or equal to the movement value of bangle's health event.
|
||||
* __NoMoThresh__ | no movement threshold
|
||||
* __NoMo Thresh__ | no movement threshold
|
||||
(only available when not on power saving mode)
|
||||
_0.006_ / _0.007_ / _..._ / __0.012__ / _..._ / _0.020_
|
||||
The standard deviation over the measured values needs to be lower then this threshold to count as not moving.
|
||||
The defaut threshold value worked best for my watch. A threshold value below 0.008 may get triggert by noise.
|
||||
* __MinDuration__ | minimal no movement duration
|
||||
* __Min Duration__ | minimal no movement duration
|
||||
(only available when not on power saving mode)
|
||||
_5min_ / _6min_ / _..._ / __10min__ / _..._ / _15min_
|
||||
If no movement is detected for this duration, the watch is considered as resting.
|
||||
* __Enabled__
|
||||
__on__ / _off_
|
||||
En-/Disable the service (all background activities). _Saves the most battery, but might make this app useless._
|
||||
En-/Disable the service (all background activities). _Saves the most battery, but might make this app useless._
|
||||
In app icon showing that the service is disabled: 
|
||||
* __Logfile__
|
||||
__default__ / _off_
|
||||
En-/Disable logging by setting the logfile to _sleeplog.log_ / _undefined_.
|
||||
If the logfile has been customized it is displayed with _custom_.
|
||||
If the logfile has been customized it is displayed with _custom_.
|
||||
In app icon showing that logging is disabled: 
|
||||
|
||||
---
|
||||
### Global Object and Module Functions
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function drawLog(topY, viewUntil) {
|
|||
for (var x = 0; x < hours; x++) {
|
||||
g.fillRect(x * stepwidth, y + 2, x * stepwidth, y + 4);
|
||||
g.setFontAlign(-1, -1).setFont("6x8")
|
||||
.drawString((startHour + x) % 24, x * stepwidth, y + 6);
|
||||
.drawString((startHour + x) % 24, x * stepwidth + 1, y + 6);
|
||||
}
|
||||
|
||||
// define variables for sleep calculation
|
||||
|
|
@ -127,34 +127,24 @@ function drawLog(topY, viewUntil) {
|
|||
return output.map(value => value /= 6E4);
|
||||
}
|
||||
|
||||
// define draw night to function
|
||||
function drawNightTo(prevDays) {
|
||||
// calculate 10am of this or a previous day
|
||||
var date = Date();
|
||||
date = Date(date.getFullYear(), date.getMonth(), date.getDate() - prevDays, breaktod);
|
||||
// define function to draw the analysis
|
||||
function drawAnalysis(toDate) {
|
||||
//var t0 = Date.now();
|
||||
|
||||
// get width
|
||||
var width = g.getWidth();
|
||||
|
||||
// clear app area
|
||||
g.clearRect(0, 24, width, width);
|
||||
|
||||
// define variable for sleep calculation
|
||||
var outputs = [0, 0]; // [estimated, true]
|
||||
// draw log graphs and read outputs
|
||||
drawLog(110, date).forEach(
|
||||
(value, index) => outputs[index] += value);
|
||||
drawLog(145, Date(date.valueOf() - 432E5)).forEach(
|
||||
(value, index) => outputs[index] += value);
|
||||
|
||||
// reduce date by 1s to ensure correct headline
|
||||
date = Date(date.valueOf() - 1E3);
|
||||
// draw headline, on red bg if service or loggging disabled or green bg if powersaving enabled
|
||||
g.setColor(global.sleeplog && sleeplog.enabled && sleeplog.logfile ? sleeplog.powersaving ? 2016 : g.theme.bg : 63488);
|
||||
g.fillRect(0, 30, width, 66).reset();
|
||||
g.setFont("12x20").setFontAlign(0, -1);
|
||||
g.drawString("Night to " + require('locale').dow(date, 1) + "\n" +
|
||||
require('locale').date(date, 1), width / 2, 30);
|
||||
// clear analysis area
|
||||
g.clearRect(0, 71, width, width);
|
||||
|
||||
// draw log graphs and read outputs
|
||||
drawLog(110, toDate).forEach(
|
||||
(value, index) => outputs[index] += value);
|
||||
drawLog(144, Date(toDate.valueOf() - 432E5)).forEach(
|
||||
(value, index) => outputs[index] += value);
|
||||
|
||||
// draw outputs
|
||||
g.reset(); // area: 0, 70, width, 105
|
||||
|
|
@ -166,8 +156,57 @@ function drawNightTo(prevDays) {
|
|||
Math.floor(outputs[0] % 60) + "min", width - 10, 70);
|
||||
g.drawString(Math.floor(outputs[1] / 60) + "h " +
|
||||
Math.floor(outputs[1] % 60) + "min", width - 10, 90);
|
||||
|
||||
//print("analysis processing seconds:", Math.round(Date.now() - t0) / 1000);
|
||||
}
|
||||
|
||||
// define draw night to function
|
||||
function drawNightTo(prevDays) {
|
||||
// calculate 10am of this or a previous day
|
||||
var toDate = Date();
|
||||
toDate = Date(toDate.getFullYear(), toDate.getMonth(), toDate.getDate() - prevDays, breaktod);
|
||||
|
||||
// get width
|
||||
var width = g.getWidth();
|
||||
var center = width / 2;
|
||||
|
||||
// reduce date by 1s to ensure correct headline
|
||||
toDate = Date(toDate.valueOf() - 1E3);
|
||||
|
||||
// clear heading area
|
||||
g.clearRect(0, 24, width, 70);
|
||||
|
||||
// display service 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);
|
||||
ATID = setTimeout(drawAnalysis, 100, toDate);
|
||||
}
|
||||
|
||||
// define function to draw and setup UI
|
||||
function startApp() {
|
||||
|
|
@ -182,8 +221,9 @@ function startApp() {
|
|||
});
|
||||
}
|
||||
|
||||
// define day to display
|
||||
// define day to display and analysis timeout id
|
||||
var prevDays = 0;
|
||||
var ATID;
|
||||
|
||||
// setup app
|
||||
g.clear();
|
||||
|
|
|
|||
|
|
@ -28,36 +28,43 @@ if (sleeplog.enabled) {
|
|||
resting: undefined,
|
||||
status: undefined,
|
||||
|
||||
// define stop function (logging will restart if enabled and boot file is executed)
|
||||
stop: function() {
|
||||
// define function to handle stopping the service, it will be restarted on reload if enabled
|
||||
stopHandler: function() {
|
||||
// remove all listeners
|
||||
Bangle.removeListener('accel', sleeplog.accel);
|
||||
Bangle.removeListener('health', sleeplog.health);
|
||||
E.removeListener('kill', () => sleeplog.stop());
|
||||
// exit on missing global object
|
||||
if (!global.sleeplog) return;
|
||||
// write log with undefined sleeping status
|
||||
require("sleeplog").writeLog(0, [Math.floor(Date.now()), 0]);
|
||||
// reset always used cached values
|
||||
sleeplog.resting = undefined;
|
||||
sleeplog.status = undefined;
|
||||
sleeplog.ess_values = [];
|
||||
sleeplog.nomocount = 0;
|
||||
sleeplog.firstnomodate = undefined;
|
||||
// reset cached values if sleeplog is defined
|
||||
if (global.sleeplog) {
|
||||
sleeplog.resting = undefined;
|
||||
sleeplog.status = undefined;
|
||||
// reset cached ESS calculation values
|
||||
if (!sleeplog.powersaving) {
|
||||
sleeplog.ess_values = [];
|
||||
sleeplog.nomocount = 0;
|
||||
sleeplog.firstnomodate = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// define restart function (also use for initial starting)
|
||||
// define function to remove the kill listener and stop the service
|
||||
// https://github.com/espruino/BangleApps/issues/1445
|
||||
stop: function() {
|
||||
E.removeListener('kill', sleeplog.stopHandler);
|
||||
sleeplog.stopHandler();
|
||||
},
|
||||
|
||||
// define function to initialy start or restart the service
|
||||
start: function() {
|
||||
// exit on missing global object
|
||||
if (!global.sleeplog) return;
|
||||
// add kill listener
|
||||
E.on('kill', sleeplog.stopHandler);
|
||||
// add health listener if defined and
|
||||
if (sleeplog.health) Bangle.on('health', sleeplog.health);
|
||||
// add acceleration listener if defined and set status to unknown
|
||||
if (sleeplog.accel) Bangle.on('accel', sleeplog.accel);
|
||||
// add kill listener
|
||||
E.on('kill', () => sleeplog.stop());
|
||||
// read log since 5min ago and restore status to last known state or unknown
|
||||
sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1]
|
||||
sleeplog.status = (require("sleeplog").readLog(0, Date.now() - 3E5)[1] || [0, 0])[1];
|
||||
// update resting according to status
|
||||
sleeplog.resting = sleeplog.status % 2;
|
||||
// write restored status to log
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 9.5 KiB |
|
|
@ -5,8 +5,8 @@ exports = {
|
|||
if (typeof global.sleeplog !== "object") return;
|
||||
|
||||
// set default logfile
|
||||
logfile = (typeof logfile === "string" && logfile.endsWith(".log")) ? logfile :
|
||||
logfile === false ? undefined : "sleeplog.log";
|
||||
if ((typeof logfile !== "string" || !logfile.endsWith(".log")) &&
|
||||
logfile !== false) logfile = "sleeplog.log";
|
||||
|
||||
// stop if enabled
|
||||
if (global.sleeplog.enabled) global.sleeplog.stop();
|
||||
|
|
@ -40,8 +40,9 @@ exports = {
|
|||
// - string // additional information
|
||||
readLog: function(logfile, since, until) {
|
||||
// check/set logfile
|
||||
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
|
||||
(global.sleeplog || {}).logfile || "sleeplog.log";
|
||||
if (typeof logfile !== "string" || !logfile.endsWith(".log")) {
|
||||
logfile = (global.sleeplog || {}).logfile || "sleeplog.log";
|
||||
}
|
||||
|
||||
// check if since is in the future
|
||||
if (since > Date()) return [];
|
||||
|
|
@ -73,8 +74,10 @@ exports = {
|
|||
// replace log with input if at least one entry like above is inside another array
|
||||
writeLog: function(logfile, input) {
|
||||
// check/set logfile
|
||||
logfile = typeof logfile === "string" && logfile.endsWith(".log") ? logfile :
|
||||
(global.sleeplog || {}).logfile || "sleeplog.log";
|
||||
if (typeof logfile !== "string" || !logfile.endsWith(".log")) {
|
||||
if (!global.sleeplog || sleeplog.logfile === false) return;
|
||||
logfile = sleeplog.logfile || "sleeplog.log";
|
||||
}
|
||||
|
||||
// check if input is an array
|
||||
if (typeof input !== "object" || typeof input.length !== "number") return;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id":"sleeplog",
|
||||
"name":"Sleep Log",
|
||||
"shortName": "SleepLog",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Log and view your sleeping habits. This app derived from SleepPhaseAlarm and uses also the principe of Estimation of Stationary Sleep-segments (ESS). It also provides a power saving mode using the built in movement calculation.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -29,11 +29,6 @@
|
|||
storage.writeJSON(filename, settings);
|
||||
}
|
||||
|
||||
// define circulate function
|
||||
function circulate(min, max, value) {
|
||||
return value > max ? min : value < min ? max : value;
|
||||
}
|
||||
|
||||
// define function to change values that need a restart of the service
|
||||
function changeRestart() {
|
||||
require("sleeplog").setEnabled(settings.enabled, settings.logfile, settings.powersaving);
|
||||
|
|
@ -49,77 +44,79 @@
|
|||
title: "Sleep Log",
|
||||
selected: selected
|
||||
},
|
||||
"< Exit": () => load(),
|
||||
"Exit": () => load(),
|
||||
"< Back": () => back(),
|
||||
"BreakTod": {
|
||||
"Break Tod": {
|
||||
value: settings.breaktod,
|
||||
step: 1,
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(0, 23, v);
|
||||
writeSetting("breaktod", v);
|
||||
}
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: v => writeSetting("breaktod", v),
|
||||
},
|
||||
"MaxAwake": {
|
||||
"Max Awake": {
|
||||
value: settings.maxawake / 6E4,
|
||||
step: 5,
|
||||
min: 15,
|
||||
max: 120,
|
||||
wrap: true,
|
||||
format: v => v + "min",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(15, 120, v);
|
||||
writeSetting("maxawake", v * 6E4);
|
||||
}
|
||||
onchange: v => writeSetting("maxawake", v * 6E4),
|
||||
},
|
||||
"MinConsec": {
|
||||
"Min Consec": {
|
||||
value: settings.minconsec / 6E4,
|
||||
step: 5,
|
||||
min: 15,
|
||||
max: 120,
|
||||
wrap: true,
|
||||
format: v => v + "min",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(15, 120, v);
|
||||
writeSetting("minconsec", v * 6E4);
|
||||
}
|
||||
onchange: v => writeSetting("minconsec", v * 6E4),
|
||||
},
|
||||
"TempThresh": {
|
||||
"Temp Thresh": {
|
||||
value: settings.tempthresh,
|
||||
step: 0.5,
|
||||
min: 20,
|
||||
max: 40,
|
||||
wrap: true,
|
||||
format: v => v + "°C",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(20, 40, v);
|
||||
writeSetting("tempthresh", v);
|
||||
}
|
||||
onchange: v => writeSetting("tempthresh", v),
|
||||
},
|
||||
"PowerSaving": {
|
||||
"Power Saving": {
|
||||
value: settings.powersaving,
|
||||
format: v => v ? "on" : "off",
|
||||
onchange: function(v) {
|
||||
settings.powersaving = v;
|
||||
changeRestart();
|
||||
showMain(7);
|
||||
// redraw menu with changed entries subsequent to onchange
|
||||
// https://github.com/espruino/Espruino/issues/2149
|
||||
setTimeout(showMain, 1, 6);
|
||||
}
|
||||
},
|
||||
"MaxMove": {
|
||||
"Max Move": {
|
||||
value: settings.maxmove,
|
||||
step: 1,
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(50, 200, v);
|
||||
writeSetting("maxmove", v);
|
||||
}
|
||||
min: 50,
|
||||
max: 200,
|
||||
wrap: true,
|
||||
onchange: v => writeSetting("maxmove", v),
|
||||
},
|
||||
"NoMoThresh": {
|
||||
"NoMo Thresh": {
|
||||
value: settings.nomothresh,
|
||||
step: 0.001,
|
||||
min: 0.006,
|
||||
max: 0.02,
|
||||
wrap: true,
|
||||
format: v => ("" + v).padEnd(5, "0"),
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(0.006, 0.02, v);
|
||||
writeSetting("nomothresh", v);
|
||||
}
|
||||
onchange: v => writeSetting("nomothresh", v),
|
||||
},
|
||||
"MinDuration": {
|
||||
"Min Duration": {
|
||||
value: Math.floor(settings.sleepthresh * stFactor),
|
||||
step: 1,
|
||||
min: 5,
|
||||
max: 15,
|
||||
wrap: true,
|
||||
format: v => v + "min",
|
||||
onchange: function(v) {
|
||||
this.value = v = circulate(5, 15, v);
|
||||
writeSetting("sleepthresh", Math.ceil(v / stFactor));
|
||||
}
|
||||
onchange: v => writeSetting("sleepthresh", Math.ceil(v / stFactor)),
|
||||
},
|
||||
"Enabled": {
|
||||
value: settings.enabled,
|
||||
|
|
@ -130,7 +127,7 @@
|
|||
}
|
||||
},
|
||||
"Logfile ": {
|
||||
value: settings.logfile === "sleeplog.log" ? true : settings.logfile.endsWith(".log") ? "custom" : false,
|
||||
value: settings.logfile === "sleeplog.log" ? true : (settings.logfile || "").endsWith(".log") ? "custom" : false,
|
||||
format: v => v === true ? "default" : v ? "custom" : "off",
|
||||
onchange: function(v) {
|
||||
if (v !== "custom") {
|
||||
|
|
@ -141,11 +138,8 @@
|
|||
}
|
||||
};
|
||||
// check power saving mode to delete unused entries
|
||||
(settings.powersaving ? ["NoMoThresh", "MinDuration"] : ["MaxMove"]).forEach(property => delete mainMenu[property]);
|
||||
(settings.powersaving ? ["NoMo Thresh", "Min Duration"] : ["Max Move"]).forEach(property => delete mainMenu[property]);
|
||||
var menu = E.showMenu(mainMenu);
|
||||
// workaround to display changed entries correct
|
||||
// https://github.com/espruino/Espruino/issues/2149
|
||||
if (selected) setTimeout(m => m.draw(), 1, menu);
|
||||
}
|
||||
|
||||
// draw main menu
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@
|
|||
0.08: New features. Added waypoints file and distance to selected waypoint display. Added integration with GPS Setup module to switch GPS to low power mode when screen off. Save display settings and restore when app restarted.
|
||||
0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight.
|
||||
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
|
||||
0.11: Now also runs on Bangle.js 2 with basic functionality
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# GPS Speed, Altimeter and Distance to Waypoint
|
||||
|
||||
You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint.
|
||||
You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint.
|
||||
|
||||
*Note for **Bangle.js 2:** Currently only the BTN3 functionality is working with the Bangle.js 2 button.*
|
||||
|
||||
Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed.
|
||||
|
||||
|
|
@ -10,6 +12,8 @@ The waypoints list is the same as that used with the [GPS Navigation](https://ba
|
|||
|
||||
BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint
|
||||
|
||||
***Bangle.js 2:** Currently only this button function is working*
|
||||
|
||||
### [A]ltitude mode
|
||||
|
||||
BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum speed/alt values recorded.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,16 @@ Mike Bennett mike[at]kereru.com
|
|||
1.01 : Third mode large clock display
|
||||
1.02 : add smoothing with kalman filter
|
||||
*/
|
||||
var v = '1.02g';
|
||||
//var v = '1.02g';
|
||||
|
||||
const BANGLEJS2 = process.env.HWVERSION==2;
|
||||
const screenH = g.getHeight();
|
||||
const screenH_Half = screenH / 2;
|
||||
const screenH_Third = screenH / 3;
|
||||
const screenH_TwoThirds = screenH * 2 / 3;
|
||||
const screenW = g.getWidth();
|
||||
const screenW_Half = screenW / 2;
|
||||
const fontFactorB2 = 2/3;
|
||||
|
||||
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
||||
var KalmanFilter = (function () {
|
||||
|
|
@ -171,10 +180,11 @@ var KalmanFilter = (function () {
|
|||
}());
|
||||
|
||||
|
||||
var buf = Graphics.createArrayBuffer(240,160,2,{msb:true});
|
||||
|
||||
// Load fonts
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
var buf = Graphics.createArrayBuffer(screenW,screenH_TwoThirds,2,{msb:true});
|
||||
|
||||
if (!BANGLEJS2)
|
||||
require("Font7x11Numeric7Seg").add(Graphics); // Load fonts
|
||||
|
||||
var lf = {fix:0,satellites:0};
|
||||
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
||||
|
|
@ -188,9 +198,10 @@ max.spd = 0;
|
|||
max.alt = 0;
|
||||
max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
|
||||
|
||||
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
|
||||
var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
|
||||
|
||||
var wp = {}; // Waypoint to use for distance from cur position.
|
||||
var SATinView = 0;
|
||||
|
||||
function nxtWp(inc){
|
||||
cfg.wp+=inc;
|
||||
|
|
@ -212,7 +223,7 @@ function radians(a) {
|
|||
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);
|
||||
|
||||
|
||||
// Distance in selected units
|
||||
var d = Math.sqrt(x*x + y*y) * 6371000;
|
||||
d = (d/parseFloat(cfg.dist)).toFixed(2);
|
||||
|
|
@ -228,41 +239,53 @@ function drawFix(dat) {
|
|||
|
||||
buf.clear();
|
||||
|
||||
var v = '';
|
||||
var v = '';
|
||||
var u='';
|
||||
|
||||
|
||||
// Primary Display
|
||||
v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
|
||||
|
||||
|
||||
// Primary Units
|
||||
u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units;
|
||||
|
||||
drawPrimary(v,u);
|
||||
|
||||
|
||||
// Secondary Display
|
||||
v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString();
|
||||
|
||||
// Secondary Units
|
||||
u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit;
|
||||
|
||||
|
||||
drawSecondary(v,u);
|
||||
|
||||
|
||||
// Time
|
||||
drawTime();
|
||||
|
||||
// Waypoint name
|
||||
drawWP();
|
||||
|
||||
|
||||
//Sats
|
||||
if ( dat.age > 10 ) {
|
||||
if ( dat.age > 90 ) dat.age = '>90';
|
||||
drawSats('Age:'+dat.age);
|
||||
}
|
||||
else drawSats('Sats:'+dat.sats);
|
||||
|
||||
|
||||
/* else if (!BANGLEJS2) {
|
||||
drawSats('Sats:'+dat.sats);
|
||||
} else {
|
||||
if (lf.fix) {
|
||||
if(emulator)console.log("fix "+lf.fix);
|
||||
drawSats('Sats:'+dat.sats);
|
||||
} else {
|
||||
if(emulator)console.log("inView: "+SATinView);
|
||||
drawSats('View:' + SATinView);
|
||||
}
|
||||
}
|
||||
*/
|
||||
g.reset();
|
||||
g.drawImage(img,0,40);
|
||||
|
||||
|
||||
}
|
||||
|
||||
function drawClock() {
|
||||
|
|
@ -275,125 +298,143 @@ function drawClock() {
|
|||
}
|
||||
|
||||
function drawPrimary(n,u) {
|
||||
|
||||
if(emulator)console.log("drawPrimary: " + n +" "+ u);
|
||||
// Primary Display
|
||||
|
||||
|
||||
var s=40; // Font size
|
||||
var l=n.length;
|
||||
|
||||
|
||||
if ( l <= 7 ) s=48;
|
||||
if ( l <= 6 ) s=55;
|
||||
if ( l <= 5 ) s=66;
|
||||
if ( l <= 4 ) s=85;
|
||||
if ( l <= 3 ) s=110;
|
||||
|
||||
buf.setFontAlign(0,-1); //Centre
|
||||
buf.setColor(1);
|
||||
|
||||
buf.setFontAlign(0,-1); //Centre
|
||||
buf.setColor(1);
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
buf.setFontVector(s);
|
||||
buf.drawString(n,110,0);
|
||||
|
||||
|
||||
|
||||
// Primary Units
|
||||
buf.drawString(n,screenW_Half-10,0);
|
||||
|
||||
// Primary Units
|
||||
s = 35; // Font size
|
||||
buf.setFontAlign(1,-1,3); //right
|
||||
buf.setColor(2);
|
||||
buf.setFontVector(35);
|
||||
buf.drawString(u,210,0);
|
||||
buf.setColor(2);
|
||||
if (BANGLEJS2) s = 20;
|
||||
buf.setFontVector(s);
|
||||
buf.drawString(u,screenW-30,0);
|
||||
}
|
||||
|
||||
function drawSecondary(n,u) {
|
||||
|
||||
var s=180; // units X position
|
||||
if(emulator)console.log("drawSecondary: " + n +" "+ u);
|
||||
var xu = 180; // units X position
|
||||
var l=n.length;
|
||||
if ( l <= 5 ) s=155;
|
||||
if ( l <= 4 ) s=125;
|
||||
if ( l <= 3 ) s=100;
|
||||
if ( l <= 2 ) s=65;
|
||||
if ( l <= 1 ) s=35;
|
||||
|
||||
buf.setFontAlign(-1,1); //left, bottom
|
||||
buf.setColor(1);
|
||||
buf.setFontVector(45);
|
||||
buf.drawString(n,5,140);
|
||||
|
||||
if ( l <= 5 ) xu = 155;
|
||||
if ( l <= 4 ) xu = 125;
|
||||
if ( l <= 3 ) xu = 100;
|
||||
if ( l <= 2 ) xu = 65;
|
||||
if ( l <= 1 ) xu = 35;
|
||||
|
||||
buf.setFontAlign(-1,1); //left, bottom
|
||||
buf.setColor(1);
|
||||
var s = 45; // Font size
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
buf.setFontVector(s);
|
||||
buf.drawString(n,5,screenH_TwoThirds-20);
|
||||
|
||||
// Secondary Units
|
||||
buf.setFontAlign(-1,1); //left, bottom
|
||||
buf.setColor(2);
|
||||
buf.setFontVector(30);
|
||||
buf.drawString(u,s,135);
|
||||
|
||||
buf.setColor(2);
|
||||
s = 30; // Font size
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
buf.setFontVector(s);
|
||||
buf.drawString(u,xu - (BANGLEJS2*20),screenH_TwoThirds-25);
|
||||
}
|
||||
|
||||
function drawTime() {
|
||||
var x, y;
|
||||
|
||||
|
||||
if ( cfg.modeA == 2 ) {
|
||||
x=120;
|
||||
y=0;
|
||||
buf.setFontAlign(0,-1);
|
||||
buf.setFontVector(80);
|
||||
x = screenW_Half;
|
||||
y = 0;
|
||||
buf.setFontAlign(0,-1);
|
||||
buf.setFontVector(screenH_Third);
|
||||
}
|
||||
else {
|
||||
x = 0;
|
||||
y = 160;
|
||||
y = screenH_TwoThirds;
|
||||
buf.setFontAlign(-1,1);
|
||||
if (!BANGLEJS2)
|
||||
buf.setFont("7x11Numeric7Seg", 2);
|
||||
else
|
||||
buf.setFont("6x8", 2);
|
||||
}
|
||||
|
||||
|
||||
buf.setColor(0);
|
||||
buf.drawString(time,x,y);
|
||||
time = require("locale").time(new Date(),1);
|
||||
buf.setColor(3);
|
||||
buf.setColor(3);
|
||||
buf.drawString(time,x,y);
|
||||
}
|
||||
|
||||
function drawWP() {
|
||||
function drawWP() { // from waypoints.json - see README.md
|
||||
var nm = wp.name;
|
||||
if ( nm == undefined || nm == 'NONE' || cfg.modeA ==1 ) nm = '';
|
||||
buf.setColor(2);
|
||||
|
||||
if (emulator) nm="waypoint";
|
||||
buf.setColor(2);
|
||||
var s = 20; // Font size
|
||||
|
||||
if ( cfg.modeA == 0 ) { // dist mode
|
||||
if(emulator)console.log("drawWP() 0: "+nm);
|
||||
buf.setFontAlign(-1,1); //left, bottom
|
||||
buf.setFontVector(20);
|
||||
buf.drawString(nm.substring(0,6),72,160);
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
buf.setFontVector(s);
|
||||
buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 20));
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 2 ) { // clock/large mode
|
||||
if(emulator)console.log("drawWP() 2: "+nm);
|
||||
s = 55; // Font size
|
||||
buf.setFontAlign(0,1); //left, bottom
|
||||
buf.setFontVector(55);
|
||||
buf.drawString(nm.substring(0,6),120,160);
|
||||
if (BANGLEJS2) s *= fontFactorB2;
|
||||
buf.setFontVector(s);
|
||||
buf.drawString(nm.substring(0,6),screenW_Half,screenH_TwoThirds-(BANGLEJS2 * 20));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function drawSats(sats) {
|
||||
|
||||
buf.setColor(3);
|
||||
buf.setColor(3);
|
||||
buf.setFont("6x8", 2);
|
||||
buf.setFontAlign(1,1); //right, bottom
|
||||
buf.drawString(sats,240,160);
|
||||
buf.drawString(sats,screenW,screenH_TwoThirds);
|
||||
|
||||
s = 30; // Font size
|
||||
if (BANGLEJS2) s = 18;
|
||||
buf.setFontVector(s);
|
||||
buf.setColor(2);
|
||||
|
||||
buf.setFontVector(30);
|
||||
buf.setColor(2);
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
buf.drawString('A',240,140);
|
||||
buf.drawString('A',screenW,140-(BANGLEJS2 * 40));
|
||||
if ( showMax ) {
|
||||
buf.setFontAlign(0,1); //centre, bottom
|
||||
buf.drawString('MAX',120,164);
|
||||
}
|
||||
}
|
||||
if ( cfg.modeA == 0 ) buf.drawString('D',240,140);
|
||||
if ( cfg.modeA == 0 ) buf.drawString('D',screenW,140-(BANGLEJS2 * 40));
|
||||
}
|
||||
|
||||
function onGPS(fix) {
|
||||
|
||||
|
||||
if ( emulator ) {
|
||||
fix.fix = 1;
|
||||
fix.speed = 10 + (Math.random()*5);
|
||||
fix.alt = 354 + (Math.random()*50);
|
||||
fix.lat = -38.92;
|
||||
fix.lon = 175.7613350;
|
||||
fix.lon = 175.7613350;
|
||||
fix.course = 245;
|
||||
fix.satellites = 12;
|
||||
fix.time = new Date();
|
||||
|
|
@ -402,7 +443,7 @@ function onGPS(fix) {
|
|||
|
||||
var m;
|
||||
|
||||
var sp = '---';
|
||||
var sp = '---';
|
||||
var al = '---';
|
||||
var di = '---';
|
||||
var age = '---';
|
||||
|
|
@ -411,6 +452,8 @@ function onGPS(fix) {
|
|||
|
||||
if (lf.fix) {
|
||||
|
||||
// if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw);
|
||||
|
||||
// Smooth data
|
||||
if ( lf.smoothed !== 1 ) {
|
||||
if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
|
||||
|
|
@ -418,8 +461,8 @@ function onGPS(fix) {
|
|||
lf.smoothed = 1;
|
||||
if ( max.n <= 15 ) max.n++;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Speed
|
||||
if ( cfg.spd == 0 ) {
|
||||
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
|
||||
|
|
@ -427,7 +470,7 @@ function onGPS(fix) {
|
|||
cfg.spd_unit = m[2];
|
||||
}
|
||||
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
||||
|
||||
|
||||
if ( sp < 10 ) sp = sp.toFixed(1);
|
||||
else sp = Math.round(sp);
|
||||
if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
|
||||
|
|
@ -444,9 +487,9 @@ function onGPS(fix) {
|
|||
// Age of last fix (secs)
|
||||
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
|
||||
}
|
||||
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
if ( showMax )
|
||||
if ( showMax )
|
||||
drawFix({
|
||||
speed:max.spd,
|
||||
sats:lf.satellites,
|
||||
|
|
@ -455,7 +498,7 @@ function onGPS(fix) {
|
|||
age:age,
|
||||
fix:lf.fix
|
||||
}); // Speed and alt maximums
|
||||
else
|
||||
else
|
||||
drawFix({
|
||||
speed:sp,
|
||||
sats:lf.satellites,
|
||||
|
|
@ -467,7 +510,7 @@ function onGPS(fix) {
|
|||
}
|
||||
if ( cfg.modeA == 0 ) {
|
||||
// Show speed/distance
|
||||
if ( di <= 0 )
|
||||
if ( di <= 0 )
|
||||
drawFix({
|
||||
speed:sp,
|
||||
sats:lf.satellites,
|
||||
|
|
@ -476,7 +519,7 @@ function onGPS(fix) {
|
|||
age:age,
|
||||
fix:lf.fix
|
||||
}); // No WP selected
|
||||
else
|
||||
else
|
||||
drawFix({
|
||||
speed:sp,
|
||||
sats:lf.satellites,
|
||||
|
|
@ -494,7 +537,7 @@ function onGPS(fix) {
|
|||
}
|
||||
|
||||
function setButtons(){
|
||||
|
||||
if (!BANGLEJS2) { // Buttons for Bangle.js
|
||||
// Spd+Dist : Select next waypoint
|
||||
setWatch(function(e) {
|
||||
var dur = e.time - e.lastTime;
|
||||
|
|
@ -506,10 +549,10 @@ function setButtons(){
|
|||
else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint
|
||||
onGPS(lf);
|
||||
}, BTN1, { edge:"falling",repeat:true});
|
||||
|
||||
// Power saving on/off
|
||||
|
||||
// Power saving on/off
|
||||
setWatch(function(e){
|
||||
pwrSav=!pwrSav;
|
||||
pwrSav=!pwrSav;
|
||||
if ( pwrSav ) {
|
||||
LED1.reset();
|
||||
var s = require('Storage').readJSON('setting.json',1)||{};
|
||||
|
|
@ -522,15 +565,15 @@ function setButtons(){
|
|||
LED1.set();
|
||||
}
|
||||
}, BTN2, {repeat:true,edge:"falling"});
|
||||
|
||||
|
||||
// Toggle between alt or dist
|
||||
setWatch(function(e){
|
||||
cfg.modeA = cfg.modeA+1;
|
||||
if ( cfg.modeA > 2 ) cfg.modeA = 0;
|
||||
savSettings();
|
||||
onGPS(lf);
|
||||
onGPS(lf);
|
||||
}, BTN3, {repeat:true,edge:"falling"});
|
||||
|
||||
|
||||
// Touch left screen to toggle display
|
||||
setWatch(function(e){
|
||||
cfg.primSpd = !cfg.primSpd;
|
||||
|
|
@ -538,11 +581,42 @@ function setButtons(){
|
|||
onGPS(lf); // Update display
|
||||
}, BTN4, {repeat:true,edge:"falling"});
|
||||
|
||||
} else { // Buttons for Bangle.js 2
|
||||
setWatch(function(e){ // Bangle.js BTN3
|
||||
cfg.modeA = cfg.modeA+1;
|
||||
if ( cfg.modeA > 2 ) cfg.modeA = 0;
|
||||
if(emulator)console.log("cfg.modeA="+cfg.modeA);
|
||||
savSettings();
|
||||
onGPS(lf);
|
||||
}, BTN1, {repeat:true,edge:"falling"});
|
||||
|
||||
/* Bangle.on('tap', function(data) { // data - {dir, double, x, y, z}
|
||||
cfg.primSpd = !cfg.primSpd;
|
||||
if(emulator)console.log("!cfg.primSpd");
|
||||
}); */
|
||||
|
||||
/* Bangle.on('swipe', function(dir) {
|
||||
if (dir < 0) { // left: Bangle.js BTN3
|
||||
cfg.modeA = cfg.modeA+1;
|
||||
if ( cfg.modeA > 2 ) cfg.modeA = 0;
|
||||
if(emulator)console.log("cfg.modeA="+cfg.modeA);
|
||||
}
|
||||
else
|
||||
{ // right: Bangle.js BTN4
|
||||
cfg.primSpd = !cfg.primSpd;
|
||||
if(emulator)console.log("!cfg.primSpd");
|
||||
}
|
||||
});
|
||||
*/
|
||||
savSettings();
|
||||
onGPS(lf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateClock() {
|
||||
if (!canDraw) return;
|
||||
drawTime();
|
||||
drawTime();
|
||||
g.reset();
|
||||
g.drawImage(img,0,40);
|
||||
if ( emulator ) {max.spd++;max.alt++;}
|
||||
|
|
@ -573,21 +647,21 @@ function setLpMode(m) {
|
|||
|
||||
// =Main Prog
|
||||
|
||||
// Read settings.
|
||||
// Read settings.
|
||||
let cfg = require('Storage').readJSON('speedalt.json',1)||{};
|
||||
|
||||
cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit
|
||||
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
|
||||
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
|
||||
cfg.spd_unit = cfg.spd_unit||'km/h'; // Displayed speed unit
|
||||
cfg.alt = cfg.alt||1;// Multiplier for altitude unit conversions. (feet:'0.3048')
|
||||
cfg.alt_unit = cfg.alt_unit||'meter'; // Displayed altitude units ('feet')
|
||||
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
|
||||
cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units
|
||||
cfg.colour = cfg.colour||0; // Colour scheme.
|
||||
cfg.wp = cfg.wp||0; // Last selected waypoint for dist
|
||||
cfg.modeA = cfg.modeA||0; // 0 = [D]ist, 1 = [A]ltitude, 2 = [C]lock
|
||||
cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
|
||||
cfg.modeA = cfg.modeA||1; // 0 = [D]ist, 1 = [A]ltitude, 2 = [C]lock
|
||||
cfg.primSpd = cfg.primSpd||1; // 1 = Spd in primary, 0 = Spd in secondary
|
||||
|
||||
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
||||
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
||||
cfg.altFilt = cfg.altFilt==undefined?true:cfg.altFilt;
|
||||
|
||||
if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
|
||||
|
|
@ -602,29 +676,43 @@ Colour Pallet Idx
|
|||
2 : Units
|
||||
3 : Sats
|
||||
*/
|
||||
const background = 0; // g.theme.bg = 0xFFFF = gelb!?
|
||||
var img = {
|
||||
width:buf.getWidth(),
|
||||
height:buf.getHeight(),
|
||||
bpp:2,
|
||||
buffer:buf.buffer,
|
||||
palette:new Uint16Array([0,0x4FE0,0xEFE0,0x07DB])
|
||||
palette:new Uint16Array([background,0x4FE0,0xEFE0,0x07DB]) // "Default"
|
||||
};
|
||||
|
||||
if ( cfg.colour == 1 ) img.palette = new Uint16Array([0,0xFFFF,0xFFF6,0xDFFF]);
|
||||
if ( cfg.colour == 2 ) img.palette = new Uint16Array([0,0xFF800,0xFAE0,0xF813]);
|
||||
if ( cfg.colour == 1 ) img.palette = new Uint16Array([background,0xFFFF,0xFFF6,0xDFFF]); // "Hi contrast"
|
||||
if ( cfg.colour == 2 ) img.palette = new Uint16Array([background,0xFF800,0xFAE0,0xF813]); // "Night"
|
||||
|
||||
var SCREENACCESS = {
|
||||
withApp:true,
|
||||
request:function(){this.withApp=false;stopDraw();},
|
||||
release:function(){this.withApp=true;startDraw();}
|
||||
};
|
||||
};
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
if (!SCREENACCESS.withApp) return;
|
||||
if (on) startDraw();
|
||||
if (on) startDraw();
|
||||
else stopDraw();
|
||||
});
|
||||
|
||||
/*
|
||||
function onGPSraw(nmea) {
|
||||
var nofGP = 0, nofBD = 0, nofGL = 0;
|
||||
if (nmea.slice(3,6) == "GSV") {
|
||||
// console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
|
||||
if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
|
||||
SATinView = nofGP + nofBD + nofGL;
|
||||
} }
|
||||
if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
|
||||
*/
|
||||
|
||||
var gpssetup;
|
||||
try {
|
||||
gpssetup = require("gpssetup");
|
||||
|
|
@ -634,8 +722,6 @@ try {
|
|||
|
||||
// All set up. Lets go.
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
onGPS(lf);
|
||||
Bangle.setGPSPower(1);
|
||||
|
||||
|
|
@ -650,3 +736,5 @@ Bangle.on('GPS', onGPS);
|
|||
|
||||
setButtons();
|
||||
setInterval(updateClock, 10000);
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
"id": "speedalt",
|
||||
"name": "GPS Adventure Sports",
|
||||
"shortName": "GPS Adv Sport",
|
||||
"version": "0.10",
|
||||
"version": "0.11",
|
||||
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,outdoors",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@
|
|||
'': {'title': 'Units'},
|
||||
'< Back': function() { E.showMenu(appMenu); },
|
||||
'default (spd)' : function() { setUnits(0,''); },
|
||||
'Kph (spd)' : function() { setUnits(1,'kph'); },
|
||||
'km/h (spd)' : function() { setUnits(1,'km/h'); },
|
||||
'Knots (spd)' : function() { setUnits(1.852,'kts'); },
|
||||
'Mph (spd)' : function() { setUnits(1.60934,'mph'); },
|
||||
'm/s (spd)' : function() { setUnits(3.6,'m/s'); },
|
||||
'Km (dist)' : function() { setUnitsDist(1000,'km'); },
|
||||
'Miles (dist)' : function() { setUnitsDist(1609.344,'mi'); },
|
||||
'Nm (dist)' : function() { setUnitsDist(1852.001,'nm'); },
|
||||
'Meters (alt)' : function() { setUnitsAlt(1,'m'); },
|
||||
'Meters (alt)' : function() { setUnitsAlt(1,'meter'); },
|
||||
'Feet (alt)' : function() { setUnitsAlt(0.3048,'ft'); }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@
|
|||
1.10: Adds Kalman filter.
|
||||
1.14: Add VMG and coordinates screens
|
||||
1.43: Adds mirroring of the watch face to an Android device. See README.md
|
||||
1.48: Droidscript mirroring prog automatically uses last connection address. Auto connects when run.
|
||||
|
|
|
|||
|
|
@ -3,27 +3,66 @@
|
|||
|
||||
app.LoadPlugin("PuckJS");
|
||||
|
||||
app.SetDebugEnabled(true);
|
||||
|
||||
|
||||
v = '1.52' // Version of this script
|
||||
requiredBangleVer = '1.47'; // Minimum speedalt2 version required on Bangle
|
||||
curBangleVer = '-.--'
|
||||
isStopped = true; // Data receive turned off
|
||||
lastData = new Date().getTime() / 1000; // Time of last data received
|
||||
|
||||
//Colours
|
||||
// Mode = 0 // 0=SPD, 1=ALT, 2=DST, 3=VMG, 4=POSN, 5=TIME
|
||||
btnOff = '#175A63'
|
||||
btnOn = '#4285F4'
|
||||
col = new Array(['black'],['#64FF00'],['#FCFA00'],['#00E4FF']) // bg, main, units, wp - 0xFFFF,0x007F,0x0054,0x0054
|
||||
|
||||
// Settings
|
||||
conf = JSON.parse( app.LoadText( "settings", "{}" ))
|
||||
if ( typeof(conf.btAddr) != 'string' ) conf.btAddr = '' // Address of last connection
|
||||
if ( typeof(conf.btName) != 'string' ) conf.btName = '' // Name of last connection
|
||||
|
||||
// Extend PuckJS
|
||||
app.CreateBangleJS = function( options )
|
||||
{
|
||||
return new BangleJS( options );
|
||||
}
|
||||
|
||||
class BangleJS extends PuckJS {
|
||||
|
||||
constructor(options) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Called when application is started.
|
||||
function OnStart() {
|
||||
|
||||
v = '1.49' // Version of this script
|
||||
requiredBangleVer = '1.46'; // Minimum speedalt2 version required on Bangle
|
||||
curBangleVer = '-.--'
|
||||
isStopped = true; // Data receive turned off
|
||||
lastData = new Date().getTime() / 1000; // Time of last data received
|
||||
addr = ''; // Address of last connection
|
||||
|
||||
// Mode = 0 // 0=SPD, 1=ALT, 2=DST, 3=VMG, 4=POSN, 5=TIME
|
||||
btnOff = '#175A63'
|
||||
btnOn = '#4285F4'
|
||||
col = new Array(['black'],['#64FF00'],['#FCFA00'],['#00E4FF']) // bg, main, units, wp - 0xFFFF,0x007F,0x0054,0x0054
|
||||
|
||||
// Connect to Bangle
|
||||
puck = app.CreatePuckJS();
|
||||
puck.SetOnConnect(onConnect); // Callback.
|
||||
puck.SetOnReceive(readResponse); // Callback to capture console output from app.
|
||||
puck.Scan("Bangle");
|
||||
if( !app.IsBluetoothEnabled() ) app.SetBluetoothEnabled( true );
|
||||
|
||||
// puck = app.CreatePuckJS();
|
||||
bngl = app.CreateBangleJS();
|
||||
bngl.SetOnConnect(onConnect); // Callback.
|
||||
bngl.SetOnReceive(readResponse); // Callback to capture console output from app.
|
||||
|
||||
|
||||
if ( conf.btAddr == '' ) {
|
||||
console.log('Scanning')
|
||||
bngl.Scan("Bangle");
|
||||
}
|
||||
else {
|
||||
console.log('Reconnecting to : '+conf.btAddr)
|
||||
bngl.address = conf.btAddr
|
||||
bngl.name = conf.btName
|
||||
bngl.Connect(bngl.address);
|
||||
}
|
||||
|
||||
app.SetDebugEnabled(false);
|
||||
|
||||
setInterval(checkConnection,5000) // Periodic check for data timeout and attempt a reconnect
|
||||
|
||||
// Controls
|
||||
|
|
@ -198,7 +237,7 @@ function readResponse(data) {
|
|||
|
||||
if (d.m == 5) { // Time
|
||||
val.SetTextSize(90)
|
||||
val2.SetTextSize(10)
|
||||
val2.SetTextSize(0)
|
||||
|
||||
dt = new Date();
|
||||
|
||||
|
|
@ -222,10 +261,16 @@ function setLED(canvas,on,colour) {
|
|||
canvas.Update()
|
||||
}
|
||||
|
||||
function onConnect(name, address, bonded, rssi) {
|
||||
addr = address
|
||||
console.log( "Connected to " + address );
|
||||
function onConnect(name, address) {
|
||||
// app.SetDebugEnabled(true)
|
||||
|
||||
if ( typeof(address) == 'string' ) conf.btAddr = address
|
||||
if ( typeof(name) == 'string' ) conf.btName = name
|
||||
app.SaveText( "settings", JSON.stringify( conf )) // persist connection for future so no need to scan each time used.
|
||||
|
||||
console.log( "Connected to : " + conf.btAddr );
|
||||
btn_OnStart() // Once connect tell app to start sending updates
|
||||
app.SetDebugEnabled(false)
|
||||
}
|
||||
|
||||
// Periodic check for data timeout and attempt a reconnect
|
||||
|
|
@ -233,30 +278,33 @@ function checkConnection() {
|
|||
if (isStopped) return
|
||||
if ( parseFloat(new Date().getTime() / 1000) - lastData > 30 ) {
|
||||
|
||||
console.log( "Reconnecting to : "+addr);
|
||||
console.log( "Reconnecting to : "+conf.btAddr);
|
||||
|
||||
// Flash orange 'led' indicator for connection attempts.
|
||||
setLED(led,true,"#FC8A00")
|
||||
setTimeout(function() {setLED(led,false,-1)}, 500)
|
||||
puck.Connect(addr)
|
||||
bngl.Connect(conf.btAddr)
|
||||
}
|
||||
}
|
||||
|
||||
function btn_OnAbout() {
|
||||
app.ShowPopup("GPS Adv Sports II\nAndroid Mirror : "+v+"\nBangle.js : "+curBangleVer,"Long")
|
||||
app.ShowPopup(
|
||||
"GPS Adv Sports II\nAndroid Mirror : "+v+
|
||||
"\nBangle.js : "+curBangleVer+
|
||||
"\nConnected : "+conf.btName,"Long")
|
||||
}
|
||||
|
||||
function btn_OnStart() {
|
||||
btnStart.SetBackColor(btnOn)
|
||||
btnStop.SetBackColor(btnOff)
|
||||
puck.SendCode('btOn(1)\n') // Enable the data send
|
||||
bngl.SendCode('btOn(1)\n') // Enable the data send
|
||||
isStopped = false
|
||||
}
|
||||
|
||||
function btn_OnStop() {
|
||||
btnStart.SetBackColor(btnOff)
|
||||
btnStop.SetBackColor(btnOn)
|
||||
puck.SendCode('btOn(0)\n') // Disable the data send
|
||||
bngl.SendCode('btOn(0)\n') // Disable the data send
|
||||
isStopped = true
|
||||
val.SetText('')
|
||||
val2.SetText('')
|
||||
|
|
@ -268,6 +316,5 @@ function btn_OnStop() {
|
|||
function btn_OnScan() {
|
||||
btnStart.SetBackColor(btnOff)
|
||||
btnStop.SetBackColor(btnOff)
|
||||
puck.Scan("Bangle");
|
||||
bngl.Scan("Bangle");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ Mike Bennett mike[at]kereru.com
|
|||
1.34 : Add bluetooth data stream for Droidscript
|
||||
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
|
||||
*/
|
||||
var v = '1.46';
|
||||
var vDroid = '1.46'; // Required DroidScript program version
|
||||
var v = '1.49';
|
||||
var vDroid = '1.50'; // Required DroidScript program version
|
||||
|
||||
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
||||
var KalmanFilter = (function () {
|
||||
|
|
@ -175,15 +175,17 @@ var KalmanFilter = (function () {
|
|||
|
||||
var buf = Graphics.createArrayBuffer(240,160,2,{msb:true});
|
||||
|
||||
|
||||
let LED = // LED as minimal and only definition (as instance / singleton)
|
||||
{ isOn: false // status on / off, not needed if you don't need to ask for it
|
||||
, set: function(v) { // turn on w/ no arg or truey, else off
|
||||
g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(40,10,10); }
|
||||
g.setColor((this.isOn=(v===undefined||!!v))?1:0,0,0).fillCircle(120,10,10); }
|
||||
, reset: function() { this.set(false); } // turn off
|
||||
, write: function(v) { this.set(v); } // turn on w/ no arg or truey, else off
|
||||
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
|
||||
}, LED1 = LED; // LED1 as 'synonym' for LED
|
||||
|
||||
|
||||
var lf = {fix:0,satellites:0};
|
||||
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
||||
var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
|
||||
|
|
@ -645,7 +647,7 @@ function btSend(dat) {
|
|||
var dur = getTime() - btLast;
|
||||
if ( dur < 1.0 ) return; // Don't need to transmit more than every 1.0 secs.
|
||||
btLast = getTime();
|
||||
console.log(JSON.stringify(dat)); // transmit the data
|
||||
Bluetooth.println(JSON.stringify(dat)); // transmit the data
|
||||
}
|
||||
|
||||
// == Events
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "speedalt2",
|
||||
"name": "GPS Adventure Sports II",
|
||||
"shortName":"GPS Adv Sport II",
|
||||
"version":"1.46",
|
||||
"version":"1.49",
|
||||
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
0.01: Initial creation of the clock face time and calendar
|
||||
0.02: Feature Request #1154 and some findings...
|
||||
-> get rendered time from optimisations
|
||||
-> *BATT SAFE* only update once a minute instead of once a second
|
||||
-> *RAM optimized* clean code, corrected minute update (timout, no intervall)
|
||||
-> locale: weekday name (first two characters) from locale
|
||||
-> added settings to render cal view begin day (-1: today, 0:sunday, 1:monday [default])
|
||||
0.03: a lot of more settings for outline, colors and highlights
|
||||
0.04: finalized README, fixed settings cancel, fixed border-setting
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Calendar Clock
|
||||
|
||||
## Features
|
||||
Shows the
|
||||
* Date
|
||||
* Time (hh:mm) - respecting 12/24 (uses locale string)
|
||||
* 3 weeks calendar view (last,current and next week)
|
||||
|
||||
### The settings menu
|
||||
Calendar View can be customized
|
||||
* < Save: Exist and save the current settings
|
||||
* Show date: Choose if and how the date is displayed: none, locale (default), monthfull or monthshort.yearshort #weeknum with 0 prefixed
|
||||
* Start wday: Set day of week start. Values: 0=Sunday, 1=Monday,...,6=Saturday or -1=Relative to today (default 0: Sunday)
|
||||
* Su color: Set Sundays color. Values: none (default), red, green or blue
|
||||
* Border: show or none (default)
|
||||
* Submenu Today settings - choose how today is highlighted
|
||||
* < Back:
|
||||
* Color: none, red (default), green or blue
|
||||
* Marker: Outline today graphically. Values: none (default), circle, rect(angle)
|
||||
* Mrk.Color: Circle/rectangle color: red (default), green or blue
|
||||
* Mrk.Size: Circle/rectangle thickness in pixel: min:1, max: 10, default:3
|
||||
* < Cancel: Exit and no change. Nevertheless missing default settings and superflous settings will be removed and saved.
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
{ "id": "timecal",
|
||||
"name": "TimeCal",
|
||||
"shortName":"TimeCal",
|
||||
"version":"0.04",
|
||||
"description": "TimeCal shows the date/time along with a 3 week calendar",
|
||||
"icon": "icon.png",
|
||||
"version":"0.01",
|
||||
"description": "TimeCal shows the Time along with a 3 week calendar",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"tags": "clock,calendar",
|
||||
"supports":["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator":true,
|
||||
"storage": [
|
||||
{"name":"timecal.app.js","url":"timecal.app.js"}
|
||||
{"name":"timecal.app.js","url":"timecal.app.js"},
|
||||
{"name":"timecal.settings.js","url":"timecal.settings.js"}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,798 @@
|
|||
//Clock renders date, time and pre,current,next week calender view
|
||||
class TimeCalClock{
|
||||
DATE_FONT_SIZE(){ return 20; }
|
||||
TIME_FONT_SIZE(){ return 40; }
|
||||
|
||||
/**
|
||||
* @param{Date} date optional the date (e.g. for testing)
|
||||
* @param{Settings} settings optional settings to use e.g. for testing
|
||||
*/
|
||||
constructor(date, settings){
|
||||
if (date)
|
||||
this.date=date;
|
||||
|
||||
if (settings)
|
||||
this._settings = settings;
|
||||
else
|
||||
this._settings = require("Storage").readJSON("timecal.settings.json", 1) || {};
|
||||
|
||||
const defaults = {
|
||||
shwDate:1, //0:none, 1:locale, 2:month, 3:monthshort.year #week
|
||||
|
||||
wdStrt:0, //identical to getDay() 0->Su, 1->Mo, ... //Issue #1154: weekstart So/Mo, -1 for use today
|
||||
|
||||
tdyNumClr:3, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
tdyMrkr:0, //0:none, 1:circle, 2:rectangle, 3:filled
|
||||
tdyMrkClr:2, //1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
tdyMrkPxl:3, //px
|
||||
|
||||
suClr:1, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
//phColor:"#E00", //public holiday
|
||||
|
||||
calBrdr:false
|
||||
};
|
||||
for (const k in this._settings) if (!defaults.hasOwnProperty(k)) delete this._settings[k]; //remove invalid settings
|
||||
for (const k in defaults) if(!this._settings.hasOwnProperty(k)) this._settings[k] = defaults[k]; //assign missing defaults
|
||||
|
||||
g.clear();
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
this.centerX = Bangle.appRect.w/2;
|
||||
this.nrgb = [g.theme.fg, "#E00", "#0E0", "#00E"]; //fg, r ,g , b
|
||||
|
||||
this.ABR_DAY=[];
|
||||
if (require("locale") && require("locale").dow)
|
||||
for (let d=0; d<=6; d++) {
|
||||
var refDay=new Date();
|
||||
refDay.setFullYear(1972);
|
||||
refDay.setMonth(0);
|
||||
refDay.setDate(2+d);
|
||||
this.ABR_DAY.push(require("locale").dow(refDay));
|
||||
|
||||
}
|
||||
else
|
||||
this.ABR_DAY=["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object} current settings object
|
||||
*/
|
||||
settings(){
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Run forest run
|
||||
**/
|
||||
draw(){
|
||||
this.drawTime();
|
||||
|
||||
if (this.TZOffset===undefined || this.TZOffset!==d.getTimezoneOffset())
|
||||
this.drawDateAndCal();
|
||||
}
|
||||
|
||||
/**
|
||||
* draw given or current time from date
|
||||
* overwatch timezone changes
|
||||
* schedules itself to update
|
||||
*/
|
||||
drawTime(){
|
||||
d=this.date ? this.date : new Date();
|
||||
const Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10;
|
||||
|
||||
d=d?d :new Date();
|
||||
|
||||
g.setFontAlign(0, -1).setFont("Vector", this.TIME_FONT_SIZE()).setColor(g.theme.fg)
|
||||
.clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7)
|
||||
.drawString(("0" + require("locale").time(d, 1)).slice(-5), this.centerX, Y, true);
|
||||
//.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); //DEV-Option
|
||||
|
||||
setTimeout(this.draw.bind(this), 60000-(d.getSeconds()*1000)-d.getMilliseconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* draws given date and cal
|
||||
* @param{Date} d provide date or uses today
|
||||
*/
|
||||
drawDateAndCal(){
|
||||
d=this.date ? this.date : new Date();
|
||||
|
||||
this.TZOffset=d.getTimezoneOffset();
|
||||
this.drawDate();
|
||||
this.drawCal();
|
||||
|
||||
if (this.tOutD) //abort exisiting
|
||||
clearTimeout(this.tOutD);
|
||||
this.tOutD=setTimeout(this.drawDateAndCal.bind(this), 86400000-(d.getHours()*24*60*1000)-(d.getMinutes()*60*1000)-d.getSeconds()-d.getMilliseconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* draws given date as defiend in settings
|
||||
*/
|
||||
drawDate(){
|
||||
d=this.date ? this.date : new Date();
|
||||
|
||||
const FONT_SIZE=20;
|
||||
const Y=Bangle.appRect.y;
|
||||
var render=false;
|
||||
var dateStr = "";
|
||||
if (this.settings().shwDate>0) { //skip if exactly -none
|
||||
const dateSttngs = ["","l","M","m.Y #W"];
|
||||
for (let c of dateSttngs[this.settings().shwDate]) { //add part as configured
|
||||
switch (c){
|
||||
case "l":{ //locale
|
||||
render=true;
|
||||
dateStr+=require("locale").date(d,1);
|
||||
break;
|
||||
}
|
||||
case "m":{ //month e.g. Jan.
|
||||
render=true;
|
||||
dateStr+=require("locale").month(d,1);
|
||||
break;
|
||||
}
|
||||
case "M":{ //month e.g. January
|
||||
render=true;
|
||||
dateStr+=require("locale").month(d,0);
|
||||
break;
|
||||
}
|
||||
case "y":{ //year e.g. 22
|
||||
render=true;
|
||||
dateStr+=d.getFullYear().slice(-2);
|
||||
break;
|
||||
}
|
||||
case "Y":{ //year e.g. 2022
|
||||
render=true;
|
||||
dateStr+=d.getFullYear();
|
||||
break;
|
||||
}
|
||||
case "w":{ //week e.g. #2
|
||||
dateStr+=(this.ISO8601calWeek(d));
|
||||
break;
|
||||
}
|
||||
case "W":{ //week e.g. #02
|
||||
dateStr+=("0"+this.ISO8601calWeek(d)).slice(-2);
|
||||
break;
|
||||
}
|
||||
default: //append c
|
||||
dateStr+=c;
|
||||
render=dateStr.length>0;
|
||||
break; //noop
|
||||
}
|
||||
}
|
||||
}
|
||||
if (render){
|
||||
g.setFont("Vector", FONT_SIZE).setColor(g.theme.fg).setFontAlign(0, -1).clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3).drawString(dateStr,this.centerX,Y);
|
||||
}
|
||||
//g.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); //DEV-Option
|
||||
}
|
||||
|
||||
/**
|
||||
* draws calender week view (-1,0,1) for given date
|
||||
*/
|
||||
drawCal(){
|
||||
d=this.date ? this.date : new Date();
|
||||
|
||||
const DAY_NAME_FONT_SIZE=10;
|
||||
const CAL_Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10+this.TIME_FONT_SIZE()+3;
|
||||
const CAL_AREA_H=Bangle.appRect.h-CAL_Y+24; //+24: top widegtes only
|
||||
const CELL_W=Bangle.appRect.w/7; //cell width
|
||||
const CELL_H=(CAL_AREA_H-DAY_NAME_FONT_SIZE)/3; //cell heigth
|
||||
const DAY_NUM_FONT_SIZE=Math.min(CELL_H-1,15); //size down, max 15
|
||||
|
||||
g.setFont("Vector", DAY_NAME_FONT_SIZE).setColor(g.theme.fg).setFontAlign(-1, -1).clearRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H);
|
||||
|
||||
//draw grid & Headline
|
||||
const dNames = this.ABR_DAY.map((a) => a.length<=2 ? a : a.substr(0, 2)); //force shrt 2
|
||||
for(var dNo=0; dNo<dNames.length; dNo++){
|
||||
const dIdx=this.settings().wdStrt>=0 ? (dNo+this.settings().wdStrt)%7 : (dNo+d.getDay()+4)%7;
|
||||
const dName=dNames[dIdx];
|
||||
if(dNo>0)
|
||||
g.drawLine(dNo*CELL_W, CAL_Y, dNo*CELL_W, CAL_Y+CAL_AREA_H-1);
|
||||
|
||||
if (dIdx==0) g.setColor(this.nrgb[this.settings().suClr]); //sunday maybe colorize txt
|
||||
g.drawString(dName, dNo*CELL_W+(CELL_W-g.stringWidth(dName))/2+2, CAL_Y+1).setColor(g.theme.fg);
|
||||
}
|
||||
|
||||
var nextY=CAL_Y+DAY_NAME_FONT_SIZE;
|
||||
|
||||
for(i=0; i<3; i++){
|
||||
const y=nextY+i*CELL_H;
|
||||
g.drawLine(Bangle.appRect.x, y, Bangle.appRect.x2, y);
|
||||
}
|
||||
|
||||
g.setFont("Vector", DAY_NUM_FONT_SIZE);
|
||||
|
||||
//write days
|
||||
const tdyDate=d.getDate();
|
||||
const days=this.settings().wdStrt>=0 ? 7+((7+d.getDay()-this.settings().wdStrt)%7) : 10;//start day (week before=7 days + days in this week realtive to week start) or fixed 7+3 days
|
||||
var rD=new Date(d.getTime());
|
||||
rD.setDate(rD.getDate()-days);
|
||||
var rDate=rD.getDate();
|
||||
for(var y=0; y<3; y++){
|
||||
for(var x=0; x<dNames.length; x++){
|
||||
if(rDate===tdyDate){ //today
|
||||
g.setColor(this.nrgb[this.settings().tdyMrkClr]); //today marker color or fg color
|
||||
switch(this.settings().tdyMrkr){ //0:none, 1:circle, 2:rectangle, 3:filled
|
||||
case 1:
|
||||
for(m=1; m<=this.settings().tdyMrkPxl&&m<CELL_H-1&&m<CELL_W-1; m++)
|
||||
g.drawCircle(x*CELL_W+(CELL_W/2)+1, nextY+(CELL_H*y)+(CELL_H/2)+1, Math.min((CELL_W-m)/2, (CELL_H-m)/2)-2);
|
||||
break;
|
||||
case 2:
|
||||
for(m=1; m<=this.settings().tdyMrkPxl&&m<CELL_H-1&&m<CELL_W-1; m++)
|
||||
g.drawRect(x*CELL_W+m, nextY+CELL_H+m, x*CELL_W+CELL_W-m, nextY+CELL_H+CELL_H-m);
|
||||
break;
|
||||
case 3:
|
||||
g.fillRect(x*CELL_W+1, nextY+CELL_H+1, x*CELL_W+CELL_W-1, nextY+CELL_H+CELL_H-1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
g.setColor(this.nrgb[this.settings().tdyNumClr]); //today color or fg color
|
||||
}else if(this.settings().suClr && rD.getDay()==0){ //sundays
|
||||
g.setColor(this.nrgb[this.settings().suClr]);
|
||||
}else{ //default
|
||||
g.setColor(g.theme.fg);
|
||||
}
|
||||
g.drawString(rDate, x*CELL_W+((CELL_W-g.stringWidth(rDate))/2)+2, nextY+((CELL_H-DAY_NUM_FONT_SIZE+2)/2)+(CELL_H*y));
|
||||
rD.setDate(rDate+1);
|
||||
rDate=rD.getDate();
|
||||
}
|
||||
}
|
||||
if (this.settings().calBrdr) {
|
||||
g.setColor(g.theme.fg).drawRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H-1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates current ISO8601 week number e.g. 2
|
||||
* @param{Date} date for the date
|
||||
* @returns{Number}} e.g. 2
|
||||
*/
|
||||
ISO8601calWeek(date){ //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
|
||||
var tdt = new Date(date.valueOf());
|
||||
var dayn = (date.getDay() + 6) % 7;
|
||||
tdt.setDate(tdt.getDate() - dayn + 3);
|
||||
var firstThursday = tdt.valueOf();
|
||||
tdt.setMonth(0, 1);
|
||||
if (tdt.getDay() !== 4){
|
||||
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
|
||||
}
|
||||
return Number(1 + Math.ceil((firstThursday - tdt) / 604800000));
|
||||
}
|
||||
}
|
||||
|
||||
//*************************************************************************************
|
||||
//*************************************************************************************
|
||||
//*************************************************************************************
|
||||
//Copy ABOVE the src code of clock-app class and load via espruino WEB IDE
|
||||
//*************************************************************************************
|
||||
//*************************************************************************************
|
||||
//*************************************************************************************
|
||||
|
||||
/**
|
||||
* Severity for logging
|
||||
*/
|
||||
const LogSeverity={
|
||||
DEBUG: 5,
|
||||
INFO: 4,
|
||||
WARNING: 3,
|
||||
ERROR: 2,
|
||||
EXCEPTION: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* Exception: Mandatory Field not provided
|
||||
*/
|
||||
class EmptyMandatoryError extends Error{
|
||||
/**
|
||||
* Create Exception
|
||||
* @param {String} name of the field
|
||||
* @param {*} given data e.g. an object
|
||||
* @param {*} expected *optional* an working example
|
||||
*/
|
||||
constructor(name, given, expected) {
|
||||
this.field = name;
|
||||
this.got = given;
|
||||
this.message = "Missing mandatory '"+ name +"'. given '"+JSON.stringify(given)+"'";
|
||||
if (expected) {
|
||||
this.message+= " != expected: '"+JSON.stringify(expected)+"'";
|
||||
this.sample = expected;
|
||||
}
|
||||
Error(this.message);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception: Invalid Function
|
||||
*/
|
||||
class InvalidMethodName extends Error{
|
||||
/**
|
||||
* Create Exception
|
||||
* @param {String} name of the field
|
||||
* @param {*} given data e.g. an object
|
||||
* @param {*} expected *optional* an working example
|
||||
*/
|
||||
constructor(className, methodName) {
|
||||
this.class = className;
|
||||
this.method = methodName;
|
||||
this.message = "Function '"+methodName+"' not found in '"+className+"'";
|
||||
Error(this.message);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
/*************************************************************************/
|
||||
|
||||
/**
|
||||
* All Test Masterclass
|
||||
*/
|
||||
class Test{
|
||||
}
|
||||
|
||||
/*************************************************************************/
|
||||
|
||||
/**
|
||||
* Test Settings - use this if you want e.g. test draw/render function(s)
|
||||
*/
|
||||
class TestSetting extends Test{
|
||||
TEST_SETTING_SAMPLE() {
|
||||
return {
|
||||
setting: "<settingName>",
|
||||
cases: [
|
||||
{
|
||||
value: "required,<settingValue>",
|
||||
beforeTxt: "optional,<textToDisplayBeforeTest>",
|
||||
beforeExpression: "optional,<expressionExpectedTrue>",
|
||||
afterText: "optional,<textToDisplayAfterTest>",
|
||||
afterExpression: "optional,<expressionExpectedTrue>"
|
||||
}
|
||||
],
|
||||
constructorParams: ["optional: <cpar1>","|TEST_SETTINGS|","..."], //TEST_SETTINGS will be replcaed with each current {setting: case}
|
||||
functionNames: ["required, <function under test>", "..."],
|
||||
functionParams: ["optional: <fpar1>","|TEST_SETTINGS|","..."]
|
||||
};
|
||||
}
|
||||
|
||||
constructor(data){
|
||||
|
||||
this._validate(data);
|
||||
|
||||
this.setting = data.setting;
|
||||
this.cases = data.cases.map((entry) => {
|
||||
return {
|
||||
value: entry.value,
|
||||
beforeTxt: entry.beforeTxt||"",
|
||||
beforeExpression: entry.beforeExpression||true,
|
||||
afterTxt: entry.afterTxt||"",
|
||||
afterExpression: entry.afterExpression||true
|
||||
};
|
||||
});
|
||||
this.constructorParams = data.constructorParams;
|
||||
this.functionNames = data.functionNames;
|
||||
this.functionParams = data.functionParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* validates the given data config
|
||||
*/
|
||||
_validate(data){
|
||||
//validate given config
|
||||
if (!data.setting) throw new EmptyMandatoryError("setting", data, this.TEST_SETTING_SAMPLE());
|
||||
if (!(data.cases instanceof Array) || data.cases.length==0) throw new EmptyMandatoryError("cases", data, this.TEST_SETTING_SAMPLE());
|
||||
if (!(data.functionNames instanceof Array) || data.functionNames==0) throw new EmptyMandatoryError("functionNames", data, this.TEST_SETTING_SAMPLE());
|
||||
|
||||
data.cases.forEach((entry,idx) => {
|
||||
if (entry.value === undefined) throw new EmptyMandatoryError("cases["+idx+"].value", entry, this.TEST_SETTING_SAMPLE());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************************/
|
||||
|
||||
/**
|
||||
* Testing a Bangle object
|
||||
*/
|
||||
class BangleTestRunner{
|
||||
/**
|
||||
* create for ObjClass
|
||||
* @param {Class} objClass
|
||||
* @param {LogSeverity} minSeverity to Log
|
||||
*/
|
||||
constructor(objClass, minSeverity){
|
||||
this.TESTCASE_MSG_BEFORE_TIMEOUT = 1000; //5s
|
||||
this.TESTCASE_RUN_TIMEOUT = 1000; //5s
|
||||
this.TESTCASE_MSG_AFTER_TIMEOUT = 1000; //5s
|
||||
|
||||
this.oClass = objClass;
|
||||
this.minSvrty = minSeverity;
|
||||
this.tests = [];
|
||||
|
||||
this.currentCaseNum = this.currentTestNum = this.currentTest = this.currentCase = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* add a Setting Test, return instance for chaining
|
||||
* @param {TestSetting}
|
||||
*/
|
||||
addTestSettings(sttngs) {
|
||||
this.tests.push(new TestSetting(sttngs));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execution of all tests
|
||||
*/
|
||||
execute() {
|
||||
this._init();
|
||||
while (this._nextTest()) {
|
||||
this._beforeTest();
|
||||
while (this._nextCase()) {
|
||||
this._beforeCase();
|
||||
this._runCase();
|
||||
this._afterCase();
|
||||
}
|
||||
this._afterTest();
|
||||
this._firstCase();
|
||||
}
|
||||
this._exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* global prepare - before all test
|
||||
*/
|
||||
_init() {
|
||||
console.log(this._nowTime(), ">>init");
|
||||
this.currentTestNum=-1;
|
||||
this.currentCaseNum=-1;
|
||||
}
|
||||
|
||||
/**
|
||||
* before each test
|
||||
*/
|
||||
_beforeTest() {
|
||||
console.log(this._nowTime(), ">>test #" + this.currentTestNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* befor each testcase
|
||||
*/
|
||||
_beforeCase() {
|
||||
console.log(this.currentTest);
|
||||
console.log(this._nowTime(), ">>case #" + this.currentTestNum + "." + this.currentCaseNum + "/" + (this.currentTest.cases.length-1));
|
||||
if (this.currentTest instanceof TestSetting)
|
||||
console.log(this.currentTest.setting+"="+this.currentCase.value+"/n"+(this.currentCase.beforeTxt ? "#"+this.currentCase.beforeTxt : ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* testcase runner
|
||||
*/
|
||||
_runCase() {
|
||||
console.log(this._nowTime(), ">>running...");
|
||||
var returns = [];
|
||||
this.currentTest.functionNames.forEach((fName) => {
|
||||
var settings={}; settings[this.currentTest.setting] = this.currentCase.value;
|
||||
var cParams = this.currentTest.constructorParams||[];
|
||||
cParams = cParams.map((v) => (v && v instanceof String && v==="|TEST_SETTINGS|") ? settings : v);//replace settings in call params
|
||||
var fParams = this.currentTest.functionParams||[];
|
||||
fParams = fParams.map((v) => (v && v instanceof String && v==="|TEST_SETTINGS|") ? settings : v);//replace settings in call params
|
||||
|
||||
var creatorFunc = new Function("console.log('Constructor params:', arguments); return new " + this.oClass + "(arguments[0],arguments[1],arguments[2],arguments[3],arguments[4],arguments[5],arguments[6],arguments[7],arguments[8],arguments[9])"); //prepare spwan arguments[0],arguments[1]
|
||||
let instance = creatorFunc.call(this.oClass, cParams[0], cParams[1], cParams[2], cParams[3], cParams[4], cParams[5], cParams[6], cParams[7], cParams[8], cParams[9]); //spwan
|
||||
|
||||
console.log(">>"+this.oClass+"["+fName+"]()");
|
||||
|
||||
console.log('Instance:', instance);
|
||||
console.log('Function params:', fParams);
|
||||
returns.push(instance[fName](fParams[0], fParams[1], fParams[2], fParams[3], fParams[4], fParams[5], fParams[6], fParams[7], fParams[8], fParams[9])); //run method and store result
|
||||
g.dump();
|
||||
console.log("<<"+this.oClass+"["+fName+"]()");
|
||||
});
|
||||
console.log(this._nowTime(), "<<...running");
|
||||
}
|
||||
|
||||
/**
|
||||
* after each testcase
|
||||
*/
|
||||
_afterCase() {
|
||||
if (this.currentTest instanceof TestSetting)
|
||||
if (this.currentCase.afterTxt.length>0)
|
||||
console.log("++EXPECTED:" + this.currentCase.afterTxt + "EXPECTED++");
|
||||
console.log(this._nowTime(), "<<case #" + this.currentTestNum + "." + this.currentCaseNum + "/" + (this.currentTest.cases.length-1));
|
||||
}
|
||||
|
||||
/**
|
||||
* after each test
|
||||
*/
|
||||
_afterTest() {
|
||||
console.log(this._nowTime(), "<<test #" + this.currentTestNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* after all tests
|
||||
*/
|
||||
_exit() {
|
||||
console.log(this._nowTime(), "<<exit");
|
||||
}
|
||||
|
||||
/**
|
||||
* delays for x seconds
|
||||
* @param {Number} sec to delay
|
||||
*/
|
||||
_delay(sec) {
|
||||
return new Promise(resolve => setTimeout(resolve, sec));
|
||||
}
|
||||
|
||||
_waits(sec) {
|
||||
this._delay(1).then();
|
||||
}
|
||||
|
||||
_log() {
|
||||
|
||||
}
|
||||
|
||||
_nextTest() {
|
||||
if (this.currentTestNum>=-1 && (this.currentTestNum+1)<this.tests.length) {
|
||||
this.currentTestNum++; this.currentTest = this.tests[this.currentTestNum];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_firstCase() {
|
||||
this.currentCaseNum=-1;
|
||||
}
|
||||
|
||||
_nextCase() {
|
||||
if (this.currentCaseNum>=-1 && (this.currentCaseNum+1)<this.currentTest.cases.length) {
|
||||
this.currentCaseNum++; this.currentCase = this.currentTest.cases[this.currentCaseNum];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_nowTime() {
|
||||
d = new Date();
|
||||
return(("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2) + ":" + ("0" + d.getSeconds()).slice(-2) + "." + ("00" + d.getMilliseconds()).slice(-3));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* TEST all Settings
|
||||
*/
|
||||
new BangleTestRunner("TimeCalClock", LogSeverity.INFO)
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "shwDate",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"No date display?", afterTxt: "top area should be 'emtpy'" },
|
||||
{ value: 1, beforeTxt:"Locale date display?", afterTxt: "date should be 06/05/1234" },
|
||||
{ value: 2, beforeTxt:"Month longname?", afterTxt: "date should be June" },
|
||||
{ value: 3, beforeTxt:"Monthshort yearshort #week", afterTxt: "date should be Jun.34 #23" }
|
||||
],
|
||||
constructorParams: [new Date(1234,5,6,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawDate"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"Week start Sunday?" , afterTxt: "Calendar first day is Sunday" },
|
||||
{ value: 1, beforeTxt:"Week start Monday?" , afterTxt: "Calendar first day is Monday"},
|
||||
{ value: 2, beforeTxt:"Week start Tuesday?" , afterTxt: "Calendar first day is Tuesday"},
|
||||
{ value: 3, beforeTxt:"Week start Wednesday?" , afterTxt: "Calendar first day is Wednesday"},
|
||||
{ value: 4, beforeTxt:"Week start Thursday?" , afterTxt: "Calendar first day is Thursday"},
|
||||
{ value: 5, beforeTxt:"Week start Friday?" , afterTxt: "Calendar first day is Friday"},
|
||||
{ value: 6, beforeTxt:"Week start Saturday?" , afterTxt: "Calendar first day is Saturday"},
|
||||
],
|
||||
constructorParams: [new Date(1234,5,3,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"Week start Sunday?" , afterTxt: "Calendar first day is Sunday" },
|
||||
{ value: 1, beforeTxt:"Week start Monday?" , afterTxt: "Calendar first day is Monday"},
|
||||
{ value: 2, beforeTxt:"Week start Tuesday?" , afterTxt: "Calendar first day is Tuesday"},
|
||||
{ value: 3, beforeTxt:"Week start Wednesday?" , afterTxt: "Calendar first day is Wednesday"},
|
||||
{ value: 4, beforeTxt:"Week start Thursday?" , afterTxt: "Calendar first day is Thursday"},
|
||||
{ value: 5, beforeTxt:"Week start Friday?" , afterTxt: "Calendar first day is Friday"},
|
||||
{ value: 6, beforeTxt:"Week start Saturday?" , afterTxt: "Calendar first day is Saturday"},
|
||||
],
|
||||
constructorParams: [new Date(1234,5,4,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"Week start Sunday?" , afterTxt: "Calendar first day is Sunday" },
|
||||
{ value: 1, beforeTxt:"Week start Monday?" , afterTxt: "Calendar first day is Monday"},
|
||||
{ value: 2, beforeTxt:"Week start Tuesday?" , afterTxt: "Calendar first day is Tuesday"},
|
||||
{ value: 3, beforeTxt:"Week start Wednesday?" , afterTxt: "Calendar first day is Wednesday"},
|
||||
{ value: 4, beforeTxt:"Week start Thursday?" , afterTxt: "Calendar first day is Thursday"},
|
||||
{ value: 5, beforeTxt:"Week start Friday?" , afterTxt: "Calendar first day is Friday"},
|
||||
{ value: 6, beforeTxt:"Week start Saturday?" , afterTxt: "Calendar first day is Saturday"},
|
||||
],
|
||||
constructorParams: [new Date(1234,5,5,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"Week start Sunday?" , afterTxt: "Calendar first day is Sunday" },
|
||||
{ value: 1, beforeTxt:"Week start Monday?" , afterTxt: "Calendar first day is Monday"},
|
||||
{ value: 2, beforeTxt:"Week start Tuesday?" , afterTxt: "Calendar first day is Tuesday"},
|
||||
{ value: 3, beforeTxt:"Week start Wednesday?" , afterTxt: "Calendar first day is Wednesday"},
|
||||
{ value: 4, beforeTxt:"Week start Thursday?" , afterTxt: "Calendar first day is Thursday"},
|
||||
{ value: 5, beforeTxt:"Week start Friday?" , afterTxt: "Calendar first day is Friday"},
|
||||
{ value: 6, beforeTxt:"Week start Saturday?" , afterTxt: "Calendar first day is Saturday"},
|
||||
],
|
||||
constructorParams: [new Date(1234,5,6,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"Week start Sunday?" , afterTxt: "Calendar first day is Sunday" },
|
||||
{ value: 1, beforeTxt:"Week start Monday?" , afterTxt: "Calendar first day is Monday"},
|
||||
{ value: 2, beforeTxt:"Week start Tuesday?" , afterTxt: "Calendar first day is Tuesday"},
|
||||
{ value: 3, beforeTxt:"Week start Wednesday?" , afterTxt: "Calendar first day is Wednesday"},
|
||||
{ value: 4, beforeTxt:"Week start Thursday?" , afterTxt: "Calendar first day is Thursday"},
|
||||
{ value: 5, beforeTxt:"Week start Friday?" , afterTxt: "Calendar first day is Friday"},
|
||||
{ value: 6, beforeTxt:"Week start Saturday?" , afterTxt: "Calendar first day is Saturday"},
|
||||
],
|
||||
constructorParams: [new Date(1234,5,7,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: 0, beforeTxt:"Week start Sunday?" , afterTxt: "Calendar first day is Sunday" },
|
||||
{ value: 1, beforeTxt:"Week start Monday?" , afterTxt: "Calendar first day is Monday"},
|
||||
{ value: 2, beforeTxt:"Week start Tuesday?" , afterTxt: "Calendar first day is Tuesday"},
|
||||
{ value: 3, beforeTxt:"Week start Wednesday?" , afterTxt: "Calendar first day is Wednesday"},
|
||||
{ value: 4, beforeTxt:"Week start Thursday?" , afterTxt: "Calendar first day is Thursday"},
|
||||
{ value: 5, beforeTxt:"Week start Friday?" , afterTxt: "Calendar first day is Friday"},
|
||||
{ value: 6, beforeTxt:"Week start Saturday?" , afterTxt: "Calendar first day is Saturday"},
|
||||
],
|
||||
constructorParams: [new Date(1234,5,8,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Sunday in mid?" , afterTxt: "Calendar focus today: Sunday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,3,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Monday in mid?" , afterTxt: "Calendar focus today: Monday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,4,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Tuesday in mid?" , afterTxt: "Calendar focus today: Tuesday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,5,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Wednesday in mid?" , afterTxt: "Calendar focus today: Wednesday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,6,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Thursday in mid?" , afterTxt: "Calendar focus today: Thursday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,7,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Friday in mid?" , afterTxt: "Calendar focus today: Friday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,8,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
.addTestSettings({
|
||||
setting: "wdStrt",
|
||||
cases: [
|
||||
{ value: -1, beforeTxt:"Saturday in mid?" , afterTxt: "Calendar focus today: Saturday" },
|
||||
],
|
||||
constructorParams: [new Date(1234,5,9,7,8,9),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "tdyNumClr",
|
||||
cases: [
|
||||
{ value: 1, beforeTxt:"Today color: red?" , afterTxt: "Today is marked red" },
|
||||
{ value: 2, beforeTxt:"Today color: green?" , afterTxt: "Today is marked green" },
|
||||
{ value: 3, beforeTxt:"Today color: blue?" , afterTxt: "Today is marked blue" },
|
||||
],
|
||||
constructorParams: [new Date(),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "tdyMrkr",
|
||||
cases: [
|
||||
{ value: 1, beforeTxt:"Today highlight cricle?" , afterTxt: "Today circled." },
|
||||
{ value: 2, beforeTxt:"Today highlight rectangle?" , afterTxt: "Today rectangled." },
|
||||
{ value: 3, beforeTxt:"Today highlight filled?" , afterTxt: "Today filled." },
|
||||
],
|
||||
constructorParams: [new Date(),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "suClr",
|
||||
cases: [
|
||||
{ value: 1, beforeTxt:"Sundays color: red?" , afterTxt: "Sundays are red" },
|
||||
{ value: 2, beforeTxt:"Sundays color: green?" , afterTxt: "Sundays are green" },
|
||||
{ value: 3, beforeTxt:"Sundays color: blue?" , afterTxt: "Sundays are blue" },
|
||||
],
|
||||
constructorParams: [new Date(),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
|
||||
/*
|
||||
.addTestSettings({
|
||||
setting: "calBrdr",
|
||||
cases: [
|
||||
{ value: false, beforeTxt:"Calendar without border?" , afterTxt: "No outer border." },
|
||||
{ value: true, beforeTxt:"Calendar with border?" , afterTxt: "Outer border." },
|
||||
],
|
||||
constructorParams: [new Date(),"|TEST_SETTINGS|"],
|
||||
functionNames: ["drawCal"],
|
||||
functionParams: [],
|
||||
})
|
||||
*/
|
||||
.execute();
|
||||
|
|
@ -1,94 +1,274 @@
|
|||
var center = g.getWidth() / 2;
|
||||
var lastDayDraw;
|
||||
var lastTimeDraw;
|
||||
//Clock renders date, time and pre,current,next week calender view
|
||||
class TimeCalClock{
|
||||
DATE_FONT_SIZE(){ return 20; }
|
||||
TIME_FONT_SIZE(){ return 40; }
|
||||
|
||||
/**
|
||||
* @param{Date} date optional the date (e.g. for testing)
|
||||
* @param{Settings} settings optional settings to use e.g. for testing
|
||||
*/
|
||||
constructor(date, settings){
|
||||
if (date)
|
||||
this.date=date;
|
||||
|
||||
var fontColor = g.theme.fg;
|
||||
var accentColor = "#FF0000";
|
||||
var locale = require("locale");
|
||||
if (settings)
|
||||
this._settings = settings;
|
||||
else
|
||||
this._settings = require("Storage").readJSON("timecal.settings.json", 1) || {};
|
||||
|
||||
function loop() {
|
||||
var d = new Date();
|
||||
var cleared = false;
|
||||
if(lastDayDraw != d.getDate()){
|
||||
lastDayDraw = d.getDate();
|
||||
drawDate(d);
|
||||
drawCal(d);
|
||||
}
|
||||
|
||||
if(lastTimeDraw != d.getMinutes() || cleared){
|
||||
lastTimeDraw = d.getMinutes();
|
||||
drawTime(d);
|
||||
}
|
||||
}
|
||||
function drawTime(d){
|
||||
var hour = ("0" + d.getHours()).slice(-2);
|
||||
var min = ("0" + d.getMinutes()).slice(-2);
|
||||
g.setFontAlign(0,-1,0);
|
||||
g.setFont("Vector",40);
|
||||
g.setColor(fontColor);
|
||||
g.clearRect(0,50,g.getWidth(),90);
|
||||
g.drawString(hour + ":" + min,center,50);
|
||||
}
|
||||
function drawDate(d){
|
||||
var day = ("0" + d.getDate()).slice(-2);
|
||||
var month = ("0" + d.getMonth()).slice(-2);
|
||||
var dateStr = locale.date(d,1);
|
||||
g.clearRect(0,24,g.getWidth(),44);
|
||||
g.setFont("Vector",20);
|
||||
g.setColor(fontColor);
|
||||
g.setFontAlign(0,-1,0);
|
||||
g.drawString(dateStr,center,24);
|
||||
}
|
||||
const defaults = {
|
||||
shwDate:1, //0:none, 1:locale, 2:month, 3:monthshort.year #week
|
||||
|
||||
wdStrt:0, //identical to getDay() 0->Su, 1->Mo, ... //Issue #1154: weekstart So/Mo, -1 for use today
|
||||
|
||||
tdyNumClr:3, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
tdyMrkr:0, //0:none, 1:circle, 2:rectangle, 3:filled
|
||||
tdyMrkClr:2, //1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
tdyMrkPxl:3, //px
|
||||
|
||||
suClr:1, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
//phColor:"#E00", //public holiday
|
||||
|
||||
calBrdr:false
|
||||
};
|
||||
for (const k in this._settings) if (!defaults.hasOwnProperty(k)) delete this._settings[k]; //remove invalid settings
|
||||
for (const k in defaults) if(!this._settings.hasOwnProperty(k)) this._settings[k] = defaults[k]; //assign missing defaults
|
||||
|
||||
g.clear();
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
this.centerX = Bangle.appRect.w/2;
|
||||
this.nrgb = [g.theme.fg, "#E00", "#0E0", "#00E"]; //fg, r ,g , b
|
||||
|
||||
this.ABR_DAY=[];
|
||||
if (require("locale") && require("locale").dow)
|
||||
for (let d=0; d<=6; d++) {
|
||||
var refDay=new Date();
|
||||
refDay.setFullYear(1972);
|
||||
refDay.setMonth(0);
|
||||
refDay.setDate(2+d);
|
||||
this.ABR_DAY.push(require("locale").dow(refDay));
|
||||
|
||||
function drawCal(d){
|
||||
var calStart = 101;
|
||||
var cellSize = g.getWidth() / 7;
|
||||
var halfSize = cellSize / 2;
|
||||
g.clearRect(0,calStart,g.getWidth(),g.getHeight());
|
||||
g.drawLine(0,calStart,g.getWidth(),calStart);
|
||||
var days = ["Mo","Tu","We","Th","Fr","Sa","Su"];
|
||||
g.setFont("Vector",10);
|
||||
g.setColor(fontColor);
|
||||
g.setFontAlign(-1,-1,0);
|
||||
for(var i = 0; i < days.length;i++){
|
||||
g.drawString(days[i],i*cellSize+5,calStart -11);
|
||||
if(i!=0){
|
||||
g.drawLine(i*cellSize,calStart,i*cellSize,g.getHeight());
|
||||
}
|
||||
}
|
||||
var cellHeight = (g.getHeight() -calStart ) / 3;
|
||||
for(var i = 0;i < 3;i++){
|
||||
var starty = calStart + i * cellHeight;
|
||||
g.drawLine(0,starty,g.getWidth(),starty);
|
||||
}
|
||||
|
||||
g.setFont("Vector",15);
|
||||
|
||||
var dayOfWeek = d.getDay();
|
||||
var dayRem = d.getDay() - 1;
|
||||
if(dayRem <0){
|
||||
dayRem = 0;
|
||||
}
|
||||
|
||||
var start = new Date();
|
||||
start.setDate(start.getDate()-(7+dayRem));
|
||||
g.setFontAlign(0,-1,0);
|
||||
for (var y = 0;y < 3; y++){
|
||||
for(var x = 0;x < 7; x++){
|
||||
if(start.getDate() === d.getDate()){
|
||||
g.setColor(accentColor);
|
||||
}else{
|
||||
g.setColor(fontColor);
|
||||
}
|
||||
g.drawString(start.getDate(),x*cellSize +(cellSize / 2) + 2,calStart+(cellHeight*y) + 5);
|
||||
start.setDate(start.getDate()+1);
|
||||
else
|
||||
this.ABR_DAY=["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Object} current settings object
|
||||
*/
|
||||
settings(){
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Run forest run
|
||||
**/
|
||||
draw(){
|
||||
this.drawTime();
|
||||
|
||||
if (this.TZOffset===undefined || this.TZOffset!==d.getTimezoneOffset())
|
||||
this.drawDateAndCal();
|
||||
}
|
||||
|
||||
/**
|
||||
* draw given or current time from date
|
||||
* overwatch timezone changes
|
||||
* schedules itself to update
|
||||
*/
|
||||
drawTime(){
|
||||
d=this.date ? this.date : new Date();
|
||||
const Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10;
|
||||
|
||||
d=d?d :new Date();
|
||||
|
||||
g.setFontAlign(0, -1).setFont("Vector", this.TIME_FONT_SIZE()).setColor(g.theme.fg)
|
||||
.clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7)
|
||||
.drawString(("0" + require("locale").time(d, 1)).slice(-5), this.centerX, Y, true);
|
||||
//.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+this.TIME_FONT_SIZE()-7); //DEV-Option
|
||||
|
||||
setTimeout(this.draw.bind(this), 60000-(d.getSeconds()*1000)-d.getMilliseconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* draws given date and cal
|
||||
* @param{Date} d provide date or uses today
|
||||
*/
|
||||
drawDateAndCal(){
|
||||
d=this.date ? this.date : new Date();
|
||||
|
||||
this.TZOffset=d.getTimezoneOffset();
|
||||
this.drawDate();
|
||||
this.drawCal();
|
||||
|
||||
if (this.tOutD) //abort exisiting
|
||||
clearTimeout(this.tOutD);
|
||||
this.tOutD=setTimeout(this.drawDateAndCal.bind(this), 86400000-(d.getHours()*24*60*1000)-(d.getMinutes()*60*1000)-d.getSeconds()-d.getMilliseconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* draws given date as defiend in settings
|
||||
*/
|
||||
drawDate(){
|
||||
d=this.date ? this.date : new Date();
|
||||
|
||||
const FONT_SIZE=20;
|
||||
const Y=Bangle.appRect.y;
|
||||
var render=false;
|
||||
var dateStr = "";
|
||||
if (this.settings().shwDate>0) { //skip if exactly -none
|
||||
const dateSttngs = ["","l","M","m.Y #W"];
|
||||
for (let c of dateSttngs[this.settings().shwDate]) { //add part as configured
|
||||
switch (c){
|
||||
case "l":{ //locale
|
||||
render=true;
|
||||
dateStr+=require("locale").date(d,1);
|
||||
break;
|
||||
}
|
||||
case "m":{ //month e.g. Jan.
|
||||
render=true;
|
||||
dateStr+=require("locale").month(d,1);
|
||||
break;
|
||||
}
|
||||
case "M":{ //month e.g. January
|
||||
render=true;
|
||||
dateStr+=require("locale").month(d,0);
|
||||
break;
|
||||
}
|
||||
case "y":{ //year e.g. 22
|
||||
render=true;
|
||||
dateStr+=d.getFullYear().slice(-2);
|
||||
break;
|
||||
}
|
||||
case "Y":{ //year e.g. 2022
|
||||
render=true;
|
||||
dateStr+=d.getFullYear();
|
||||
break;
|
||||
}
|
||||
case "w":{ //week e.g. #2
|
||||
dateStr+=(this.ISO8601calWeek(d));
|
||||
break;
|
||||
}
|
||||
case "W":{ //week e.g. #02
|
||||
dateStr+=("0"+this.ISO8601calWeek(d)).slice(-2);
|
||||
break;
|
||||
}
|
||||
default: //append c
|
||||
dateStr+=c;
|
||||
render=dateStr.length>0;
|
||||
break; //noop
|
||||
}
|
||||
}
|
||||
}
|
||||
if (render){
|
||||
g.setFont("Vector", FONT_SIZE).setColor(g.theme.fg).setFontAlign(0, -1).clearRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3).drawString(dateStr,this.centerX,Y);
|
||||
}
|
||||
//g.drawRect(Bangle.appRect.x, Y, Bangle.appRect.x2, Y+FONT_SIZE-3); //DEV-Option
|
||||
}
|
||||
|
||||
/**
|
||||
* draws calender week view (-1,0,1) for given date
|
||||
*/
|
||||
drawCal(){
|
||||
d=this.date ? this.date : new Date();
|
||||
|
||||
const DAY_NAME_FONT_SIZE=10;
|
||||
const CAL_Y=Bangle.appRect.y+this.DATE_FONT_SIZE()+10+this.TIME_FONT_SIZE()+3;
|
||||
const CAL_AREA_H=Bangle.appRect.h-CAL_Y+24; //+24: top widegtes only
|
||||
const CELL_W=Bangle.appRect.w/7; //cell width
|
||||
const CELL_H=(CAL_AREA_H-DAY_NAME_FONT_SIZE)/3; //cell heigth
|
||||
const DAY_NUM_FONT_SIZE=Math.min(CELL_H-1,15); //size down, max 15
|
||||
|
||||
g.setFont("Vector", DAY_NAME_FONT_SIZE).setColor(g.theme.fg).setFontAlign(-1, -1).clearRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H);
|
||||
|
||||
//draw grid & Headline
|
||||
const dNames = this.ABR_DAY.map((a) => a.length<=2 ? a : a.substr(0, 2)); //force shrt 2
|
||||
for(var dNo=0; dNo<dNames.length; dNo++){
|
||||
const dIdx=this.settings().wdStrt>=0 ? (dNo+this.settings().wdStrt)%7 : (dNo+d.getDay()+4)%7;
|
||||
const dName=dNames[dIdx];
|
||||
if(dNo>0)
|
||||
g.drawLine(dNo*CELL_W, CAL_Y, dNo*CELL_W, CAL_Y+CAL_AREA_H-1);
|
||||
|
||||
if (dIdx==0) g.setColor(this.nrgb[this.settings().suClr]); //sunday maybe colorize txt
|
||||
g.drawString(dName, dNo*CELL_W+(CELL_W-g.stringWidth(dName))/2+2, CAL_Y+1).setColor(g.theme.fg);
|
||||
}
|
||||
|
||||
var nextY=CAL_Y+DAY_NAME_FONT_SIZE;
|
||||
|
||||
for(i=0; i<3; i++){
|
||||
const y=nextY+i*CELL_H;
|
||||
g.drawLine(Bangle.appRect.x, y, Bangle.appRect.x2, y);
|
||||
}
|
||||
|
||||
g.setFont("Vector", DAY_NUM_FONT_SIZE);
|
||||
|
||||
//write days
|
||||
const tdyDate=d.getDate();
|
||||
const days=this.settings().wdStrt>=0 ? 7+((7+d.getDay()-this.settings().wdStrt)%7) : 10;//start day (week before=7 days + days in this week realtive to week start) or fixed 7+3 days
|
||||
var rD=new Date(d.getTime());
|
||||
rD.setDate(rD.getDate()-days);
|
||||
var rDate=rD.getDate();
|
||||
for(var y=0; y<3; y++){
|
||||
for(var x=0; x<dNames.length; x++){
|
||||
if(rDate===tdyDate){ //today
|
||||
g.setColor(this.nrgb[this.settings().tdyMrkClr]); //today marker color or fg color
|
||||
switch(this.settings().tdyMrkr){ //0:none, 1:circle, 2:rectangle, 3:filled
|
||||
case 1:
|
||||
for(m=1; m<=this.settings().tdyMrkPxl&&m<CELL_H-1&&m<CELL_W-1; m++)
|
||||
g.drawCircle(x*CELL_W+(CELL_W/2)+1, nextY+(CELL_H*y)+(CELL_H/2)+1, Math.min((CELL_W-m)/2, (CELL_H-m)/2)-2);
|
||||
break;
|
||||
case 2:
|
||||
for(m=1; m<=this.settings().tdyMrkPxl&&m<CELL_H-1&&m<CELL_W-1; m++)
|
||||
g.drawRect(x*CELL_W+m, nextY+CELL_H+m, x*CELL_W+CELL_W-m, nextY+CELL_H+CELL_H-m);
|
||||
break;
|
||||
case 3:
|
||||
g.fillRect(x*CELL_W+1, nextY+CELL_H+1, x*CELL_W+CELL_W-1, nextY+CELL_H+CELL_H-1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
g.setColor(this.nrgb[this.settings().tdyNumClr]); //today color or fg color
|
||||
}else if(this.settings().suClr && rD.getDay()==0){ //sundays
|
||||
g.setColor(this.nrgb[this.settings().suClr]);
|
||||
}else{ //default
|
||||
g.setColor(g.theme.fg);
|
||||
}
|
||||
g.drawString(rDate, x*CELL_W+((CELL_W-g.stringWidth(rDate))/2)+2, nextY+((CELL_H-DAY_NUM_FONT_SIZE+2)/2)+(CELL_H*y));
|
||||
rD.setDate(rDate+1);
|
||||
rDate=rD.getDate();
|
||||
}
|
||||
}
|
||||
if (this.settings().calBrdr) {
|
||||
g.setColor(g.theme.fg).drawRect(Bangle.appRect.x, CAL_Y, Bangle.appRect.x2, CAL_Y+CAL_AREA_H-1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates current ISO8601 week number e.g. 2
|
||||
* @param{Date} date for the date
|
||||
* @returns{Number}} e.g. 2
|
||||
*/
|
||||
ISO8601calWeek(date){ //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
|
||||
var tdt = new Date(date.valueOf());
|
||||
var dayn = (date.getDay() + 6) % 7;
|
||||
tdt.setDate(tdt.getDate() - dayn + 3);
|
||||
var firstThursday = tdt.valueOf();
|
||||
tdt.setMonth(0, 1);
|
||||
if (tdt.getDay() !== 4){
|
||||
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
|
||||
}
|
||||
return Number(1 + Math.ceil((firstThursday - tdt) / 604800000));
|
||||
}
|
||||
}
|
||||
|
||||
g.clear();
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
loop();
|
||||
setInterval(loop,1000);
|
||||
timeCalClock = new TimeCalClock(); timeCalClock.draw();
|
||||
|
||||
//hook on settime to redraw immediatly
|
||||
var _setTime = setTime;
|
||||
var setTime = function(t) {
|
||||
_setTime(t);
|
||||
timeCalClock.draw(true);
|
||||
};
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// Settings menu for Time calendar clock
|
||||
(function(exit) {
|
||||
ABR_DAY = require("locale") && require("locale").abday ? require("locale").abday : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
var FILE = "timecal.validSttngs.json";
|
||||
|
||||
const DEFAULTS = {
|
||||
shwDate:1, //0:none, 1:locale, 2:month, 3:monthshort.year #week
|
||||
|
||||
wdStrt:0, //identical to getDay() 0->Su, 1->Mo, ... //Issue #1154: weekstart So/Mo, -1 for use today
|
||||
|
||||
tdyNumClr:3, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
tdyMrkr:0, //0:none, 1:circle, 2:rectangle, 3:filled
|
||||
tdyMrkClr:2, //1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
tdyMrkPxl:3, //px
|
||||
|
||||
suClr:1, //0:fg, 1:red=#E00, 2:green=#0E0, 3:blue=#00E
|
||||
//phColor:"#E00", //public holiday
|
||||
|
||||
calBrdr:false
|
||||
};
|
||||
validSttngs = require("Storage").readJSON("timecal.validSttngs.json", 1) || {};
|
||||
for (const k in validSttngs) if (!DEFAULTS.hasOwnProperty(k)) delete this.validSttngs[k]; //remove invalid settings
|
||||
for (const k in DEFAULTS) if(!validSttngs.hasOwnProperty(k)) validSttngs[k] = validSttngs[k]; //assign missing defaults
|
||||
|
||||
var changedSttngs = Object.assign({}, validSttngs);
|
||||
|
||||
var saveExitSettings = () => {
|
||||
require('Storage').writeJSON(FILE, changedSttngs);
|
||||
exit();
|
||||
};
|
||||
|
||||
var cancelExitSettings = () => {
|
||||
require('Storage').writeJSON(FILE, validSttngs);
|
||||
exit();
|
||||
};
|
||||
|
||||
var showMainMenu = () => {
|
||||
E.showMenu({
|
||||
"": {
|
||||
"title": "TimeCal "+ /*LANG*/"settings"
|
||||
},
|
||||
/*LANG*/"< Save": () => saveExitSettings(),
|
||||
/*LANG*/"Show date": {
|
||||
value: validSttngs.shwDate,
|
||||
min: 0, max: 3,
|
||||
format: v => [/*LANG*/"none", /*LANG*/"locale", /*LANG*/"M", /*LANG*/"m.Y #W"][v],
|
||||
onchange: v => validSttngs.shwDate = v
|
||||
},
|
||||
/*LANG*/"Start wday": {
|
||||
value: validSttngs.wdStrt,
|
||||
min: -1, max: 6,
|
||||
format: v => v>=0 ? ABR_DAY[v] : /*LANG*/"today",
|
||||
onchange: v => validSttngs.wdStrt = v
|
||||
},
|
||||
/*LANG*/"Su color": {
|
||||
value: validSttngs.suClr,
|
||||
min: 0, max: 3,
|
||||
format: v => [/*LANG*/"none", /*LANG*/"red", /*LANG*/"green", /*LANG*/"blue"][v],
|
||||
onchange: v => validSttngs.suClr = v
|
||||
},
|
||||
/*LANG*/"Border": {
|
||||
value: validSttngs.calBrdr,
|
||||
format: v => v ? /*LANG*/"show" : /*LANG*/"none",
|
||||
onchange: v => validSttngs.calBrdr = v
|
||||
},
|
||||
/*LANG*/"Today settings": () => {
|
||||
showTodayMenu();
|
||||
},
|
||||
/*LANG*/"< Cancel": () => cancelExitSettings()
|
||||
});
|
||||
};
|
||||
|
||||
var showTodayMenu = () => {
|
||||
E.showMenu({
|
||||
"": {
|
||||
"title": /*LANG*/"Today settings"
|
||||
},
|
||||
"< Back": () => showMainMenu(),
|
||||
/*LANG*/"Color": {
|
||||
value: validSttngs.tdyNumClr,
|
||||
min: 0, max: 3,
|
||||
format: v => [/*LANG*/"none", /*LANG*/"red", /*LANG*/"green", /*LANG*/"blue"][v],
|
||||
onchange: v => validSttngs.tdyNumClr = v
|
||||
},
|
||||
/*LANG*/"Marker": {
|
||||
value: validSttngs.tdyMrkr,
|
||||
min: 0, max: 3,
|
||||
format: v => [/*LANG*/"none", /*LANG*/"circle", /*LANG*/"rectangle", /*LANG*/"filled"][v],
|
||||
onchange: v => validSttngs.tdyMrkr = v
|
||||
},
|
||||
/*LANG*/"Mrk.Color": {
|
||||
value: validSttngs.tdyMrkClr,
|
||||
min: 0, max: 2,
|
||||
format: v => [/*LANG*/"red", /*LANG*/"green", /*LANG*/"blue"][v],
|
||||
onchange: v => validSttngs.tdyMrkClr = v
|
||||
},
|
||||
/*LANG*/"Mrk.Size": {
|
||||
value: validSttngs.tdyMrkPxl,
|
||||
min: 1, max: 10,
|
||||
format: v => v+"px",
|
||||
onchange: v => validSttngs.tdyMrkPxl = v
|
||||
},
|
||||
/*LANG*/"< Cancel": () => cancelExitSettings()
|
||||
});
|
||||
};
|
||||
|
||||
showMainMenu();
|
||||
});
|
||||
|
|
@ -1 +1,3 @@
|
|||
0.01: New Widget!
|
||||
0.02: Battery bar turns yellow on charge
|
||||
Memory status bar does not trigger garbage collect
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widbars",
|
||||
"name": "Bars Widget",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Display several measurements as vertical bars.",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
|
|
|
|||
|
|
@ -42,19 +42,25 @@
|
|||
if (top) g .clearRect(x,y, x+w-1,y+top-1); // erase above bar
|
||||
if (f) g.setColor(col).fillRect(x,y+top, x+w-1,y+h-1); // even for f=0.001 this is still 1 pixel high
|
||||
}
|
||||
let batColor='#0f0';
|
||||
function draw() {
|
||||
g.reset();
|
||||
const x = this.x, y = this.y,
|
||||
m = process.memory();
|
||||
m = process.memory(false);
|
||||
let b=0;
|
||||
// ==HRM== bar(x+(w*b++),y,'#f00'/*red */,bpm/200); // >200 seems very unhealthy; if we have no valid bpm this will just be empty space
|
||||
// ==Temperature== bar(x+(w*b++),y,'#ff0'/*yellow */,E.getTemperature()/50); // you really don't want to wear a watch that's hotter than 50°C
|
||||
bar(x+(w*b++),y,g.theme.dark?'#0ff':'#00f'/*cyan/blue*/,1-(require('Storage').getFree() / process.env.STORAGE));
|
||||
bar(x+(w*b++),y,'#f0f'/*magenta*/,m.usage/m.total);
|
||||
bar(x+(w*b++),y,'#0f0'/*green */,E.getBattery()/100);
|
||||
bar(x+(w*b++),y,batColor,E.getBattery()/100);
|
||||
}
|
||||
|
||||
let redraw;
|
||||
Bangle.on('charging', function(charging) {
|
||||
batColor=charging?'#ff0':'#0f0';
|
||||
WIDGETS["bars"].draw();
|
||||
});
|
||||
|
||||
Bangle.on('lcdPower', on => {
|
||||
if (redraw) clearInterval(redraw)
|
||||
redraw = undefined;
|
||||
|
|
|
|||
2
core
|
|
@ -1 +1 @@
|
|||
Subproject commit 3093d78a5d752cbf03ea8f9a1a7c0b50b9c8123b
|
||||
Subproject commit bf29f5697445686255a785476e6b1ed6a13ff697
|
||||