Merge branch 'espruino:master' into master
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
# Needs to be ignored because it uses ESM export/import
|
||||
apps/gipy/pkg/gps.js
|
||||
apps/gipy/pkg/gps.d.ts
|
||||
apps/gipy/pkg/gps_bg.wasm.d.ts
|
||||
|
||||
# Needs to be ignored because it includes broken JS
|
||||
apps/health/chart.min.js
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
1.00: initial release
|
||||
1.01: added tap event to scroll METAR and toggle seconds display
|
||||
1.02: continue showing METAR during AVWX updates (show update status next to time)
|
||||
re-try GPS fix if it takes too long
|
||||
bug fix
|
||||
don't attempt to update METAR if Bluetooth is NOT connected
|
||||
toggle seconds display on front double-taps (if un-locked) to avoid accidential enabling
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const APP_NAME = 'aviatorclk';
|
|||
const horizontalCenter = g.getWidth()/2;
|
||||
const mainTimeHeight = 38;
|
||||
const secondaryFontHeight = 22;
|
||||
require("Font8x16").add(Graphics); // tertiary font
|
||||
const dateColour = ( g.theme.dark ? COLOUR_YELLOW : COLOUR_BLUE );
|
||||
const UTCColour = ( g.theme.dark ? COLOUR_LIGHT_CYAN : COLOUR_DARK_CYAN );
|
||||
const separatorColour = ( g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_DARK_GREY );
|
||||
|
|
@ -37,6 +38,7 @@ var settings = Object.assign({
|
|||
var drawTimeout;
|
||||
var secondsInterval;
|
||||
var avwxTimeout;
|
||||
var gpsTimeout;
|
||||
|
||||
var AVWXrequest;
|
||||
var METAR = '';
|
||||
|
|
@ -92,16 +94,51 @@ function drawAVWX() {
|
|||
if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); }
|
||||
}
|
||||
|
||||
// show AVWX update status
|
||||
function showUpdateAVWXstatus(status) {
|
||||
let y = Bangle.appRect.y + 10;
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.clearRect(0, y, horizontalCenter - 54, y + 16);
|
||||
if (status) {
|
||||
g.setFontAlign(0, -1).setFont("8x16").setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW );
|
||||
g.drawString(status, horizontalCenter - 71, y, true);
|
||||
}
|
||||
}
|
||||
|
||||
// re-try if the GPS doesn't return a fix in time
|
||||
function GPStookTooLong() {
|
||||
Bangle.setGPSPower(false, APP_NAME);
|
||||
if (gpsTimeout) clearTimeout(gpsTimeout);
|
||||
gpsTimeout = undefined;
|
||||
|
||||
showUpdateAVWXstatus('X');
|
||||
|
||||
if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); }
|
||||
}
|
||||
|
||||
// update the METAR info
|
||||
function updateAVWX() {
|
||||
if (avwxTimeout) clearTimeout(avwxTimeout);
|
||||
avwxTimeout = undefined;
|
||||
if (gpsTimeout) clearTimeout(gpsTimeout);
|
||||
gpsTimeout = undefined;
|
||||
|
||||
METAR = '\nGetting GPS fix';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
if (! NRF.getSecurityStatus().connected) {
|
||||
// if Bluetooth is NOT connected, try again in 5min
|
||||
showUpdateAVWXstatus('X');
|
||||
avwxTimeout = setTimeout(updateAVWX, 5 * 60000);
|
||||
return;
|
||||
}
|
||||
|
||||
showUpdateAVWXstatus('GPS');
|
||||
if (! METAR) {
|
||||
METAR = '\nUpdating METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
}
|
||||
drawAVWX();
|
||||
|
||||
gpsTimeout = setTimeout(GPStookTooLong, 30 * 60000);
|
||||
Bangle.setGPSPower(true, APP_NAME);
|
||||
Bangle.on('GPS', fix => {
|
||||
// prevent multiple, simultaneous requests
|
||||
|
|
@ -109,12 +146,18 @@ function updateAVWX() {
|
|||
|
||||
if ('fix' in fix && fix.fix != 0 && fix.satellites >= 4) {
|
||||
Bangle.setGPSPower(false, APP_NAME);
|
||||
if (gpsTimeout) clearTimeout(gpsTimeout);
|
||||
gpsTimeout = undefined;
|
||||
|
||||
let lat = fix.lat;
|
||||
let lon = fix.lon;
|
||||
|
||||
METAR = '\nRequesting METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
showUpdateAVWXstatus('AVWX');
|
||||
if (! METAR) {
|
||||
METAR = '\nUpdating METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
}
|
||||
drawAVWX();
|
||||
|
||||
// get latest METAR from nearest airport (via AVWX API)
|
||||
|
|
@ -146,6 +189,7 @@ function updateAVWX() {
|
|||
METARts = undefined;
|
||||
}
|
||||
|
||||
showUpdateAVWXstatus('');
|
||||
drawAVWX();
|
||||
AVWXrequest = undefined;
|
||||
|
||||
|
|
@ -155,6 +199,7 @@ function updateAVWX() {
|
|||
METAR = 'ERR: ' + error;
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
showUpdateAVWXstatus('');
|
||||
drawAVWX();
|
||||
AVWXrequest = undefined;
|
||||
});
|
||||
|
|
@ -268,10 +313,10 @@ Bangle.on('tap', data => {
|
|||
case 'bottom':
|
||||
scrollAVWX(1);
|
||||
break;
|
||||
case 'left':
|
||||
case 'right':
|
||||
// toggle seconds display on double taps left or right
|
||||
if (data.double) {
|
||||
case 'front':
|
||||
// toggle seconds display on double tap on front/watch-face
|
||||
// (if watch is un-locked)
|
||||
if (data.double && ! Bangle.isLocked()) {
|
||||
if (settings.showSeconds) {
|
||||
clearInterval(secondsInterval);
|
||||
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
||||
|
|
@ -295,7 +340,7 @@ Bangle.loadWidgets();
|
|||
Bangle.drawWidgets();
|
||||
|
||||
// draw static separator line
|
||||
y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight;
|
||||
let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight;
|
||||
g.setColor(separatorColour);
|
||||
g.drawLine(0, y, g.getWidth(), y);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "aviatorclk",
|
||||
"name": "Aviator Clock",
|
||||
"shortName":"AV8R Clock",
|
||||
"version":"1.01",
|
||||
"version":"1.02",
|
||||
"description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport",
|
||||
"icon": "aviatorclk.png",
|
||||
"screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }],
|
||||
|
|
|
|||
|
|
@ -3,4 +3,13 @@
|
|||
0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false
|
||||
0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher
|
||||
0.05: Fixes step count not resetting after a new day starts
|
||||
0.06 Added clockbackground app functionality
|
||||
0.06: Added clockbackground app functionality
|
||||
0.07: Allow custom backgrounds per boxclk config and from the clockbg module
|
||||
0.08: Improves performance, responsiveness, and bug fixes
|
||||
- [+] Added box size caching to reduce calculations
|
||||
- [+] Improved step count with real-time updates
|
||||
- [+] Improved battery level update logic to reduce unnecessary refreshes
|
||||
- [+] Fixed optional seconds not displaying in time
|
||||
- [+] Fixed drag handler by adding E.stopEventPropagation()
|
||||
- [+] General code optimization and cleanup
|
||||
0.09: Revised event handler code
|
||||
|
|
@ -1,58 +1,123 @@
|
|||
{
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 1. Module dependencies and initial configurations
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 1. Module dependencies and initial configurations
|
||||
let background = require("clockbg");
|
||||
let storage = require("Storage");
|
||||
let locale = require("locale");
|
||||
let widgets = require("widget_utils");
|
||||
let date = new Date();
|
||||
let bgImage;
|
||||
let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0;
|
||||
let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json';
|
||||
// Add a condition to check if the file exists, if it does not, default to 'boxclk.json'
|
||||
if (!storage.read(fileName)) {
|
||||
fileName = 'boxclk.json';
|
||||
}
|
||||
let boxesConfig = storage.readJSON(fileName, 1) || {};
|
||||
let boxes = {};
|
||||
let boxPos = {};
|
||||
let isDragging = {};
|
||||
let wasDragging = {};
|
||||
let isDragging = false;
|
||||
let doubleTapTimer = null;
|
||||
let g_setColor;
|
||||
|
||||
let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA"));
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 2. Graphical and visual configurations
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 2. Graphical and visual configurations
|
||||
let w = g.getWidth();
|
||||
let h = g.getHeight();
|
||||
let totalWidth, totalHeight;
|
||||
let drawTimeout;
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 3. Touchscreen Handlers
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
let touchHandler;
|
||||
let dragHandler;
|
||||
let movementDistance = 0;
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 4. Font loading function
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
// 3. Event handlers
|
||||
let touchHandler = function(zone, e) {
|
||||
let boxTouched = false;
|
||||
let touchedBox = null;
|
||||
|
||||
for (let boxKey in boxes) {
|
||||
if (touchInText(e, boxes[boxKey])) {
|
||||
touchedBox = boxKey;
|
||||
boxTouched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxTouched) {
|
||||
// Toggle the selected state of the touched box
|
||||
boxes[touchedBox].selected = !boxes[touchedBox].selected;
|
||||
|
||||
// Update isDragging based on whether any box is selected
|
||||
isDragging = Object.values(boxes).some(box => box.selected);
|
||||
|
||||
if (isDragging) {
|
||||
widgets.hide();
|
||||
} else {
|
||||
deselectAllBoxes();
|
||||
}
|
||||
} else {
|
||||
// If tapped outside any box, deselect all boxes
|
||||
deselectAllBoxes();
|
||||
}
|
||||
|
||||
// Always redraw after a touch event
|
||||
draw();
|
||||
|
||||
// Handle double tap for saving
|
||||
if (!boxTouched && !isDragging) {
|
||||
if (doubleTapTimer) {
|
||||
clearTimeout(doubleTapTimer);
|
||||
doubleTapTimer = null;
|
||||
for (let boxKey in boxes) {
|
||||
boxesConfig[boxKey].boxPos.x = (boxes[boxKey].pos.x / w).toFixed(3);
|
||||
boxesConfig[boxKey].boxPos.y = (boxes[boxKey].pos.y / h).toFixed(3);
|
||||
}
|
||||
storage.write(fileName, JSON.stringify(boxesConfig));
|
||||
displaySaveIcon();
|
||||
return;
|
||||
}
|
||||
|
||||
doubleTapTimer = setTimeout(() => {
|
||||
doubleTapTimer = null;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
let dragHandler = function(e) {
|
||||
if (!isDragging) return;
|
||||
|
||||
// Stop propagation of the drag event to prevent other handlers
|
||||
E.stopEventPropagation();
|
||||
|
||||
for (let key in boxes) {
|
||||
if (boxes[key].selected) {
|
||||
let boxItem = boxes[key];
|
||||
calcBoxSize(boxItem);
|
||||
let newX = boxItem.pos.x + e.dx;
|
||||
let newY = boxItem.pos.y + e.dy;
|
||||
|
||||
if (newX - boxItem.cachedSize.width / 2 >= 0 &&
|
||||
newX + boxItem.cachedSize.width / 2 <= w &&
|
||||
newY - boxItem.cachedSize.height / 2 >= 0 &&
|
||||
newY + boxItem.cachedSize.height / 2 <= h) {
|
||||
boxItem.pos.x = newX;
|
||||
boxItem.pos.y = newY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
};
|
||||
|
||||
let stepHandler = function(up) {
|
||||
if (boxes.step && !isDragging) {
|
||||
boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps);
|
||||
boxes.step.cachedSize = null;
|
||||
draw();
|
||||
}
|
||||
};
|
||||
|
||||
let lockHandler = function(isLocked) {
|
||||
if (isLocked) {
|
||||
deselectAllBoxes();
|
||||
draw();
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Font loading function
|
||||
let loadCustomFont = function() {
|
||||
Graphics.prototype.setFontBrunoAce = function() {
|
||||
// Actual height 23 (24 - 2)
|
||||
|
|
@ -60,42 +125,43 @@
|
|||
E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))),
|
||||
46,
|
||||
atob("CBEdChgYGhgaGBsaCQ=="),
|
||||
32|65536
|
||||
32 | 65536
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 5. Initial settings of boxes and their positions
|
||||
* ---------------------------------------------------------------
|
||||
// 5. Initial settings of boxes and their positions
|
||||
let isBool = (val, defaultVal) => val !== undefined ? Boolean(val) : defaultVal;
|
||||
|
||||
for (let key in boxesConfig) {
|
||||
if (key === 'bg' && boxesConfig[key].img) {
|
||||
bgImage = storage.read(boxesConfig[key].img);
|
||||
} else if (key !== 'selectedConfig') {
|
||||
boxes[key] = Object.assign({}, boxesConfig[key]);
|
||||
// Set default values for short, shortMonth, and disableSuffix
|
||||
boxes[key].short = isBool(boxes[key].short, true);
|
||||
boxes[key].shortMonth = isBool(boxes[key].shortMonth, true);
|
||||
boxes[key].disableSuffix = isBool(boxes[key].disableSuffix, false);
|
||||
|
||||
// Set box position
|
||||
boxes[key].pos = {
|
||||
x: w * boxes[key].boxPos.x,
|
||||
y: h * boxes[key].boxPos.y
|
||||
};
|
||||
// Cache box size
|
||||
boxes[key].cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Text and drawing functions
|
||||
|
||||
/*
|
||||
Overwrite the setColor function to allow the
|
||||
use of (x) in g.theme.x as a string
|
||||
in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH")
|
||||
*/
|
||||
|
||||
let boxKeys = Object.keys(boxes);
|
||||
|
||||
boxKeys.forEach((key) => {
|
||||
let boxConfig = boxes[key];
|
||||
boxPos[key] = {
|
||||
x: w * boxConfig.boxPos.x,
|
||||
y: h * boxConfig.boxPos.y
|
||||
};
|
||||
isDragging[key] = false;
|
||||
wasDragging[key] = false;
|
||||
});
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 6. Text and drawing functions
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Overwrite the setColor function to allow the
|
||||
// use of (x) in g.theme.x as a string
|
||||
// in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH")
|
||||
let modSetColor = function() {
|
||||
// Save the original setColor function
|
||||
g_setColor = g.setColor;
|
||||
// Overwrite setColor with the new function
|
||||
g.setColor = function(color) {
|
||||
if (typeof color === "string" && color in g.theme) {
|
||||
g_setColor.call(g, g.theme[color]);
|
||||
|
|
@ -106,7 +172,6 @@
|
|||
};
|
||||
|
||||
let restoreSetColor = function() {
|
||||
// Restore the original setColor function
|
||||
if (g_setColor) {
|
||||
g.setColor = g_setColor;
|
||||
}
|
||||
|
|
@ -130,25 +195,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
let calcBoxSize = function(boxItem) {
|
||||
g.reset();
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont(boxItem.font, boxItem.fontSize);
|
||||
let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline;
|
||||
let fontHeight = g.getFontHeight() + 2 * boxItem.outline;
|
||||
totalWidth = strWidth + 2 * boxItem.xPadding;
|
||||
totalHeight = fontHeight + 2 * boxItem.yPadding;
|
||||
};
|
||||
|
||||
let calcBoxPos = function(boxKey) {
|
||||
return {
|
||||
x1: boxPos[boxKey].x - totalWidth / 2,
|
||||
y1: boxPos[boxKey].y - totalHeight / 2,
|
||||
x2: boxPos[boxKey].x + totalWidth / 2,
|
||||
y2: boxPos[boxKey].y + totalHeight / 2
|
||||
};
|
||||
};
|
||||
|
||||
let displaySaveIcon = function() {
|
||||
draw(boxes);
|
||||
g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24);
|
||||
|
|
@ -159,33 +205,15 @@
|
|||
}, 2000);
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 7. String forming helper functions
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
let isBool = function(val, defaultVal) {
|
||||
return typeof val !== 'undefined' ? Boolean(val) : defaultVal;
|
||||
};
|
||||
|
||||
// 7. String forming helper functions
|
||||
let getDate = function(short, shortMonth, disableSuffix) {
|
||||
const date = new Date();
|
||||
const dayOfMonth = date.getDate();
|
||||
const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0);
|
||||
const year = date.getFullYear();
|
||||
let suffix;
|
||||
if ([1, 21, 31].includes(dayOfMonth)) {
|
||||
suffix = "st";
|
||||
} else if ([2, 22].includes(dayOfMonth)) {
|
||||
suffix = "nd";
|
||||
} else if ([3, 23].includes(dayOfMonth)) {
|
||||
suffix = "rd";
|
||||
} else {
|
||||
suffix = "th";
|
||||
}
|
||||
let dayOfMonthStr = disableSuffix ? dayOfMonth : dayOfMonth + suffix;
|
||||
return month + " " + dayOfMonthStr + (short ? '' : (", " + year)); // not including year for short version
|
||||
let suffix = ["st", "nd", "rd"][(dayOfMonth - 1) % 10] || "th";
|
||||
let dayOfMonthStr = disableSuffix ? dayOfMonth : `${dayOfMonth}${suffix}`;
|
||||
return `${month} ${dayOfMonthStr}${short ? '' : `, ${year}`}`;
|
||||
};
|
||||
|
||||
let getDayOfWeek = function(date, short) {
|
||||
|
|
@ -198,187 +226,215 @@
|
|||
return short ? meridian[0] : meridian;
|
||||
};
|
||||
|
||||
let modString = function(boxItem, data) {
|
||||
let prefix = boxItem.prefix || '';
|
||||
let suffix = boxItem.suffix || '';
|
||||
return prefix + data + suffix;
|
||||
let formatStr = function(boxItem, data) {
|
||||
return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 8. Main draw function
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
// 8. Main draw function and update logic
|
||||
let lastDay = -1;
|
||||
const BATTERY_UPDATE_INTERVAL = 300000;
|
||||
|
||||
let draw = (function() {
|
||||
let updatePerMinute = true; // variable to track the state of time display
|
||||
let updateBoxData = function() {
|
||||
let date = new Date();
|
||||
let currentDay = date.getDate();
|
||||
let now = Date.now();
|
||||
|
||||
return function(boxes) {
|
||||
date = new Date();
|
||||
g.clear();
|
||||
background.fillRect(Bangle.appRect);
|
||||
if (boxes.time || boxes.meridian || boxes.date || boxes.dow) {
|
||||
if (boxes.time) {
|
||||
boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0));
|
||||
updatePerMinute = isBool(boxes.time.short, true);
|
||||
}
|
||||
if (boxes.meridian) {
|
||||
boxes.meridian.string = modString(boxes.meridian, locale.meridian(date, isBool(boxes.meridian.short, true)));
|
||||
}
|
||||
if (boxes.date) {
|
||||
boxes.date.string = (
|
||||
modString(boxes.date,
|
||||
getDate(isBool(boxes.date.short, true),
|
||||
isBool(boxes.date.shortMonth, true),
|
||||
isBool(boxes.date.disableSuffix, false)
|
||||
)));
|
||||
}
|
||||
if (boxes.dow) {
|
||||
boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true)));
|
||||
}
|
||||
if (boxes.batt) {
|
||||
boxes.batt.string = modString(boxes.batt, E.getBattery());
|
||||
}
|
||||
if (boxes.step) {
|
||||
boxes.step.string = modString(boxes.step, Bangle.getHealthStatus("day").steps);
|
||||
}
|
||||
boxKeys.forEach((boxKey) => {
|
||||
let boxItem = boxes[boxKey];
|
||||
calcBoxSize(boxItem);
|
||||
const pos = calcBoxPos(boxKey);
|
||||
if (isDragging[boxKey]) {
|
||||
g.setColor(boxItem.border);
|
||||
g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||
let showSeconds = !boxes.time.short;
|
||||
let timeString = locale.time(date, 1).trim();
|
||||
if (showSeconds) {
|
||||
let seconds = date.getSeconds().toString().padStart(2, '0');
|
||||
timeString += ':' + seconds;
|
||||
}
|
||||
let newTimeString = formatStr(boxes.time, timeString);
|
||||
if (newTimeString !== boxes.time.string) {
|
||||
boxes.time.string = newTimeString;
|
||||
boxes.time.cachedSize = null;
|
||||
}
|
||||
g.drawString(
|
||||
boxItem,
|
||||
boxItem.string,
|
||||
boxPos[boxKey].x + boxItem.xOffset,
|
||||
boxPos[boxKey].y + boxItem.yOffset
|
||||
);
|
||||
});
|
||||
if (!Object.values(isDragging).some(Boolean)) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
let interval = updatePerMinute ? 60000 - (Date.now() % 60000) : 1000;
|
||||
drawTimeout = setTimeout(() => draw(boxes), interval);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 9. Helper function for touch event
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
if (boxes.meridian) {
|
||||
let newMeridianString = formatStr(boxes.meridian, locale.meridian(date, boxes.meridian.short));
|
||||
if (newMeridianString !== boxes.meridian.string) {
|
||||
boxes.meridian.string = newMeridianString;
|
||||
boxes.meridian.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
let touchInText = function(e, boxItem, boxKey) {
|
||||
if (boxes.date && currentDay !== lastDay) {
|
||||
let newDateString = formatStr(boxes.date,
|
||||
getDate(boxes.date.short,
|
||||
boxes.date.shortMonth,
|
||||
boxes.date.noSuffix)
|
||||
);
|
||||
if (newDateString !== boxes.date.string) {
|
||||
boxes.date.string = newDateString;
|
||||
boxes.date.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxes.dow) {
|
||||
let newDowString = formatStr(boxes.dow, getDayOfWeek(date, boxes.dow.short));
|
||||
if (newDowString !== boxes.dow.string) {
|
||||
boxes.dow.string = newDowString;
|
||||
boxes.dow.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
lastDay = currentDay;
|
||||
}
|
||||
|
||||
if (boxes.step) {
|
||||
let newStepCount = Bangle.getHealthStatus("day").steps;
|
||||
let newStepString = formatStr(boxes.step, newStepCount);
|
||||
if (newStepString !== boxes.step.string) {
|
||||
boxes.step.string = newStepString;
|
||||
boxes.step.cachedSize = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxes.batt) {
|
||||
if (!boxes.batt.lastUpdate || now - boxes.batt.lastUpdate >= BATTERY_UPDATE_INTERVAL) {
|
||||
let currentLevel = E.getBattery();
|
||||
if (currentLevel !== boxes.batt.lastLevel) {
|
||||
let newBattString = formatStr(boxes.batt, currentLevel);
|
||||
if (newBattString !== boxes.batt.string) {
|
||||
boxes.batt.string = newBattString;
|
||||
boxes.batt.cachedSize = null;
|
||||
boxes.batt.lastLevel = currentLevel;
|
||||
}
|
||||
}
|
||||
boxes.batt.lastUpdate = now;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let draw = function() {
|
||||
g.clear();
|
||||
|
||||
// Always draw backgrounds full screen
|
||||
if (bgImage) { // Check for bg in boxclk config
|
||||
g.drawImage(bgImage, 0, 0);
|
||||
} else { // Otherwise use clockbg module
|
||||
background.fillRect(0, 0, g.getWidth(), g.getHeight());
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
updateBoxData();
|
||||
}
|
||||
|
||||
for (let boxKey in boxes) {
|
||||
let boxItem = boxes[boxKey];
|
||||
|
||||
// Set font and alignment for each box individually
|
||||
g.setFont(boxItem.font, boxItem.fontSize);
|
||||
g.setFontAlign(0, 0);
|
||||
|
||||
calcBoxSize(boxItem);
|
||||
|
||||
const pos = calcBoxPos(boxItem);
|
||||
|
||||
if (boxItem.selected) {
|
||||
g.setColor(boxItem.border);
|
||||
g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||
}
|
||||
|
||||
g.drawString(
|
||||
boxItem,
|
||||
boxItem.string,
|
||||
boxItem.pos.x + boxItem.xOffset,
|
||||
boxItem.pos.y + boxItem.yOffset
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
let updateInterval = boxes.time && !isBool(boxes.time.short, true) ? 1000 : 60000 - (Date.now() % 60000);
|
||||
drawTimeout = setTimeout(draw, updateInterval);
|
||||
}
|
||||
};
|
||||
|
||||
// 9. Helper function for touch event
|
||||
let calcBoxPos = function(boxItem) {
|
||||
calcBoxSize(boxItem);
|
||||
const pos = calcBoxPos(boxKey);
|
||||
return {
|
||||
x1: boxItem.pos.x - boxItem.cachedSize.width / 2,
|
||||
y1: boxItem.pos.y - boxItem.cachedSize.height / 2,
|
||||
x2: boxItem.pos.x + boxItem.cachedSize.width / 2,
|
||||
y2: boxItem.pos.y + boxItem.cachedSize.height / 2
|
||||
};
|
||||
};
|
||||
|
||||
// Use cached size if available, otherwise calculate and cache
|
||||
let calcBoxSize = function(boxItem) {
|
||||
if (boxItem.cachedSize) {
|
||||
return boxItem.cachedSize;
|
||||
}
|
||||
|
||||
g.setFont(boxItem.font, boxItem.fontSize);
|
||||
g.setFontAlign(0, 0);
|
||||
|
||||
let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline;
|
||||
let fontHeight = g.getFontHeight() + 2 * boxItem.outline;
|
||||
let totalWidth = strWidth + 2 * boxItem.xPadding;
|
||||
let totalHeight = fontHeight + 2 * boxItem.yPadding;
|
||||
|
||||
boxItem.cachedSize = {
|
||||
width: totalWidth,
|
||||
height: totalHeight
|
||||
};
|
||||
|
||||
return boxItem.cachedSize;
|
||||
};
|
||||
|
||||
let touchInText = function(e, boxItem) {
|
||||
calcBoxSize(boxItem);
|
||||
const pos = calcBoxPos(boxItem);
|
||||
return e.x >= pos.x1 &&
|
||||
e.x <= pos.x2 &&
|
||||
e.y >= pos.y1 &&
|
||||
e.y <= pos.y2;
|
||||
e.x <= pos.x2 &&
|
||||
e.y >= pos.y1 &&
|
||||
e.y <= pos.y2;
|
||||
};
|
||||
|
||||
let deselectAllBoxes = function() {
|
||||
Object.keys(isDragging).forEach((boxKey) => {
|
||||
isDragging[boxKey] = false;
|
||||
});
|
||||
isDragging = false;
|
||||
for (let boxKey in boxes) {
|
||||
boxes[boxKey].selected = false;
|
||||
}
|
||||
restoreSetColor();
|
||||
widgets.show();
|
||||
widgets.swipeOn();
|
||||
modSetColor();
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 10. Setup function to configure event handlers
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 10. Setup function to configure event handlers
|
||||
let setup = function() {
|
||||
// ------------------------------------
|
||||
// Define the touchHandler function
|
||||
// ------------------------------------
|
||||
touchHandler = function(zone, e) {
|
||||
wasDragging = Object.assign({}, isDragging);
|
||||
let boxTouched = false;
|
||||
boxKeys.forEach((boxKey) => {
|
||||
if (touchInText(e, boxes[boxKey], boxKey)) {
|
||||
isDragging[boxKey] = true;
|
||||
wasDragging[boxKey] = true;
|
||||
boxTouched = true;
|
||||
}
|
||||
});
|
||||
if (!boxTouched) {
|
||||
if (!Object.values(isDragging).some(Boolean)) { // check if no boxes are being dragged
|
||||
deselectAllBoxes();
|
||||
if (doubleTapTimer) {
|
||||
clearTimeout(doubleTapTimer);
|
||||
doubleTapTimer = null;
|
||||
// Save boxesConfig on double tap outside of any box and when no boxes are being dragged
|
||||
Object.keys(boxPos).forEach((boxKey) => {
|
||||
boxesConfig[boxKey].boxPos.x = (boxPos[boxKey].x / w).toFixed(3);
|
||||
boxesConfig[boxKey].boxPos.y = (boxPos[boxKey].y / h).toFixed(3);
|
||||
});
|
||||
storage.write(fileName, JSON.stringify(boxesConfig));
|
||||
displaySaveIcon();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// if any box is being dragged, just deselect all without saving
|
||||
deselectAllBoxes();
|
||||
}
|
||||
}
|
||||
if (Object.values(wasDragging).some(Boolean) || !boxTouched) {
|
||||
draw(boxes);
|
||||
}
|
||||
doubleTapTimer = setTimeout(() => {
|
||||
doubleTapTimer = null;
|
||||
}, 500); // Increase or decrease this value based on the desired double tap timing
|
||||
movementDistance = 0;
|
||||
};
|
||||
|
||||
// ------------------------------------
|
||||
// Define the dragHandler function
|
||||
// ------------------------------------
|
||||
dragHandler = function(e) {
|
||||
// Check if any box is being dragged
|
||||
if (!Object.values(isDragging).some(Boolean)) return;
|
||||
// Calculate the movement distance
|
||||
movementDistance += Math.abs(e.dx) + Math.abs(e.dy);
|
||||
// Check if the movement distance exceeds a threshold
|
||||
if (movementDistance > 1) {
|
||||
boxKeys.forEach((boxKey) => {
|
||||
if (isDragging[boxKey]) {
|
||||
widgets.hide();
|
||||
let boxItem = boxes[boxKey];
|
||||
calcBoxSize(boxItem);
|
||||
let newX = boxPos[boxKey].x + e.dx;
|
||||
let newY = boxPos[boxKey].y + e.dy;
|
||||
if (newX - totalWidth / 2 >= 0 &&
|
||||
newX + totalWidth / 2 <= w &&
|
||||
newY - totalHeight / 2 >= 0 &&
|
||||
newY + totalHeight / 2 <= h ) {
|
||||
boxPos[boxKey].x = newX;
|
||||
boxPos[boxKey].y = newY;
|
||||
}
|
||||
const pos = calcBoxPos(boxKey);
|
||||
g.clearRect(pos.x1, pos.y1, pos.x2, pos.y2);
|
||||
}
|
||||
});
|
||||
draw(boxes);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on('lock', lockHandler);
|
||||
Bangle.on('touch', touchHandler);
|
||||
Bangle.on('drag', dragHandler);
|
||||
|
||||
|
||||
if (boxes.step) {
|
||||
boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps);
|
||||
Bangle.on('step', stepHandler);
|
||||
}
|
||||
|
||||
if (boxes.batt) {
|
||||
boxes.batt.lastLevel = E.getBattery();
|
||||
boxes.batt.string = formatStr(boxes.batt, boxes.batt.lastLevel);
|
||||
boxes.batt.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
// Remove event handlers, stop draw timer, remove custom font if used
|
||||
mode: "clock",
|
||||
remove: function() {
|
||||
// Remove event handlers, stop draw timer, remove custom font
|
||||
Bangle.removeListener('touch', touchHandler);
|
||||
Bangle.removeListener('drag', dragHandler);
|
||||
Bangle.removeListener('lock', lockHandler);
|
||||
if (boxes.step) {
|
||||
Bangle.removeListener('step', stepHandler);
|
||||
}
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
delete Graphics.prototype.setFontBrunoAce;
|
||||
|
|
@ -388,16 +444,12 @@
|
|||
widgets.show();
|
||||
}
|
||||
});
|
||||
|
||||
loadCustomFont();
|
||||
draw(boxes);
|
||||
draw();
|
||||
};
|
||||
|
||||
/**
|
||||
* ---------------------------------------------------------------
|
||||
* 11. Main execution part
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// 11. Main execution
|
||||
Bangle.loadWidgets();
|
||||
widgets.swipeOn();
|
||||
modSetColor();
|
||||
|
|
|
|||
|
|
@ -11,15 +11,15 @@
|
|||
"xOffset": 3,
|
||||
"yOffset": 0,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.33"
|
||||
"x": "0.494",
|
||||
"y": "0.739"
|
||||
}
|
||||
},
|
||||
"dow": {
|
||||
"font": "6x8",
|
||||
"fontSize": 3,
|
||||
"outline": 1,
|
||||
"color": "#5ccd73",
|
||||
"color": "bgH",
|
||||
"outlineColor": "fg",
|
||||
"border": "#f0f",
|
||||
"xPadding": -1,
|
||||
|
|
@ -27,8 +27,8 @@
|
|||
"xOffset": 2,
|
||||
"yOffset": 0,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.57"
|
||||
"x": "0.421",
|
||||
"y": "0.201"
|
||||
},
|
||||
"short": false
|
||||
},
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"font": "6x8",
|
||||
"fontSize": 2,
|
||||
"outline": 1,
|
||||
"color": "#5ccd73",
|
||||
"color": "bgH",
|
||||
"outlineColor": "fg",
|
||||
"border": "#f0f",
|
||||
"xPadding": -0.5,
|
||||
|
|
@ -44,8 +44,8 @@
|
|||
"xOffset": 1,
|
||||
"yOffset": 0,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.75"
|
||||
"x": "0.454",
|
||||
"y": "0.074"
|
||||
},
|
||||
"shortMonth": false,
|
||||
"disableSuffix": true
|
||||
|
|
@ -62,8 +62,8 @@
|
|||
"xOffset": 2,
|
||||
"yOffset": 1,
|
||||
"boxPos": {
|
||||
"x": "0.5",
|
||||
"y": "0.92"
|
||||
"x": "0.494",
|
||||
"y": "0.926"
|
||||
},
|
||||
"prefix": "Steps: "
|
||||
},
|
||||
|
|
@ -79,8 +79,8 @@
|
|||
"xOffset": 2,
|
||||
"yOffset": 2,
|
||||
"boxPos": {
|
||||
"x": "0.85",
|
||||
"y": "0.08"
|
||||
"x": "0.805",
|
||||
"y": "0.427"
|
||||
},
|
||||
"suffix": "%"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "boxclk",
|
||||
"name": "Box Clock",
|
||||
"version": "0.05",
|
||||
"version": "0.09",
|
||||
"description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background",
|
||||
"icon": "app.png",
|
||||
"dependencies" : { "clockbg":"module" },
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"data": [
|
||||
{"name":"boxclk.json","url":"boxclk.json"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.3 KiB |
|
|
@ -5,3 +5,4 @@
|
|||
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
|
||||
0.06: Bangle.js 2: Exit with a short press of the physical button
|
||||
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen
|
||||
0.08: truncate long numbers (and append '...' to displayed value)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
g.clear();
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
|
||||
var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R';
|
||||
var RIGHT_MARGIN = 20;
|
||||
var DEFAULT_SELECTION_NUMBERS = '5';
|
||||
var RESULT_HEIGHT = 40;
|
||||
var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14);
|
||||
var COLORS = {
|
||||
// [normal, selected]
|
||||
DEFAULT: ['#7F8183', '#A6A6A7'],
|
||||
|
|
@ -88,28 +88,11 @@ function prepareScreen(screen, grid, defaultColor) {
|
|||
}
|
||||
|
||||
function drawKey(name, k, selected) {
|
||||
var rMargin = 0;
|
||||
var bMargin = 0;
|
||||
var color = k.color || COLORS.DEFAULT;
|
||||
g.setColor(color[selected ? 1 : 0]);
|
||||
g.setFont('Vector', 20).setFontAlign(0,0);
|
||||
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
|
||||
g.setColor(-1);
|
||||
// correct margins to center the texts
|
||||
if (name == '0') {
|
||||
rMargin = (RIGHT_MARGIN * 2) - 7;
|
||||
} else if (name === '/') {
|
||||
rMargin = 5;
|
||||
} else if (name === '*') {
|
||||
bMargin = 5;
|
||||
rMargin = 3;
|
||||
} else if (name === '-') {
|
||||
rMargin = 3;
|
||||
} else if (name === 'R' || name === 'N') {
|
||||
rMargin = k.val === 'C' ? 0 : -9;
|
||||
} else if (name === '%') {
|
||||
rMargin = -3;
|
||||
}
|
||||
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
|
||||
}
|
||||
|
||||
|
|
@ -138,29 +121,21 @@ function drawGlobal() {
|
|||
screen[k] = specials[k];
|
||||
}
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||
}
|
||||
function drawNumbers() {
|
||||
screen = numbers;
|
||||
screenColor = COLORS.DEFAULT;
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_NUMBERS;
|
||||
var prevSelected = DEFAULT_SELECTION_NUMBERS;
|
||||
}
|
||||
function drawOperators() {
|
||||
screen = operators;
|
||||
screenColor =COLORS.OPERATOR;
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_OPERATORS;
|
||||
var prevSelected = DEFAULT_SELECTION_OPERATORS;
|
||||
}
|
||||
function drawSpecials() {
|
||||
screen = specials;
|
||||
screenColor = COLORS.SPECIAL;
|
||||
drawKeys();
|
||||
var selected = DEFAULT_SELECTION_SPECIALS;
|
||||
var prevSelected = DEFAULT_SELECTION_SPECIALS;
|
||||
}
|
||||
|
||||
function getIntWithPrecision(x) {
|
||||
|
|
@ -218,8 +193,6 @@ function doMath(x, y, operator) {
|
|||
}
|
||||
|
||||
function displayOutput(num) {
|
||||
var len;
|
||||
var minusMarge = 0;
|
||||
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
|
||||
g.setColor(-1);
|
||||
if (num === Infinity || num === -Infinity || isNaN(num)) {
|
||||
|
|
@ -230,9 +203,7 @@ function displayOutput(num) {
|
|||
num = '-INFINITY';
|
||||
} else {
|
||||
num = 'NOT A NUMBER';
|
||||
minusMarge = -25;
|
||||
}
|
||||
len = (num + '').length;
|
||||
currNumber = null;
|
||||
results = null;
|
||||
isDecimal = false;
|
||||
|
|
@ -261,6 +232,9 @@ function displayOutput(num) {
|
|||
num = num.toString();
|
||||
num = num.replace("-","- "); // fix padding for '-'
|
||||
g.setFont('7x11Numeric7Seg', 2);
|
||||
if (num.length > RESULT_MAX_LEN) {
|
||||
num = num.substr(0, RESULT_MAX_LEN - 1)+'...';
|
||||
}
|
||||
}
|
||||
g.setFontAlign(1,0);
|
||||
g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "calculator",
|
||||
"name": "Calculator",
|
||||
"shortName": "Calculator",
|
||||
"version": "0.07",
|
||||
"version": "0.08",
|
||||
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
|
||||
"icon": "calculator.png",
|
||||
"screenshots": [{"url":"screenshot_calculator.png"}],
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.20: First release
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Dutch Clock
|
||||
This clock shows the time, in words, the way a Dutch person might respond when asked what time it is. Useful when learning Dutch and/or pretending to know Dutch.
|
||||
|
||||
Dedicated to my wife, who will sometimes insist I tell her exactly what time it says on the watch and not just an approximation.
|
||||
|
||||
## Options
|
||||
- Three modes:
|
||||
- exact time ("zeven voor half zes / twee voor tien")
|
||||
- approximate time, rounded to the nearest 5-minute mark ("bijna vijf voor half zes / tegen tienen") (the default)
|
||||
- hybrid mode, rounded when close to the quarter marks and exact otherwise ("zeven voor half zes / tegen tienen")
|
||||
- Option to turn top widgets on/off (on by default)
|
||||
- Option to show digital time at the bottom (off by default)
|
||||
- Option to show the date at the bottom (on by default)
|
||||
|
||||
The app respects top and bottom widgets, but it gets a bit crowded when you add the time/date and you also have bottom widgets turned on.
|
||||
|
||||
When you turn widgets off, you can still see the top widgets by swiping down from the top.
|
||||
|
||||
## Screenshots
|
||||

|
||||

|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgP/AE0/Ao/4sccAoX79NtAofttIFD8dsAof3t1/GZ397oGE/YLE6IFDloFE1vbAoeNAondAon/z4FE356U/nNxhZC/drlpLDscNAoX4ue9C4f3L4oAKt4FEQ4qxE/0skIGDtg7DAoNtAocsAogAX94POA"))
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
// Load libraries
|
||||
const storage = require("Storage");
|
||||
const locale = require('locale');
|
||||
const widget_utils = require('widget_utils');
|
||||
|
||||
// Define constants
|
||||
const DATETIME_SPACING_HEIGHT = 5;
|
||||
const TIME_HEIGHT = 8;
|
||||
const DATE_HEIGHT = 8;
|
||||
const BOTTOM_SPACING = 2;
|
||||
|
||||
const MINS_IN_HOUR = 60;
|
||||
const MINS_IN_DAY = 24 * MINS_IN_HOUR;
|
||||
|
||||
const VARIANT_EXACT = 'exact';
|
||||
const VARIANT_APPROXIMATE = 'approximate';
|
||||
const VARIANT_HYBRID = 'hybrid';
|
||||
|
||||
const DEFAULTS_FILE = "dutchclock.default.json";
|
||||
const SETTINGS_FILE = "dutchclock.json";
|
||||
|
||||
// Load settings
|
||||
const settings = Object.assign(
|
||||
storage.readJSON(DEFAULTS_FILE, true) || {},
|
||||
storage.readJSON(SETTINGS_FILE, true) || {}
|
||||
);
|
||||
|
||||
// Define global variables
|
||||
const textBox = {};
|
||||
let date, mins;
|
||||
|
||||
// Define functions
|
||||
function initialize() {
|
||||
// Reset the state of the graphics library
|
||||
g.clear(true);
|
||||
|
||||
// Tell Bangle this is a clock
|
||||
Bangle.setUI("clock");
|
||||
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Show widgets, or not
|
||||
if (settings.showWidgets) {
|
||||
Bangle.drawWidgets();
|
||||
} else {
|
||||
widget_utils.swipeOn();
|
||||
}
|
||||
|
||||
const dateTimeHeight = (settings.showDate || settings.showTime ? DATETIME_SPACING_HEIGHT : 0)
|
||||
+ (settings.showDate ? DATE_HEIGHT : 0)
|
||||
+ (settings.showTime ? TIME_HEIGHT : 0);
|
||||
|
||||
Object.assign(textBox, {
|
||||
x: Bangle.appRect.x + Bangle.appRect.w / 2,
|
||||
y: Bangle.appRect.y + (Bangle.appRect.h - dateTimeHeight) / 2,
|
||||
w: Bangle.appRect.w - 2,
|
||||
h: Bangle.appRect.h - dateTimeHeight
|
||||
});
|
||||
|
||||
// draw immediately at first
|
||||
tick();
|
||||
|
||||
// now check every second
|
||||
let secondInterval = setInterval(tick, 1000);
|
||||
|
||||
// Stop updates when LCD is off, restart when on
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (secondInterval) clearInterval(secondInterval);
|
||||
secondInterval = undefined;
|
||||
if (on) {
|
||||
secondInterval = setInterval(tick, 1000);
|
||||
draw(); // draw immediately
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tick() {
|
||||
date = new Date();
|
||||
const m = (date.getHours() * MINS_IN_HOUR + date.getMinutes()) % MINS_IN_DAY;
|
||||
|
||||
if (m !== mins) {
|
||||
mins = m;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
// work out how to display the current time
|
||||
const timeLines = getTimeLines(mins);
|
||||
const bottomLines = getBottomLines();
|
||||
|
||||
g.reset().clearRect(Bangle.appRect);
|
||||
|
||||
// draw the current time (4x size 7 segment)
|
||||
setFont(timeLines);
|
||||
|
||||
g.setFontAlign(0,0); // align center top
|
||||
g.drawString(timeLines.join("\n"), textBox.x, textBox.y, false);
|
||||
|
||||
if (bottomLines.length) {
|
||||
// draw the time and/or date, in a normal font
|
||||
g.setFont("6x8");
|
||||
g.setFontAlign(0,1); // align center bottom
|
||||
// pad the date - this clears the background if the date were to change length
|
||||
g.drawString(bottomLines.join('\n'), Bangle.appRect.w / 2, Bangle.appRect.y2 - BOTTOM_SPACING, false);
|
||||
}
|
||||
}
|
||||
|
||||
function setFont(timeLines) {
|
||||
const size = textBox.h / timeLines.length;
|
||||
|
||||
g.setFont("Vector", size);
|
||||
|
||||
let width = g.stringWidth(timeLines.join('\n'));
|
||||
|
||||
if (width > textBox.w) {
|
||||
g.setFont("Vector", Math.floor(size * (textBox.w / width)));
|
||||
}
|
||||
}
|
||||
|
||||
function getBottomLines() {
|
||||
const lines = [];
|
||||
|
||||
if (settings.showTime) {
|
||||
lines.push(locale.time(date, 1));
|
||||
}
|
||||
|
||||
if (settings.showDate) {
|
||||
lines.push(locale.date(date));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function getTimeLines(m) {
|
||||
switch (settings.variant) {
|
||||
case VARIANT_EXACT:
|
||||
return getExactTimeLines(m);
|
||||
case VARIANT_APPROXIMATE:
|
||||
return getApproximateTimeLines(m);
|
||||
case VARIANT_HYBRID:
|
||||
return distanceFromNearest(15)(m) < 3
|
||||
? getApproximateTimeLines(m)
|
||||
: getExactTimeLines(m);
|
||||
default:
|
||||
console.warn(`Error in settings: unknown variant "${settings.variant}"`);
|
||||
return getExactTimeLines(m);
|
||||
}
|
||||
}
|
||||
|
||||
function getExactTimeLines(m) {
|
||||
if (m === 0) {
|
||||
return ['middernacht'];
|
||||
}
|
||||
|
||||
const hour = getHour(m);
|
||||
const minutes = getMinutes(hour.offset);
|
||||
|
||||
const lines = minutes.concat(hour.lines);
|
||||
if (lines.length === 1) {
|
||||
lines.push('uur');
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
function getApproximateTimeLines(m) {
|
||||
const roundMinutes = getRoundMinutes(m);
|
||||
|
||||
const lines = getExactTimeLines(roundMinutes.minutes);
|
||||
|
||||
return addApproximateDescription(lines, roundMinutes.offset);
|
||||
}
|
||||
|
||||
function getHour(minutes) {
|
||||
const hours = ['twaalf', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf'];
|
||||
|
||||
const h = Math.floor(minutes / MINS_IN_HOUR), m = minutes % MINS_IN_HOUR;
|
||||
|
||||
if (m <= 15) {
|
||||
return {lines: [hours[h % 12]], offset: m};
|
||||
}
|
||||
|
||||
if (m > 15 && m < 45) {
|
||||
return {
|
||||
lines: ['half', hours[(h + 1) % 12]],
|
||||
offset: m - (MINS_IN_HOUR / 2)
|
||||
};
|
||||
}
|
||||
|
||||
return {lines: [hours[(h + 1) % 12]], offset: m - MINS_IN_HOUR};
|
||||
}
|
||||
|
||||
function getMinutes(m) {
|
||||
const minutes = ['', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf', 'twaalf', 'dertien', 'veertien', 'kwart'];
|
||||
|
||||
if (m === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [minutes[Math.abs(m)], m > 0 ? 'over' : 'voor'];
|
||||
}
|
||||
|
||||
function getRoundMinutes(m) {
|
||||
const nearest = roundTo(5)(m);
|
||||
|
||||
return {
|
||||
minutes: nearest % MINS_IN_DAY,
|
||||
offset: m - nearest
|
||||
};
|
||||
}
|
||||
|
||||
function addApproximateDescription(lines, offset) {
|
||||
if (offset === 0) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (lines.length === 1 || lines[1] === 'uur') {
|
||||
const singular = lines[0];
|
||||
const plural = getPlural(singular);
|
||||
return {
|
||||
'-2': ['tegen', plural],
|
||||
'-1': ['iets voor', singular],
|
||||
'1': ['iets na', plural],
|
||||
'2': ['even na', plural]
|
||||
}[`${offset}`];
|
||||
}
|
||||
|
||||
return {
|
||||
'-2': ['bijna'].concat(lines),
|
||||
'-1': ['rond'].concat(lines),
|
||||
'1': ['iets na'].concat(lines),
|
||||
'2': lines.concat(['geweest'])
|
||||
}[`${offset}`];
|
||||
}
|
||||
|
||||
function getPlural(h) {
|
||||
return {
|
||||
middernacht: 'middernacht',
|
||||
een: 'enen',
|
||||
twee: 'tweeën',
|
||||
drie: 'drieën',
|
||||
vijf: 'vijven',
|
||||
zes: 'zessen',
|
||||
elf: 'elven',
|
||||
twaalf: 'twaalven'
|
||||
}[h] || `${h}en`;
|
||||
}
|
||||
|
||||
function distanceFromNearest(x) {
|
||||
return n => Math.abs(n - roundTo(x)(n));
|
||||
}
|
||||
|
||||
function roundTo(x) {
|
||||
return n => Math.round(n / x) * x;
|
||||
}
|
||||
|
||||
// Let's go
|
||||
initialize();
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"variant": "approximate",
|
||||
"showWidgets": true,
|
||||
"showTime": false,
|
||||
"showDate": true
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"id": "dutchclock",
|
||||
"name": "Dutch Clock",
|
||||
"shortName":"Dutch Clock",
|
||||
"icon": "app.png",
|
||||
"version":"0.20",
|
||||
"description": "A clock that displays the time the way a Dutch person would respond when asked what time it is.",
|
||||
"type": "clock",
|
||||
"tags": "clock,dutch,text",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"screenshots": [
|
||||
{"url":"screenshotbangle1-2.png"},
|
||||
{"url":"screenshotbangle2.png"},
|
||||
{"url":"screenshotbangle1.png"}
|
||||
],
|
||||
"storage": [
|
||||
{"name":"dutchclock.app.js","url":"app.js"},
|
||||
{"name":"dutchclock.settings.js","url":"settings.js"},
|
||||
{"name":"dutchclock.default.json","url":"default.json"},
|
||||
{"name":"dutchclock.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"dutchclock.json"}
|
||||
],
|
||||
"readme":"README.md"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,73 @@
|
|||
(function(back) {
|
||||
const storage = require("Storage");
|
||||
|
||||
const VARIANT_EXACT = 'exact';
|
||||
const VARIANT_APPROXIMATE = 'approximate';
|
||||
const VARIANT_HYBRID = 'hybrid';
|
||||
|
||||
const DEFAULTS_FILE = "dutchclock.default.json";
|
||||
const SETTINGS_FILE = "dutchclock.json";
|
||||
|
||||
// Load settings
|
||||
const settings = Object.assign(
|
||||
storage.readJSON(DEFAULTS_FILE, true) || {},
|
||||
storage.readJSON(SETTINGS_FILE, true) || {}
|
||||
);
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(SETTINGS_FILE, settings);
|
||||
}
|
||||
|
||||
function writeSetting(setting, value) {
|
||||
settings[setting] = value;
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
function writeOption(setting, value) {
|
||||
writeSetting(setting, value);
|
||||
showMainMenu();
|
||||
}
|
||||
|
||||
function getOption(label, setting, value) {
|
||||
return {
|
||||
title: label,
|
||||
value: settings[setting] === value,
|
||||
onchange: () => {
|
||||
writeOption(setting, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
function showMainMenu() {
|
||||
const mainMenu = [
|
||||
getOption('Exact', 'variant', VARIANT_EXACT),
|
||||
getOption('Approximate', 'variant', VARIANT_APPROXIMATE),
|
||||
getOption('Hybrid', 'variant', VARIANT_HYBRID),
|
||||
{
|
||||
title: 'Show widgets?',
|
||||
value: settings.showWidgets,
|
||||
onchange: v => writeSetting('showWidgets', v)
|
||||
},
|
||||
{
|
||||
title: 'Show time?',
|
||||
value: settings.showTime,
|
||||
onchange: v => writeSetting('showTime', v)
|
||||
},
|
||||
{
|
||||
title: 'Show date?',
|
||||
value: settings.showDate,
|
||||
onchange: v => writeSetting('showDate', v)
|
||||
}
|
||||
];
|
||||
|
||||
mainMenu[""] = {
|
||||
title : "Dutch Clock",
|
||||
back: back
|
||||
};
|
||||
|
||||
E.showMenu(mainMenu);
|
||||
}
|
||||
|
||||
showMainMenu();
|
||||
})
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Handle AM/PM time in the "set target" menu. Add yesterday/today/tomorrow when showing target date to improve readability.
|
||||
0.03: Add option to set clock as default, handle DST in day/month/year mode
|
||||
0.04: Use new pickers from the more_pickers library, add settings to display seconds never/unlocked/always
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
# Elapsed Time Clock
|
||||
A clock that calculates the time difference between now (in blue/cyan) and any given target date (in red/orange).
|
||||
|
||||
The results is show in years, months, days, hours, minutes, seconds. To save battery life, the seconds are shown only when the watch is unlocked, or can be disabled entirely.
|
||||
The results is show in years, months, days, hours, minutes, seconds. The seconds can be shown:
|
||||
- always
|
||||
- when the watch is unlocked
|
||||
- never.
|
||||
|
||||
The time difference is positive if the target date is in the past and negative if it is in the future.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,20 @@ var now = new Date();
|
|||
|
||||
var settings = Object.assign({
|
||||
// default values
|
||||
displaySeconds: true,
|
||||
displaySeconds: 1,
|
||||
displayMonthsYears: true,
|
||||
dateFormat: 0,
|
||||
time24: true
|
||||
}, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {});
|
||||
|
||||
var temp_displaySeconds = settings.displaySeconds;
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(APP_NAME + ".settings.json", settings);
|
||||
}
|
||||
|
||||
if (typeof settings.displaySeconds === 'boolean') {
|
||||
settings.displaySeconds = 1;
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
var data = Object.assign({
|
||||
// default values
|
||||
|
|
@ -49,17 +56,12 @@ function writeData() {
|
|||
require('Storage').writeJSON(APP_NAME + ".data.json", data);
|
||||
}
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(APP_NAME + ".settings.json", settings);
|
||||
temp_displaySeconds = settings.temp_displaySeconds;
|
||||
}
|
||||
|
||||
let inMenu = false;
|
||||
|
||||
Bangle.on('touch', function (zone, e) {
|
||||
if (!inMenu && e.y > 24) {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
E.showMenu(menu);
|
||||
showMainMenu();
|
||||
inMenu = true;
|
||||
}
|
||||
});
|
||||
|
|
@ -112,115 +114,151 @@ function formatDateTime(date, dateFormat, time24, showSeconds) {
|
|||
return formattedDateTime;
|
||||
}
|
||||
|
||||
function formatHourToAMPM(h){
|
||||
function formatHourToAMPM(h) {
|
||||
var ampm = (h >= 12 ? 'PM' : 'AM');
|
||||
var h_ampm = h % 12;
|
||||
h_ampm = (h_ampm == 0 ? 12 : h_ampm);
|
||||
return `${h_ampm} ${ampm}`
|
||||
return `${h_ampm}\n${ampm}`;
|
||||
}
|
||||
|
||||
function howManyDaysInMonth(month, year) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
function getDatePickerObject() {
|
||||
switch (settings.dateFormat) {
|
||||
case 0:
|
||||
return {
|
||||
back: showMainMenu,
|
||||
title: "Date",
|
||||
separator_1: "/",
|
||||
separator_2: "/",
|
||||
|
||||
function handleExceedingDay() {
|
||||
var maxDays = howManyDaysInMonth(data.target.M, data.target.Y);
|
||||
menu.Day.max = maxDays;
|
||||
if (data.target.D > maxDays) {
|
||||
menu.Day.value = maxDays;
|
||||
data.target.D = maxDays;
|
||||
value_1: data.target.D,
|
||||
min_1: 1, max_1: 31, step_1: 1, wrap_1: true,
|
||||
|
||||
value_2: data.target.M,
|
||||
min_2: 1, max_2: 12, step_2: 1, wrap_2: true,
|
||||
|
||||
value_3: data.target.Y,
|
||||
min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true,
|
||||
|
||||
format_1: function (v_1) { return (pad2(v_1)); },
|
||||
format_2: function (v_2) { return (pad2(v_2)); },
|
||||
onchange: function (v_1, v_2, v_3) { data.target.D = v_1; data.target.M = v_2; data.target.Y = v_3; setTarget(true); }
|
||||
};
|
||||
|
||||
case 1:
|
||||
return {
|
||||
back: showMainMenu,
|
||||
title: "Date",
|
||||
separator_1: "/",
|
||||
separator_2: "/",
|
||||
|
||||
value_1: data.target.M,
|
||||
min_1: 1, max_1: 12, step_1: 1, wrap_1: true,
|
||||
|
||||
value_2: data.target.D,
|
||||
min_2: 1, max_2: 31, step_2: 1, wrap_2: true,
|
||||
|
||||
value_3: data.target.Y,
|
||||
min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true,
|
||||
|
||||
format_1: function (v_1) { return (pad2(v_1)); },
|
||||
format_2: function (v_2) { return (pad2(v_2)); },
|
||||
onchange: function (v_1, v_2, v_3) { data.target.M = v_1; data.target.D = v_2; data.target.Y = v_3; setTarget(true); }
|
||||
};
|
||||
|
||||
case 2:
|
||||
return {
|
||||
back: showMainMenu,
|
||||
title: "Date",
|
||||
separator_1: "-",
|
||||
separator_2: "-",
|
||||
|
||||
value_1: data.target.Y,
|
||||
min_1: 1900, max_1: 2100, step_1: 1, wrap_1: true,
|
||||
|
||||
value_2: data.target.M,
|
||||
min_2: 1, max_2: 12, step_2: 1, wrap_2: true,
|
||||
|
||||
value_3: data.target.D,
|
||||
min_3: 1, max_3: 31, step_3: 1, wrap_3: true,
|
||||
|
||||
format_1: function (v_1) { return (pad2(v_1)); },
|
||||
format_2: function (v_2) { return (pad2(v_2)); },
|
||||
onchange: function (v_1, v_2, v_3) { data.target.Y = v_1; data.target.M = v_2; data.target.D = v_3; setTarget(true); }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var menu = {
|
||||
"": {
|
||||
"title": "Set target",
|
||||
back: function () {
|
||||
E.showMenu();
|
||||
Bangle.setUI("clock");
|
||||
inMenu = false;
|
||||
draw();
|
||||
}
|
||||
},
|
||||
'Day': {
|
||||
value: data.target.D,
|
||||
min: 1, max: 31, wrap: true,
|
||||
onchange: v => {
|
||||
data.target.D = v;
|
||||
}
|
||||
},
|
||||
'Month': {
|
||||
value: data.target.M,
|
||||
min: 1, max: 12, noList: true, wrap: true,
|
||||
onchange: v => {
|
||||
data.target.M = v;
|
||||
handleExceedingDay();
|
||||
}
|
||||
},
|
||||
'Year': {
|
||||
value: data.target.Y,
|
||||
min: 1900, max: 2100,
|
||||
onchange: v => {
|
||||
data.target.Y = v;
|
||||
handleExceedingDay();
|
||||
}
|
||||
},
|
||||
'Hours': {
|
||||
value: data.target.h,
|
||||
min: 0, max: 23, wrap: true,
|
||||
onchange: v => {
|
||||
data.target.h = v;
|
||||
},
|
||||
format: function (v) {return(settings.time24 ? pad2(v) : formatHourToAMPM(v))}
|
||||
},
|
||||
'Minutes': {
|
||||
value: data.target.m,
|
||||
min: 0, max: 59, wrap: true,
|
||||
onchange: v => {
|
||||
data.target.m = v;
|
||||
},
|
||||
format: function (v) { return pad2(v); }
|
||||
},
|
||||
'Seconds': {
|
||||
value: data.target.s,
|
||||
min: 0, max: 59, wrap: true,
|
||||
onchange: v => {
|
||||
data.target.s = v;
|
||||
},
|
||||
format: function (v) { return pad2(v); }
|
||||
},
|
||||
'Save': function () {
|
||||
E.showMenu();
|
||||
inMenu = false;
|
||||
Bangle.setUI("clock");
|
||||
setTarget(true);
|
||||
writeSettings();
|
||||
temp_displaySeconds = settings.displaySeconds;
|
||||
updateQueueMillis(settings.displaySeconds);
|
||||
draw();
|
||||
},
|
||||
'Reset': function () {
|
||||
E.showMenu();
|
||||
inMenu = false;
|
||||
Bangle.setUI("clock");
|
||||
setTarget(false);
|
||||
updateQueueMillis(settings.displaySeconds);
|
||||
draw();
|
||||
},
|
||||
'Set clock as default': function () {
|
||||
setClockAsDefault();
|
||||
E.showAlert("Elapsed Time was set as default").then(function() {
|
||||
E.showMenu();
|
||||
inMenu = false;
|
||||
Bangle.setUI("clock");
|
||||
draw();
|
||||
});
|
||||
}
|
||||
};
|
||||
function getTimePickerObject() {
|
||||
var timePickerObject = {
|
||||
back: showMainMenu,
|
||||
title: "Time",
|
||||
separator_1: ":",
|
||||
separator_2: ":",
|
||||
|
||||
function setClockAsDefault(){
|
||||
value_1: data.target.h,
|
||||
min_1: 0, max_1: 23, step_1: 1, wrap_1: true,
|
||||
|
||||
value_2: data.target.m,
|
||||
min_2: 0, max_2: 59, step_2: 1, wrap_2: true,
|
||||
|
||||
value_3: data.target.s,
|
||||
min_3: 0, max_3: 59, step_3: 1, wrap_3: true,
|
||||
|
||||
format_2: function (v_2) { return (pad2(v_2)); },
|
||||
format_3: function (v_3) { return (pad2(v_3)); },
|
||||
onchange: function (v_1, v_2, v_3) { data.target.h = v_1; data.target.m = v_2; data.target.s = v_3; setTarget(true); },
|
||||
};
|
||||
|
||||
if (settings.time24) {
|
||||
timePickerObject.format_1 = function (v_1) { return (pad2(v_1)); };
|
||||
} else {
|
||||
timePickerObject.format_1 = function (v_1) { return (formatHourToAMPM(v_1)); };
|
||||
}
|
||||
|
||||
return timePickerObject;
|
||||
}
|
||||
|
||||
function showMainMenu() {
|
||||
E.showMenu({
|
||||
"": {
|
||||
"title": "Set target",
|
||||
back: function () {
|
||||
E.showMenu();
|
||||
Bangle.setUI("clock");
|
||||
inMenu = false;
|
||||
draw();
|
||||
}
|
||||
},
|
||||
'Date': {
|
||||
value: formatDateTime(target, settings.dateFormat, settings.time24, true).date,
|
||||
onchange: function () { require("more_pickers").triplePicker(getDatePickerObject()); }
|
||||
},
|
||||
'Time': {
|
||||
value: formatDateTime(target, settings.dateFormat, settings.time24, true).time,
|
||||
onchange: function () { require("more_pickers").triplePicker(getTimePickerObject()); }
|
||||
},
|
||||
'Reset': function () {
|
||||
E.showMenu();
|
||||
inMenu = false;
|
||||
Bangle.setUI("clock");
|
||||
setTarget(false);
|
||||
draw();
|
||||
},
|
||||
'Set clock as default': function () {
|
||||
setClockAsDefault();
|
||||
E.showAlert("Elapsed Time was set as default").then(function () {
|
||||
E.showMenu();
|
||||
inMenu = false;
|
||||
Bangle.setUI("clock");
|
||||
draw();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setClockAsDefault() {
|
||||
let storage = require('Storage');
|
||||
let settings = storage.readJSON('setting.json',true)||{clock:null};
|
||||
let settings = storage.readJSON('setting.json', true) || { clock: null };
|
||||
settings.clock = "elapsed_t.app.js";
|
||||
storage.writeJSON('setting.json', settings);
|
||||
}
|
||||
|
|
@ -238,26 +276,21 @@ function setTarget(set) {
|
|||
data.target.isSet = true;
|
||||
} else {
|
||||
target = new Date();
|
||||
target.setSeconds(0);
|
||||
Object.assign(
|
||||
data,
|
||||
{
|
||||
target: {
|
||||
isSet: false,
|
||||
Y: now.getFullYear(),
|
||||
M: now.getMonth() + 1, // Month is zero-based, so add 1
|
||||
D: now.getDate(),
|
||||
h: now.getHours(),
|
||||
m: now.getMinutes(),
|
||||
Y: target.getFullYear(),
|
||||
M: target.getMonth() + 1, // Month is zero-based, so add 1
|
||||
D: target.getDate(),
|
||||
h: target.getHours(),
|
||||
m: target.getMinutes(),
|
||||
s: 0
|
||||
}
|
||||
}
|
||||
);
|
||||
menu.Day.value = data.target.D;
|
||||
menu.Month.value = data.target.M;
|
||||
menu.Year.value = data.target.Y;
|
||||
menu.Hours.value = data.target.h;
|
||||
menu.Minutes.value = data.target.m;
|
||||
menu.Seconds.value = 0;
|
||||
}
|
||||
|
||||
writeData();
|
||||
|
|
@ -267,8 +300,8 @@ var target;
|
|||
setTarget(data.target.isSet);
|
||||
|
||||
var drawTimeout;
|
||||
var queueMillis = 1000;
|
||||
|
||||
var temp_displaySeconds;
|
||||
var queueMillis;
|
||||
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
|
|
@ -283,27 +316,25 @@ function queueDraw() {
|
|||
}, delay);
|
||||
}
|
||||
|
||||
function updateQueueMillis(displaySeconds) {
|
||||
function updateQueueMillisAndDraw(displaySeconds) {
|
||||
temp_displaySeconds = displaySeconds;
|
||||
if (displaySeconds) {
|
||||
queueMillis = 1000;
|
||||
} else {
|
||||
queueMillis = 60000;
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
Bangle.on('lock', function (on, reason) {
|
||||
if (inMenu) { // if already in a menu, nothing to do
|
||||
if (inMenu || settings.displaySeconds == 0 || settings.displaySeconds == 2) { // if already in a menu, or always/never show seconds, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
if (on) { // screen is locked
|
||||
temp_displaySeconds = false;
|
||||
updateQueueMillis(false);
|
||||
draw();
|
||||
updateQueueMillisAndDraw(false);
|
||||
} else { // screen is unlocked
|
||||
temp_displaySeconds = settings.displaySeconds;
|
||||
updateQueueMillis(temp_displaySeconds);
|
||||
draw();
|
||||
updateQueueMillisAndDraw(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -335,18 +366,21 @@ function diffToTarget() {
|
|||
var end;
|
||||
|
||||
if (now > target) {
|
||||
start = target;
|
||||
end = now;
|
||||
start = new Date(target.getTime());
|
||||
end = new Date(now.getTime());
|
||||
} else {
|
||||
start = now;
|
||||
end = target;
|
||||
start = new Date(now.getTime());
|
||||
end = new Date(target.getTime());
|
||||
}
|
||||
|
||||
// Adjust for DST
|
||||
end.setMinutes(end.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset());
|
||||
|
||||
diff.Y = end.getFullYear() - start.getFullYear();
|
||||
diff.M = end.getMonth() - start.getMonth();
|
||||
diff.D = end.getDate() - start.getDate();
|
||||
diff.hh = end.getHours() - start.getHours();
|
||||
diff.mm = end.getMinutes() - start.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset();
|
||||
diff.mm = end.getMinutes() - start.getMinutes();
|
||||
diff.ss = end.getSeconds() - start.getSeconds();
|
||||
|
||||
// Adjust negative differences
|
||||
|
|
@ -372,7 +406,6 @@ function diffToTarget() {
|
|||
diff.Y--;
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
var timeDifference = target - now;
|
||||
timeDifference = Math.abs(timeDifference);
|
||||
|
|
@ -491,4 +524,14 @@ Bangle.loadWidgets();
|
|||
Bangle.drawWidgets();
|
||||
Bangle.setUI("clock");
|
||||
|
||||
draw();
|
||||
switch (settings.displaySeconds) {
|
||||
case 0: // never
|
||||
updateQueueMillisAndDraw(false);
|
||||
break;
|
||||
case 1: // unlocked
|
||||
updateQueueMillisAndDraw(Bangle.isBacklightOn());
|
||||
break;
|
||||
case 2: // always
|
||||
updateQueueMillisAndDraw(true);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Elapsed Time Clock",
|
||||
"shortName": "Elapsed Time",
|
||||
"type": "clock",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "A clock that calculates the time difference between now and any given target date.",
|
||||
"tags": "clock,tool",
|
||||
"supports": ["BANGLEJS2"],
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
// Load settings
|
||||
var settings = Object.assign({
|
||||
// default values
|
||||
displaySeconds: true,
|
||||
displaySeconds: 1,
|
||||
displayMonthsYears: true,
|
||||
dateFormat: 0,
|
||||
time24: true
|
||||
|
|
@ -14,18 +14,26 @@
|
|||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
if (typeof settings.displaySeconds === 'boolean') {
|
||||
settings.displaySeconds = 1;
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
var dateFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"];
|
||||
var displaySecondsFormats = ["Never", "Unlocked", "Always"];
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "Elapsed Time" },
|
||||
"< Back" : () => back(),
|
||||
'Show\nseconds': {
|
||||
value: !!settings.displaySeconds,
|
||||
value: settings.displaySeconds,
|
||||
min: 0, max: 2, wrap: true,
|
||||
onchange: v => {
|
||||
settings.displaySeconds = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
format: function (v) {return displaySecondsFormats[v];}
|
||||
},
|
||||
'Show months/\nyears': {
|
||||
value: !!settings.displayMonthsYears,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwYHEgMkyVAkmQDJYREyQRRoARQpARQpIRRkARNggRBkgRNgARCwARNiQRBSRIREgQRBSRIREgARCSRARFhKSKCIoFCSRAjISQ0BAQJZHCI6ZBTwKPEI44tBTIMSYoZ9IBIYyEWZCHEKwbXIDwZ6MBghjBWBR7DIQbmJAAJ7BexYRHGZZHEchRrGNJYRIRpARJWI7XDCIrVHLIeACIpuIgKwBR4RcQyDLFCJbLGCJcAZZgLEiRcLCIkCZZYvFCKAjDI6BZOPqD+PWaUJa6ARCTxARICBQRFPRIRHPRIRHBg4A="))
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
Bangle.setUI({mode:"custom",remove:()=>{}});"Bangle.loadWidgets"; // Allow fastloading.
|
||||
|
||||
Bluetooth.println(JSON.stringify({t:"intent", action:"nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_DISCONNECT", extra:{EXTRA_DEVICE_ADDRESS:NRF.getAddress()}}));
|
||||
|
||||
Bangle.showClock();
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
{ "id": "gbdiscon",
|
||||
"name": "Disconnect from Gadgetbridge",
|
||||
"shortName":"Disconnect Gadgetbridge",
|
||||
"version":"0.01",
|
||||
"description": "Disconnect from your android device by running this app. The app will forward you to your clock face immediately after triggering the command. (Gadgetbridge nightly required until version 82 is released)",
|
||||
"icon": "app.png",
|
||||
"tags": "android, gadgetbridge, bluetooth, bt",
|
||||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"gbdiscon.app.js","url":"app.js"},
|
||||
{"name":"gbdiscon.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
@ -10,4 +10,5 @@
|
|||
0.10: Simplify touch events
|
||||
Remove date+time
|
||||
0.11: Use default Bangle formatter for booleans
|
||||
0.12: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
||||
0.12: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
||||
0.13: Upgrade to new translation system
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "gbmusic",
|
||||
"name": "Gadgetbridge Music Controls",
|
||||
"shortName": "Music Controls",
|
||||
"version": "0.12",
|
||||
"version": "0.13",
|
||||
"description": "Control the music on your Gadgetbridge-connected phone",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"},
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
*/
|
||||
(function(back) {
|
||||
const SETTINGS_FILE = "gbmusic.json",
|
||||
storage = require("Storage"),
|
||||
translate = require("locale").translate;
|
||||
storage = require("Storage");
|
||||
|
||||
// initialize with default settings...
|
||||
let s = {
|
||||
|
|
@ -28,12 +27,12 @@
|
|||
let menu = {
|
||||
"": {"title": "Music Control"},
|
||||
};
|
||||
menu[translate("< Back")] = back;
|
||||
menu[translate("Auto start")] = {
|
||||
menu["< Back"] = back;
|
||||
menu[/*LANG*/"Auto start"] = {
|
||||
value: !!s.autoStart,
|
||||
onchange: save("autoStart"),
|
||||
};
|
||||
menu[translate("Simple button")] = {
|
||||
menu[/*LANG*/"Simple button"] = {
|
||||
value: !!s.simpleButton,
|
||||
onchange: save("simpleButton"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -135,5 +135,9 @@
|
|||
* Fix for files converted from maps2gpx : path was not reduced in size correctly
|
||||
* Experimental ski mode : have a ski slopes map
|
||||
* Fix for path projection display when lost and zoomed out
|
||||
|
||||
0.25: Minor code improvements
|
||||
|
||||
0.26: Add option to plot openstmap if installed
|
||||
|
||||
0.27: Support for large paths (grid sizes > 65k)
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ function compute_eta(hour, minutes, approximate_speed, remaining_distance) {
|
|||
}
|
||||
|
||||
class TilesOffsets {
|
||||
constructor(filename, offset) {
|
||||
constructor(filename, offset, bytes_per_tile_index) {
|
||||
let header = E.toArrayBuffer(s.read(filename, offset, 4));
|
||||
let type_size = Uint8Array(header, 0, 1)[0];
|
||||
offset += 1;
|
||||
|
|
@ -105,26 +105,30 @@ class TilesOffsets {
|
|||
offset += 2;
|
||||
|
||||
let bytes = (type_size==24)?3:2;
|
||||
let buffer = E.toArrayBuffer(s.read(filename, offset, 2*non_empty_tiles_number+bytes*non_empty_tiles_number));
|
||||
this.non_empty_tiles = Uint16Array(buffer, 0, non_empty_tiles_number);
|
||||
offset += 2 * non_empty_tiles_number;
|
||||
let buffer = E.toArrayBuffer(s.read(filename, offset, bytes_per_tile_index*non_empty_tiles_number));
|
||||
if (bytes_per_tile_index == 2) {
|
||||
this.non_empty_tiles = Uint16Array(buffer, 0, non_empty_tiles_number);
|
||||
} else {
|
||||
this.non_empty_tiles = Uint24Array(buffer, 0, non_empty_tiles_number);
|
||||
}
|
||||
offset += bytes_per_tile_index * non_empty_tiles_number;
|
||||
let tile_buffer = E.toArrayBuffer(s.read(filename, offset, bytes*non_empty_tiles_number));
|
||||
if (type_size == 24) {
|
||||
this.non_empty_tiles_ends = Uint24Array(
|
||||
buffer,
|
||||
2*non_empty_tiles_number,
|
||||
tile_buffer,
|
||||
0,
|
||||
non_empty_tiles_number
|
||||
);
|
||||
offset += 3 * non_empty_tiles_number;
|
||||
} else if (type_size == 16) {
|
||||
this.non_empty_tiles_ends = Uint16Array(
|
||||
buffer,
|
||||
2*non_empty_tiles_number,
|
||||
tile_buffer,
|
||||
0,
|
||||
non_empty_tiles_number
|
||||
);
|
||||
offset += 2 * non_empty_tiles_number;
|
||||
} else {
|
||||
throw "unknown size";
|
||||
}
|
||||
offset += bytes * non_empty_tiles_number;
|
||||
return [this, offset];
|
||||
}
|
||||
tile_start_offset(tile_index) {
|
||||
|
|
@ -179,7 +183,8 @@ class Map {
|
|||
offset += 8;
|
||||
|
||||
// tiles offsets
|
||||
let res = new TilesOffsets(filename, offset);
|
||||
let bytes_per_tile_index = (this.grid_size[0] * this.grid_size[1] > 65536)?3:2;
|
||||
let res = new TilesOffsets(filename, offset, bytes_per_tile_index);
|
||||
this.tiles_offsets = res[0];
|
||||
offset = res[1];
|
||||
|
||||
|
|
@ -314,7 +319,8 @@ class Interests {
|
|||
this.side = side_array[0];
|
||||
offset += 8;
|
||||
|
||||
let res = new TilesOffsets(filename, offset);
|
||||
let bytes_per_tile_index = (this.grid_size[0] * this.grid_size[1] > 65536)?3:2;
|
||||
let res = new TilesOffsets(filename, offset, bytes_per_tile_index);
|
||||
offset = res[1];
|
||||
this.offsets = res[0];
|
||||
let end = this.offsets.end_offset();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"id": "gipy",
|
||||
"name": "Gipy",
|
||||
"shortName": "Gipy",
|
||||
"version": "0.26",
|
||||
"version": "0.27",
|
||||
"description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.",
|
||||
"allow_emulator":false,
|
||||
"icon": "gipy.png",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* tslint:disable */
|
||||
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* @param {Gps} gps
|
||||
*/
|
||||
|
|
@ -80,11 +80,11 @@ export interface InitOutput {
|
|||
readonly __wbindgen_malloc: (a: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly wasm_bindgen__convert__closures__invoke1_mut__hc18aa489d857d6a0: (a: number, b: number, c: number) => void;
|
||||
readonly wasm_bindgen__convert__closures__invoke1_mut__h175ee3b9ff4e5b4c: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
||||
readonly __wbindgen_free: (a: number, b: number) => void;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly wasm_bindgen__convert__closures__invoke2_mut__h41c3b5af183df3b2: (a: number, b: number, c: number, d: number) => void;
|
||||
readonly wasm_bindgen__convert__closures__invoke2_mut__h41622a4cb7018e76: (a: number, b: number, c: number, d: number) => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,14 @@ function takeObject(idx) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
|
||||
cachedTextDecoder.decode();
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||
const idx = heap_next;
|
||||
|
|
@ -107,14 +115,6 @@ function addHeapObject(obj) {
|
|||
return idx;
|
||||
}
|
||||
|
||||
const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
|
||||
cachedTextDecoder.decode();
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
// primitive types
|
||||
const type = typeof val;
|
||||
|
|
@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
|
|||
return real;
|
||||
}
|
||||
function __wbg_adapter_24(arg0, arg1, arg2) {
|
||||
wasm.wasm_bindgen__convert__closures__invoke1_mut__hc18aa489d857d6a0(arg0, arg1, addHeapObject(arg2));
|
||||
wasm.wasm_bindgen__convert__closures__invoke1_mut__h175ee3b9ff4e5b4c(arg0, arg1, addHeapObject(arg2));
|
||||
}
|
||||
|
||||
function _assertClass(instance, klass) {
|
||||
|
|
@ -389,7 +389,7 @@ function handleError(f, args) {
|
|||
}
|
||||
}
|
||||
function __wbg_adapter_86(arg0, arg1, arg2, arg3) {
|
||||
wasm.wasm_bindgen__convert__closures__invoke2_mut__h41c3b5af183df3b2(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||
wasm.wasm_bindgen__convert__closures__invoke2_mut__h41622a4cb7018e76(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -464,10 +464,6 @@ function getImports() {
|
|||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||
const ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return addHeapObject(ret);
|
||||
|
|
@ -476,6 +472,10 @@ function getImports() {
|
|||
const ret = fetch(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||
const ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
|
||||
const ret = getObject(arg0).signal;
|
||||
return addHeapObject(ret);
|
||||
|
|
@ -695,8 +695,8 @@ function getImports() {
|
|||
const ret = wasm.memory;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper2356 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 293, __wbg_adapter_24);
|
||||
imports.wbg.__wbindgen_closure_wrapper2375 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 301, __wbg_adapter_24);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* tslint:disable */
|
||||
|
||||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export function __wbg_gps_free(a: number): void;
|
||||
export function disable_elevation(a: number): void;
|
||||
|
|
@ -14,8 +14,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number, e: num
|
|||
export function __wbindgen_malloc(a: number): number;
|
||||
export function __wbindgen_realloc(a: number, b: number, c: number): number;
|
||||
export const __wbindgen_export_2: WebAssembly.Table;
|
||||
export function wasm_bindgen__convert__closures__invoke1_mut__hc18aa489d857d6a0(a: number, b: number, c: number): void;
|
||||
export function wasm_bindgen__convert__closures__invoke1_mut__h175ee3b9ff4e5b4c(a: number, b: number, c: number): void;
|
||||
export function __wbindgen_add_to_stack_pointer(a: number): number;
|
||||
export function __wbindgen_free(a: number, b: number): void;
|
||||
export function __wbindgen_exn_store(a: number): void;
|
||||
export function wasm_bindgen__convert__closures__invoke2_mut__h41c3b5af183df3b2(a: number, b: number, c: number, d: number): void;
|
||||
export function wasm_bindgen__convert__closures__invoke2_mut__h41622a4cb7018e76(a: number, b: number, c: number, d: number): void;
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@
|
|||
0.17: Fix regression where long month names were 'undefined' (fix #1641)
|
||||
0.18: Fix lint warnings, change anv->janv for fr_BE and fr_CH
|
||||
0.19: Deprecate currency information
|
||||
0.20: Improve support for meridians
|
||||
|
|
|
|||
|
|
@ -201,11 +201,14 @@ function round(n, dp) {
|
|||
var p = Math.max(0,Math.min(dp,dp - Math.floor(Math.log(n)/Math.log(10))));
|
||||
return n.toFixed(p);
|
||||
}
|
||||
var is12;
|
||||
var _is12Hours;
|
||||
function is12Hours() {
|
||||
if (_is12Hours === undefined) _is12Hours = ${isLocal ? "false" : `(require('Storage').readJSON('setting.json', 1) || {})["12hour"]`};
|
||||
return _is12Hours;
|
||||
}
|
||||
function getHours(d) {
|
||||
var h = d.getHours();
|
||||
if (is12 === undefined) is12 = ${isLocal ? "false" : `(require('Storage').readJSON('setting.json', 1) || {})["12hour"]`};
|
||||
if (!is12) return ('0' + h).slice(-2);
|
||||
if (!is12Hours()) return ('0' + h).slice(-2);
|
||||
return ((h % 12 == 0) ? 12 : h % 12).toString();
|
||||
}
|
||||
exports = {
|
||||
|
|
@ -234,7 +237,8 @@ exports = {
|
|||
translate: s => ${locale.trans?`{var t=${js(locale.trans)};s=''+s;return t[s]||t[s.toLowerCase()]||s;}`:`s`},
|
||||
date: (d,short) => short ? \`${dateS}\` : \`${dateN}\`,
|
||||
time: (d,short) => short ? \`${timeS}\` : \`${timeN}\`,
|
||||
meridian: d => d.getHours() < 12 ? ${js(locale.ampm[0])}:${js(locale.ampm[1])},
|
||||
meridian: (d,force) => (force||is12Hours()) ? d.getHours() < 12 ? ${js(locale.ampm[0])}:${js(locale.ampm[1])} : "",
|
||||
is12Hours,
|
||||
};
|
||||
`.trim()
|
||||
};
|
||||
|
|
@ -306,8 +310,8 @@ ${customizeLocale ? `<tr><td class="table_t">Time format</td>
|
|||
`;
|
||||
document.getElementById("examples").innerHTML = `
|
||||
<tr><td class="table_t">Meridian</td><td>
|
||||
<span id="meridian-am-output">${exports.meridian(new Date(0))}</span> /
|
||||
<span id="meridian-pm-output">${exports.meridian(new Date(43200000))}</span>
|
||||
<span id="meridian-am-output">${exports.meridian(new Date(0), true)}</span> /
|
||||
<span id="meridian-pm-output">${exports.meridian(new Date(43200000), true)}</span>
|
||||
</td></tr>
|
||||
${customizeLocale ? `<tr><td class="table_t">Meridian names</td>
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "locale",
|
||||
"name": "Languages",
|
||||
"version": "0.19",
|
||||
"version": "0.20",
|
||||
"description": "Translations for different countries",
|
||||
"icon": "locale.png",
|
||||
"type": "locale",
|
||||
|
|
|
|||
|
|
@ -106,3 +106,4 @@
|
|||
0.77: Messages can now use international fonts if they are installed
|
||||
0.78: Fix: When user taps on a new message, clear the unread timeout
|
||||
0.79: Fix: Reset the unread timeout each time a new message is shown. When the message is read from user input, do not set an unread timeout.
|
||||
0.80: Add ability to reply to messages if a reply library is installed and the message can be replied to
|
||||
|
|
@ -24,6 +24,8 @@ require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bo
|
|||
var Layout = require("Layout");
|
||||
var layout; // global var containing the layout for the currently displayed message
|
||||
var settings = require('Storage').readJSON("messages.settings.json", true) || {};
|
||||
var reply;
|
||||
try { reply = require("reply"); } catch (e) {}
|
||||
var fontSmall = "6x8";
|
||||
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
|
||||
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
|
||||
|
|
@ -45,6 +47,7 @@ if (Graphics.prototype.setFontIntl) {
|
|||
|
||||
var active; // active screen (undefined/"list"/"music"/"map"/"message"/"scroller"/"settings")
|
||||
var openMusic = false; // go back to music screen after we handle something else?
|
||||
var replying = false; // If we're replying to a message, don't interrupt
|
||||
// hack for 2v10 firmware's lack of ':size' font handling
|
||||
try {
|
||||
g.setFont("6x8:2");
|
||||
|
|
@ -267,11 +270,31 @@ function showMessageSettings(msg) {
|
|||
/*LANG*/"View Message" : () => {
|
||||
showMessageScroller(msg);
|
||||
},
|
||||
};
|
||||
|
||||
if (msg.reply && reply) {
|
||||
menu[/*LANG*/"Reply"] = () => {
|
||||
replying = true;
|
||||
reply.reply({msg: msg})
|
||||
.then(result => {
|
||||
Bluetooth.println(JSON.stringify(result));
|
||||
replying = false;
|
||||
showMessage(msg.id);
|
||||
})
|
||||
.catch(() => {
|
||||
replying = false;
|
||||
showMessage(msg.id);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
menu = Object.assign(menu, {
|
||||
/*LANG*/"Delete" : () => {
|
||||
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
||||
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (Bangle.messageIgnore && msg.src)
|
||||
menu[/*LANG*/"Ignore"] = () => {
|
||||
E.showPrompt(/*LANG*/"Ignore all messages from "+E.toJS(msg.src)+"?", {title:/*LANG*/"Ignore"}).then(isYes => {
|
||||
|
|
@ -305,6 +328,7 @@ function showMessageSettings(msg) {
|
|||
}
|
||||
|
||||
function showMessage(msgid, persist) {
|
||||
if (replying) { return; }
|
||||
if(!persist) resetReloadTimeout();
|
||||
let idx = MESSAGES.findIndex(m=>m.id==msgid);
|
||||
var msg = MESSAGES[idx];
|
||||
|
|
@ -374,15 +398,32 @@ function showMessage(msgid, persist) {
|
|||
}; footer.push({type:"img",src:atob("PhAB4A8AAAAAAAPAfAMAAAAAD4PwHAAAAAA/H4DwAAAAAH78B8AAAAAA/+A/AAAAAAH/Af//////w/gP//////8P4D///////H/Af//////z/4D8AAAAAB+/AfAAAAAA/H4DwAAAAAPg/AcAAAAADwHwDAAAAAA4A8AAAAAAAA=="),col:"#f00",cb:negHandler});
|
||||
}
|
||||
footer.push({fillx:1}); // push images to left/right
|
||||
if (msg.positive) {
|
||||
if (msg.reply && reply) {
|
||||
posHandler = ()=>{
|
||||
replying = true;
|
||||
msg.new = false;
|
||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||
reply.reply({msg: msg})
|
||||
.then(result => {
|
||||
Bluetooth.println(JSON.stringify(result));
|
||||
replying = false;
|
||||
layout.render();
|
||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
||||
})
|
||||
.catch(() => {
|
||||
replying = false;
|
||||
layout.render();
|
||||
showMessage(msg.id);
|
||||
});
|
||||
}; footer.push({type:"img",src:atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="),col:"#0f0", cb:posHandler});
|
||||
}
|
||||
else if (msg.positive) {
|
||||
posHandler = ()=>{
|
||||
msg.new = false;
|
||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||
Bangle.messageResponse(msg,true);
|
||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
|
||||
};
|
||||
footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler});
|
||||
|
||||
}; footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler});
|
||||
}
|
||||
|
||||
layout = new Layout({ type:"v", c: [
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
"id": "messagegui",
|
||||
"name": "Message UI",
|
||||
"shortName": "Messages",
|
||||
"version": "0.79",
|
||||
"version": "0.80",
|
||||
"description": "Default app to display notifications from iOS and Gadgetbridge/Android",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,system",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"dependencies" : { "messageicons":"module" },
|
||||
"dependencies" : { "messageicons":"module", "reply": "module" },
|
||||
"provides_modules": ["messagegui"],
|
||||
"default": true,
|
||||
"readme": "README.md",
|
||||
|
|
|
|||
|
|
@ -6,3 +6,4 @@
|
|||
0.05: Add message icon for 'jira'
|
||||
0.06: Add message icon for 'molly' and 'threema libre'
|
||||
0.07: Minor code improvements
|
||||
0.08: Add more icons including GMail, Google Messages, Google Agenda
|
||||
|
After Width: | Height: | Size: 189 B |
|
|
@ -91,6 +91,7 @@ exports.getColor = function(msg,options) {
|
|||
const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
|
||||
return {
|
||||
/* generic colors, using B2-safe colors */ ${ /* DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used */"" }
|
||||
"agenda": "#206cd5",
|
||||
"airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/
|
||||
"mail": "#ff0",
|
||||
"music": "#f0f",
|
||||
|
|
@ -111,8 +112,10 @@ exports.getColor = function(msg,options) {
|
|||
// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background
|
||||
"instagram": "#ff0069", // https://about.instagram.com/brand/gradient
|
||||
"jira": "#0052cc", //https://atlassian.design/resources/logo-library
|
||||
"leboncoin": "#fa7321",
|
||||
"lieferando": "#ff8000",
|
||||
"linkedin": "#0a66c2", // https://brand.linkedin.com/
|
||||
"messages": "#0a5cce",
|
||||
"messenger": "#0078ff",
|
||||
"mastodon": "#563acc", // https://www.joinmastodon.org/branding
|
||||
"mattermost": "#00f",
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 195 B |
|
|
@ -1,6 +1,7 @@
|
|||
[
|
||||
{ "app":"default", "icon":"default.png" },
|
||||
{ "app":"airbnb", "icon":"airbnb.png" },
|
||||
{ "app":"agenda", "icon":"agenda.png" },
|
||||
{ "app":"alarm", "icon":"alarm.png" },
|
||||
{ "app":"alarmclockreceiver", "icon":"alarm.png" },
|
||||
{ "app":"amazon shopping", "icon":"amazon.png" },
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
{ "app":"aurora droid", "icon":"security.png" },
|
||||
{ "app":"github", "icon":"github.png" },
|
||||
{ "app":"gitlab", "icon":"gitlab.png" },
|
||||
{ "app":"gmail", "icon":"gmail.png" },
|
||||
{ "app":"gmx", "icon":"gmx.png" },
|
||||
{ "app":"google", "icon":"google.png" },
|
||||
{ "app":"google home", "icon":"google home.png" },
|
||||
|
|
@ -45,6 +47,7 @@
|
|||
{ "app":"jira", "icon":"jira.png" },
|
||||
{ "app":"kalender", "icon":"kalender.png" },
|
||||
{ "app":"keep notes", "icon":"google keep.png" },
|
||||
{ "app":"leboncoin", "icon":"leboncoin.png" },
|
||||
{ "app":"lieferando", "icon":"lieferando.png" },
|
||||
{ "app":"linkedin", "icon":"linkedin.png" },
|
||||
{ "app":"maps", "icon":"map.png" },
|
||||
|
|
@ -55,6 +58,7 @@
|
|||
{ "app":"tooot", "icon":"mastodon.png" },
|
||||
{ "app":"tusky", "icon":"mastodon.png" },
|
||||
{ "app":"mattermost", "icon":"mattermost.png" },
|
||||
{ "app":"messages", "icon":"messages.png" },
|
||||
{ "app":"n26", "icon":"n26.png" },
|
||||
{ "app":"netflix", "icon":"netflix.png" },
|
||||
{ "app":"news", "icon":"news.png" },
|
||||
|
|
@ -110,6 +114,5 @@
|
|||
{ "app":"meet", "icon":"videoconf.png" },
|
||||
{ "app":"music", "icon":"music.png" },
|
||||
{ "app":"sms message", "icon":"default.png" },
|
||||
{ "app":"mail", "icon":"default.png" },
|
||||
{ "app":"gmail", "icon":"default.png" }
|
||||
{ "app":"mail", "icon":"default.png" }
|
||||
]
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 201 B |
|
|
@ -4,7 +4,7 @@ exports.getImage = function(msg) {
|
|||
if (msg.img) return atob(msg.img);
|
||||
let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
|
||||
if (msg.id=="music") s="music";
|
||||
let match = ",default|0,airbnb|1,alarm|2,alarmclockreceiver|2,amazon shopping|3,bibel|4,bitwarden|5,1password|5,lastpass|5,dashlane|5,bring|6,calendar|7,etar|7,chat|8,chrome|9,clock|2,corona-warn|10,bmo|11,desjardins|11,rbc mobile|11,nbc|11,rabobank|11,scotiabank|11,td (canada)|11,discord|12,drive|13,element|14,facebook|15,messenger|16,firefox|17,firefox beta|17,firefox nightly|17,f-droid|5,neo store|5,aurora droid|5,github|18,gitlab|19,gmx|20,google|21,google home|22,google play store|23,home assistant|24,instagram|25,jira|26,kalender|27,keep notes|28,lieferando|29,linkedin|30,maps|31,organic maps|31,osmand|31,mastodon|32,fedilab|32,tooot|32,tusky|32,mattermost|33,n26|34,netflix|35,news|36,cbc news|36,rc info|36,reuters|36,ap news|36,la presse|36,nbc news|36,nextbike|37,nina|38,outlook mail|39,paypal|40,phone|41,plex|42,pocket|43,post & dhl|44,proton mail|45,reddit|46,sync pro|46,sync dev|46,boost|46,infinity|46,slide|46,signal|47,molly|47,skype|48,slack|49,snapchat|50,starbucks|51,steam|52,teams|53,telegram|54,telegram foss|54,threema|55,threema libre|55,tiktok|56,to do|57,opentasks|57,tasks|57,transit|58,twitch|59,twitter|60,uber|61,lyft|61,vlc|62,warnapp|63,whatsapp|64,wordfeud|65,youtube|66,newpipe|66,zoom|67,meet|67,music|68,sms message|0,mail|0,gmail|0,".match(new RegExp(`,${s}\\|(\\d+)`))
|
||||
let match = ",default|0,airbnb|1,agenda|2,alarm|3,alarmclockreceiver|3,amazon shopping|4,bibel|5,bitwarden|6,1password|6,lastpass|6,dashlane|6,bring|7,calendar|8,etar|8,chat|9,chrome|10,clock|3,corona-warn|11,bmo|12,desjardins|12,rbc mobile|12,nbc|12,rabobank|12,scotiabank|12,td (canada)|12,discord|13,drive|14,element|15,facebook|16,messenger|17,firefox|18,firefox beta|18,firefox nightly|18,f-droid|6,neo store|6,aurora droid|6,github|19,gitlab|20,gmail|21,gmx|22,google|23,google home|24,google play store|25,home assistant|26,instagram|27,jira|28,kalender|29,keep notes|30,leboncoin|31,lieferando|32,linkedin|33,maps|34,organic maps|34,osmand|34,mastodon|35,fedilab|35,tooot|35,tusky|35,mattermost|36,messages|37,n26|38,netflix|39,news|40,cbc news|40,rc info|40,reuters|40,ap news|40,la presse|40,nbc news|40,nextbike|41,nina|42,outlook mail|43,paypal|44,phone|45,plex|46,pocket|47,post & dhl|48,proton mail|49,reddit|50,sync pro|50,sync dev|50,boost|50,infinity|50,slide|50,signal|51,molly|51,skype|52,slack|53,snapchat|54,starbucks|55,steam|56,teams|57,telegram|58,telegram foss|58,threema|59,threema libre|59,tiktok|60,to do|61,opentasks|61,tasks|61,transit|62,twitch|63,twitter|64,uber|65,lyft|65,vlc|66,warnapp|67,whatsapp|68,wordfeud|69,youtube|70,newpipe|70,zoom|71,meet|71,music|72,sms message|0,mail|0,".match(new RegExp(`,${s}\\|(\\d+)`))
|
||||
return require("Storage").read("messageicons.img", (match===null)?0:match[1]*76, 76);
|
||||
};
|
||||
|
||||
|
|
@ -16,6 +16,7 @@ exports.getColor = function(msg,options) {
|
|||
const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase();
|
||||
return {
|
||||
/* generic colors, using B2-safe colors */
|
||||
"agenda": "#206cd5",
|
||||
"airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/
|
||||
"mail": "#ff0",
|
||||
"music": "#f0f",
|
||||
|
|
@ -33,8 +34,10 @@ exports.getColor = function(msg,options) {
|
|||
// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background
|
||||
"instagram": "#ff0069", // https://about.instagram.com/brand/gradient
|
||||
"jira": "#0052cc", //https://atlassian.design/resources/logo-library
|
||||
"leboncoin": "#fa7321",
|
||||
"lieferando": "#ff8000",
|
||||
"linkedin": "#0a66c2", // https://brand.linkedin.com/
|
||||
"messages": "#0a5cce",
|
||||
"messenger": "#0078ff",
|
||||
"mastodon": "#563acc", // https://www.joinmastodon.org/branding
|
||||
"mattermost": "#00f",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "messageicons",
|
||||
"name": "Message Icons",
|
||||
"version": "0.07",
|
||||
"version": "0.08",
|
||||
"description": "Library containing a list of icons and colors for apps",
|
||||
"icon": "app.png",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Corrected NaN test for GPS
|
||||
0.03: Removed remaining invalid references to Number.isFinite
|
||||
0.04: Improved menu/display interaction
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# OpenLocate Beacon
|
||||
|
||||
Collect geolocation sensor data from the Bangle.js 2's GPS and barometer, display the live readings on-screen, and broadcast in Bluetooth Low Energy (BLE) OpenLocate Beacon packets (LCI over BLE) to any listening devices in range.
|
||||
|
||||
## Usage
|
||||
|
||||
The advertising packets will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the standard packet types. See our [Bangle.js Development Guide](https://reelyactive.github.io/diy/banglejs-dev/) for details.
|
||||
|
||||
## Features
|
||||
|
||||
Advertises packets with the OpenLocate Beacon geolocation element when a GPS fix is available, and packets with the name "Bangle.js" otherwise.
|
||||
|
||||
## Requests
|
||||
|
||||
[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates.
|
||||
|
||||
## Creator
|
||||
|
||||
Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"id": "openlocatebeacon",
|
||||
"name": "OpenLocate Beacon",
|
||||
"shortName": "OpenLocate Beacon",
|
||||
"version": "0.04",
|
||||
"description": "Advertise GPS geolocation data using the OpenLocate Beacon packet specification.",
|
||||
"icon": "openlocatebeacon.png",
|
||||
"screenshots": [],
|
||||
"type": "app",
|
||||
"tags": "tool,sensors,bluetooth",
|
||||
"supports" : [ "BANGLEJS2" ],
|
||||
"allow_emulator": true,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{ "name": "openlocatebeacon.app.js", "url": "openlocatebeacon.js" },
|
||||
{ "name": "openlocatebeacon.img", "url": "openlocatebeacon-icon.js",
|
||||
"evaluate": true }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwhHXAB+sr1dnMzmcPh4DBnNdr2sDyAsOropCABczroyaxE5FhoAFnOIFqutFqgxE1ouSrwdHnWIxGz2YPBAYIHBnQTHrwuQLgwsBURwyGnIWN2bmFnR4S1oxFmZyCFx0zLY2r0md4AACzuk1ZjGDoowKCAk6JwuBFggAFzuBOApiEmYuIBwjSE2ddvOcFxIABzl5rpWErxQJN4QuIPIN5FpYADvKlFGAivGHZAuSGBCDEFwg6EOoZnBFyQwCK4mzQg4IECIYuBqzqKehVWDwxWFLwc5M4czLxPB5HP5/I4JgJmYfDnJgFEwJeFG4MyLw4tB6wPB6wxBMA8yRAhgDHAOtY44FBq2cFw3Qa4nX6AwGziQBEIwAB1o1DHoyOG4PPFwoAB56SGSAKBGA4SVDBgc6AwKOG45eGMAXHSAwbBnSQGnIvD1psFF43IXgQAF6yQGF4SQDXQczdwezF5pfJF5uzF4bED1gACF5KPSKoMzEY0PgAAJuQdFMAIvH5wQGuQkKABVOzhgG57BE63PLw2cpwvVkgvGGAPI5/Q6HP5AuGF4MkF6pgIAAPB4/H4ILHLxsrMBbBHABlyLxcrF5ZgKABJeOqyRMMCVyEBlWwLyNywuPyzsNwPXeS6NTAAPX67AMSKCNNXwIvBSBqRBGBlyRpqOCAAIRNSJiNPRwQABqwwPF5IuPqwvD1gUOSJKNPgGsF4bBPgEqSI2clQYOXoYADlbCUXiErFwyRQgCREuQVPRoqRTkmWFwOWXh6NHGCaRBRqAuLGCIAQFxowgFx70ClYtZlbqJMUZcRAA1WFqdWFq5jESp0rLbAyJq0rGggFBqwsSA=="))
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* Copyright reelyActive 2024
|
||||
* We believe in an open Internet of Things
|
||||
*/
|
||||
|
||||
|
||||
// Non-user-configurable constants
|
||||
const APP_ID = 'openlocatebeacon';
|
||||
const ADVERTISING_OPTIONS = { showName: false, interval: 5000 };
|
||||
|
||||
|
||||
// Global variables
|
||||
let bar, gps;
|
||||
let sequenceNumber = 0;
|
||||
|
||||
|
||||
// Menus
|
||||
let mainMenu = {
|
||||
"": { "title": "OpenLocateBcn" },
|
||||
"Lat": { value: null },
|
||||
"Lon": { value: null },
|
||||
"Altitude": { value: null },
|
||||
"Satellites": { value: null }
|
||||
};
|
||||
|
||||
|
||||
// Encode the OpenLocate geo location element advertising packet
|
||||
function encodeGeoLocationElement() {
|
||||
let lci = new Uint8Array(16);
|
||||
let seqFrag = ((sequenceNumber++ & 0x0f) << 4) + 0x01;
|
||||
let rfc6225lat = toRfc6225Coordinate(gps.lat);
|
||||
let rfc6225lon = toRfc6225Coordinate(gps.lon);
|
||||
let rfc6225alt = toRfc6225Altitude(bar.altitude);
|
||||
lci[0] = rfc6225lat.integer >> 7;
|
||||
lci[1] = ((rfc6225lat.integer & 0xff) << 1) + (rfc6225lat.fraction >> 24);
|
||||
lci[2] = (rfc6225lat.fraction >> 16) & 0xff;
|
||||
lci[3] = (rfc6225lat.fraction >> 8) & 0xff;
|
||||
lci[4] = rfc6225lat.fraction & 0xff;
|
||||
lci[5] = rfc6225lon.integer >> 7;
|
||||
lci[6] = ((rfc6225lon.integer & 0xff) << 1) + (rfc6225lon.fraction >> 24);
|
||||
lci[7] = (rfc6225lon.fraction >> 16) & 0xff;
|
||||
lci[8] = (rfc6225lon.fraction >> 8) & 0xff;
|
||||
lci[9] = rfc6225lon.fraction & 0xff;
|
||||
lci[10] = bar.altitude ? 0x10 : 0x00;
|
||||
lci[11] = (rfc6225alt.integer >> 16) & 0xff;
|
||||
lci[12] = (rfc6225alt.integer >> 8) & 0xff;
|
||||
lci[13] = rfc6225alt.integer & 0xff;
|
||||
lci[14] = rfc6225alt.fraction & 0xff;
|
||||
lci[15] = 0x41;
|
||||
|
||||
return [
|
||||
0x02, 0x01, 0x06, // Flags
|
||||
0x16, 0x16, 0x94, 0xfd, 0x09, seqFrag, 0x30, lci[0], lci[1], lci[2],
|
||||
lci[3], lci[4], lci[5], lci[6], lci[7], lci[8], lci[9], lci[10], lci[11],
|
||||
lci[12], lci[13], lci[14], lci[15]
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Convert a latitude or longitude coordinate to RFC6225
|
||||
function toRfc6225Coordinate(coordinate) {
|
||||
let integer = Math.floor(coordinate);
|
||||
let fraction = Math.round((coordinate - integer) * 0x1ffffff);
|
||||
|
||||
if(integer < 0) {
|
||||
integer += 0x1ff + 1;
|
||||
}
|
||||
|
||||
return { integer: integer, fraction: fraction };
|
||||
}
|
||||
|
||||
|
||||
// Convert altitude to RFC6225
|
||||
function toRfc6225Altitude(altitude) {
|
||||
if(!altitude) {
|
||||
return { integer: 0, fraction: 0 };
|
||||
}
|
||||
|
||||
let integer = Math.floor(altitude);
|
||||
let fraction = Math.round((altitude - integer) * 0xff);
|
||||
|
||||
if(integer < 0) {
|
||||
integer += 0x3fffff + 1;
|
||||
}
|
||||
|
||||
return { integer: integer, fraction: fraction };
|
||||
}
|
||||
|
||||
|
||||
// Update barometer
|
||||
Bangle.on('pressure', (newBar) => {
|
||||
bar = newBar;
|
||||
|
||||
mainMenu.Altitude.value = bar.altitude.toFixed(1) + 'm';
|
||||
E.showMenu(mainMenu);
|
||||
});
|
||||
|
||||
|
||||
// Update GPS
|
||||
Bangle.on('GPS', (newGps) => {
|
||||
gps = newGps;
|
||||
|
||||
mainMenu.Lat.value = gps.lat.toFixed(4);
|
||||
mainMenu.Lon.value = gps.lon.toFixed(4);
|
||||
mainMenu.Satellites.value = gps.satellites;
|
||||
E.showMenu(mainMenu);
|
||||
|
||||
if(!isNaN(gps.lat) && !isNaN(gps.lon)) {
|
||||
NRF.setAdvertising(encodeGeoLocationElement(), ADVERTISING_OPTIONS);
|
||||
}
|
||||
else {
|
||||
NRF.setAdvertising({}, { name: "Bangle.js" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// On start: enable sensors and display main menu
|
||||
g.clear();
|
||||
Bangle.setGPSPower(true, APP_ID);
|
||||
Bangle.setBarometerPower(true, APP_ID);
|
||||
E.showMenu(mainMenu);
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
# Pomodoro
|
||||
|
||||
> The Pomodoro Technique is a time management method developed by Francesco Cirillo in the late 1980s. It uses a kitchen timer to break work into intervals, typically 25 minutes in length, separated by short breaks. Each interval is known as a pomodoro, from the Italian word for tomato, after the tomato-shaped kitchen timer Cirillo used as a university student.
|
||||
>
|
||||
> The original technique has six steps:
|
||||
>
|
||||
> Decide on the task to be done.
|
||||
> Set the Pomodoro timer (typically for 25 minutes).
|
||||
> Work on the task.
|
||||
> End work when the timer rings and take a short break (typically 5–10 minutes).
|
||||
> Go back to Step 2 and repeat until you complete four pomodori.
|
||||
> After four pomodori are done, take a long break (typically 20 to 30 minutes) instead of a short break. Once the long break is finished, return to step 2.
|
||||
|
||||
*Description gathered from https://en.wikipedia.org/wiki/Pomodoro_Technique*
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
"type": "app",
|
||||
"tags": "pomodoro,cooking,tools",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"screenshots": [{"url":"bangle2-pomodoro-screenshot.png"}],
|
||||
"storage": [
|
||||
|
|
|
|||
|
|
@ -2,3 +2,6 @@
|
|||
0.02-0.04: Bug fixes
|
||||
0.05: Submitted to the app loader
|
||||
0.06: Added setting to show clock after start/resume
|
||||
0.07: Make fonts and buttons larger for legibility and ease of use. Hide
|
||||
buttons when screen is locked. Toggle the graphical presentation when
|
||||
pressing the (middle) hardware button.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# Pomodoro Plus
|
||||
|
||||
> The Pomodoro Technique is a time management method developed by Francesco Cirillo in the late 1980s. It uses a kitchen timer to break work into intervals, typically 25 minutes in length, separated by short breaks. Each interval is known as a pomodoro, from the Italian word for tomato, after the tomato-shaped kitchen timer Cirillo used as a university student.
|
||||
>
|
||||
> The original technique has six steps:
|
||||
>
|
||||
> Decide on the task to be done.
|
||||
> Set the Pomodoro timer (typically for 25 minutes).
|
||||
> Work on the task.
|
||||
> End work when the timer rings and take a short break (typically 5–10 minutes).
|
||||
> Go back to Step 2 and repeat until you complete four pomodori.
|
||||
> After four pomodori are done, take a long break (typically 20 to 30 minutes) instead of a short break. Once the long break is finished, return to step 2.
|
||||
|
||||
*Description gathered from https://en.wikipedia.org/wiki/Pomodoro_Technique*
|
||||
|
||||
## Usage
|
||||
|
||||
- Click the play button to start a pomodoro and get to work!
|
||||
- Click the pause button if you're interrupted with something urgent.
|
||||
- Click the cross button if you need to end your work session.
|
||||
- Click the skip button if you forgot to start the pomodoro after the urgent interruption and ended up working for a long time! (Good on ya'!)
|
||||
- Press the (middle) hardware button to toggle visibility of widgets and software buttons.
|
||||
|
||||
Configure the pomodori and break times in the settings.
|
||||
|
||||
## Features
|
||||
|
||||
Continues to run in the background if you exit the app while the pomodoro timer is running.
|
||||
|
||||
The buttons and widgets hide automatically when the screen is locked.
|
||||
|
||||
## Requests
|
||||
|
||||
Open an issue on the espruino/BangleApps issue tracker.
|
||||
|
||||
## Creator
|
||||
|
||||
bruceblore
|
||||
|
||||
## Contributors
|
||||
|
||||
notEvil, thyttan
|
||||
|
|
@ -4,6 +4,7 @@ Bangle.POMOPLUS_ACTIVE = true; //Prevent the boot code from running. To avoid h
|
|||
|
||||
const storage = require("Storage");
|
||||
const common = require("pomoplus-com.js");
|
||||
const wu = require("widget_utils");
|
||||
|
||||
//Expire the state if necessary
|
||||
if (
|
||||
|
|
@ -14,37 +15,45 @@ if (
|
|||
common.state = common.STATE_DEFAULT;
|
||||
}
|
||||
|
||||
const W = g.getWidth();
|
||||
const H = g.getHeight();
|
||||
const SCALING = W/176; // The UI was tweaked to look good on a Bangle.js 2 (176x176 px). SCALING automatically adapts so elements have the same proportions relative to the screen size on devices with other resolutions.
|
||||
const BUTTON_HEIGHT = 56 * SCALING;
|
||||
const BUTTON_TOP = H - BUTTON_HEIGHT;
|
||||
|
||||
function drawButtons() {
|
||||
let w = g.getWidth();
|
||||
let h = g.getHeight();
|
||||
//Draw the backdrop
|
||||
const BAR_TOP = h - 24;
|
||||
g.setColor(0, 0, 1).setFontAlign(0, -1)
|
||||
.clearRect(0, BAR_TOP, w, h)
|
||||
.fillRect(0, BAR_TOP, w, h)
|
||||
const ICONS_SIZE = 24;
|
||||
const ICONS_ANCHOR_Y = BUTTON_TOP + BUTTON_HEIGHT / 2 - ICONS_SIZE / 2;
|
||||
g.setColor(0, 0, 1)
|
||||
.fillRect({x:0, y:BUTTON_TOP, x2:W-1, y2:H-1,r:15*SCALING})
|
||||
.setColor(1, 1, 1);
|
||||
|
||||
if (!common.state.wasRunning) { //If the timer was never started, only show a play button
|
||||
g.drawImage(common.BUTTON_ICONS.play, w / 2, BAR_TOP);
|
||||
g.drawImage(common.BUTTON_ICONS.play, W / 2 - ICONS_SIZE / 2, ICONS_ANCHOR_Y);
|
||||
} else {
|
||||
g.drawLine(w / 2, BAR_TOP, w / 2, h);
|
||||
g.setColor(g.theme.bg)
|
||||
.fillRect(W / 2 - 2, BUTTON_TOP, W / 2 + 2, H)
|
||||
.setColor(1,1,1);
|
||||
if (common.state.running) {
|
||||
g.drawImage(common.BUTTON_ICONS.pause, w / 4, BAR_TOP)
|
||||
.drawImage(common.BUTTON_ICONS.skip, w * 3 / 4, BAR_TOP);
|
||||
g.drawImage(common.BUTTON_ICONS.pause, W / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y)
|
||||
.drawImage(common.BUTTON_ICONS.skip, W * 3 / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y);
|
||||
} else {
|
||||
g.drawImage(common.BUTTON_ICONS.reset, w / 4, BAR_TOP)
|
||||
.drawImage(common.BUTTON_ICONS.play, w * 3 / 4, BAR_TOP);
|
||||
g.drawImage(common.BUTTON_ICONS.reset, W / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y)
|
||||
.drawImage(common.BUTTON_ICONS.play, W * 3 / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawTimerAndMessage() {
|
||||
let w = g.getWidth();
|
||||
let h = g.getHeight();
|
||||
const ANCHOR_X = W / 2;
|
||||
const ANCHOR_Y = H * 3 / 8;
|
||||
const TIME_SIZE = 48 * SCALING;
|
||||
const LABEL_SIZE = 18 * SCALING;
|
||||
g.reset()
|
||||
.setFontAlign(0, 0)
|
||||
.setFont("Vector", 36)
|
||||
.clearRect(w / 2 - 60, h / 2 - 34, w / 2 + 60, h / 2 + 34)
|
||||
.setFont("Vector", TIME_SIZE)
|
||||
.clearRect(0, ANCHOR_Y - TIME_SIZE / 2, W-1, ANCHOR_Y + TIME_SIZE / 2 + 1.2 * LABEL_SIZE)
|
||||
|
||||
//Draw the timer
|
||||
.drawString((() => {
|
||||
|
|
@ -59,17 +68,17 @@ function drawTimerAndMessage() {
|
|||
|
||||
if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||
else return `${parseInt(minutes)}:${pad(seconds)}`;
|
||||
})(), w / 2, h / 2)
|
||||
})(), ANCHOR_X, ANCHOR_Y)
|
||||
|
||||
//Draw the phase label
|
||||
.setFont("Vector", 12)
|
||||
.setFont("Vector", LABEL_SIZE)
|
||||
.drawString(((currentPhase, numShortBreaks) => {
|
||||
if (!common.state.wasRunning) return "Not started";
|
||||
else if (currentPhase == common.PHASE_WORKING) return `Work ${numShortBreaks + 1}/${common.settings.numShortBreaks + 1}`
|
||||
else if (currentPhase == common.PHASE_SHORT_BREAK) return `Short break ${numShortBreaks + 1}/${common.settings.numShortBreaks}`;
|
||||
else return "Long break!";
|
||||
})(common.state.phase, common.state.numShortBreaks),
|
||||
w / 2, h / 2 + 18);
|
||||
ANCHOR_X, ANCHOR_Y + 2*LABEL_SIZE);
|
||||
|
||||
//Update phase with vibation if needed
|
||||
if (common.getTimeLeft() <= 0) {
|
||||
|
|
@ -77,11 +86,35 @@ function drawTimerAndMessage() {
|
|||
}
|
||||
}
|
||||
|
||||
drawButtons();
|
||||
Bangle.on("touch", (button, xy) => {
|
||||
if (!Bangle.isLocked()) drawButtons();
|
||||
|
||||
let hideButtons = ()=>{
|
||||
g.clearRect(0,BUTTON_TOP,W-1,H-1);
|
||||
}
|
||||
|
||||
let graphicState = 0; // 0 - all is visible, 1 - widgets are hidden, 2 - widgets and buttons are hidden.
|
||||
let onButtonSwitchGraphics = (n)=>{
|
||||
if (process.env.HWVERSION == 2) n=2; // Translate Bangle.js 2 button to Bangle.js 1 middle button.
|
||||
if (n == 2) {
|
||||
if (graphicState == 0) {
|
||||
wu.hide();
|
||||
}
|
||||
if (graphicState == 1) {
|
||||
hideButtons();
|
||||
}
|
||||
if (graphicState == 2) {
|
||||
wu.show();
|
||||
drawButtons();
|
||||
}
|
||||
graphicState = (graphicState+1) % 3;
|
||||
}
|
||||
}
|
||||
|
||||
let onTouchSoftwareButtons = (button, xy) => {
|
||||
//If we support full touch and we're not touching the keys, ignore.
|
||||
//If we don't support full touch, we can't tell so just assume we are.
|
||||
if (xy !== undefined && xy.y <= g.getHeight() - 24) return;
|
||||
let isOutsideButtonArea = xy !== undefined && xy.y <= g.getHeight() - BUTTON_HEIGHT;
|
||||
if (isOutsideButtonArea || graphicState == 2) return;
|
||||
|
||||
if (!common.state.wasRunning) {
|
||||
//If we were never running, there is only one button: the start button
|
||||
|
|
@ -137,7 +170,13 @@ Bangle.on("touch", (button, xy) => {
|
|||
if (common.settings.showClock) Bangle.showClock();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
touch: onTouchSoftwareButtons,
|
||||
btn: onButtonSwitchGraphics
|
||||
})
|
||||
|
||||
let timerInterval;
|
||||
|
||||
|
|
@ -156,6 +195,18 @@ if (common.state.running) {
|
|||
setupTimerInterval();
|
||||
}
|
||||
|
||||
Bangle.on('lock', (on, reason) => {
|
||||
if (graphicState==2) return;
|
||||
if (on) {
|
||||
hideButtons();
|
||||
wu.hide();
|
||||
}
|
||||
if (!on) {
|
||||
drawButtons();
|
||||
if (graphicState==0) wu.show();
|
||||
}
|
||||
});
|
||||
|
||||
//Save our state when the app is closed
|
||||
E.on('kill', () => {
|
||||
storage.writeJSON(common.STATE_PATH, common.state);
|
||||
|
|
@ -163,3 +214,4 @@ E.on('kill', () => {
|
|||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
if (Bangle.isLocked()) wu.hide();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "pomoplus",
|
||||
"name": "Pomodoro Plus",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "A configurable pomodoro timer that runs in the background.",
|
||||
"icon": "icon.png",
|
||||
"type": "app",
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"BANGLEJS",
|
||||
"BANGLEJS2"
|
||||
],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{
|
||||
"name": "pomoplus.app.js",
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
0.01: New Library!
|
||||
0.01: New Library!
|
||||
0.02: Minor bug fixes
|
||||
|
|
@ -6,7 +6,12 @@ exports.reply = function (options) {
|
|||
keyboard = null;
|
||||
}
|
||||
|
||||
function constructReply(msg, replyText, resolve) {
|
||||
function constructReply(msg, replyText, resolve, reject) {
|
||||
if (!replyText) {
|
||||
reject("");
|
||||
return;
|
||||
}
|
||||
|
||||
var responseMessage = {msg: replyText};
|
||||
if (msg.id) {
|
||||
responseMessage = { t: "notify", id: msg.id, n: "REPLY", msg: replyText };
|
||||
|
|
@ -29,7 +34,10 @@ exports.reply = function (options) {
|
|||
}, // options
|
||||
/*LANG*/ "Compose": function () {
|
||||
keyboard.input().then((result) => {
|
||||
constructReply(options.msg ?? {}, result, resolve);
|
||||
if (result)
|
||||
constructReply(options.msg ?? {}, result, resolve, reject);
|
||||
else
|
||||
E.showMenu(menu);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -40,7 +48,7 @@ exports.reply = function (options) {
|
|||
) || [];
|
||||
replies.forEach((reply) => {
|
||||
menu = Object.defineProperty(menu, reply.text, {
|
||||
value: () => constructReply(options.msg ?? {}, reply.text, resolve),
|
||||
value: () => constructReply(options.msg ?? {}, reply.text, resolve, reject),
|
||||
});
|
||||
});
|
||||
if (!keyboard) delete menu[/*LANG*/ "Compose"];
|
||||
|
|
@ -60,10 +68,11 @@ exports.reply = function (options) {
|
|||
);
|
||||
} else {
|
||||
keyboard.input().then((result) => {
|
||||
constructReply(options.msg.id, result, resolve);
|
||||
constructReply(options.msg, result, resolve, reject);
|
||||
});
|
||||
}
|
||||
} else{
|
||||
E.showMenu(menu);
|
||||
}
|
||||
E.showMenu(menu);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "reply",
|
||||
"name": "Reply Library",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "A library for replying to text messages via predefined responses or keyboard",
|
||||
"icon": "app.png",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
0.01: New App!
|
||||
0.02: Fix case where we tried to push to Bangle.btnWatches but it wasn't
|
||||
defined.
|
||||
0.03: Throw exception if trying to add custom drag handler on mode updown and
|
||||
leftright.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ Bangle.setUI = (function(mode, cb) {
|
|||
try{Bangle.buzz(30);}catch(e){}
|
||||
}
|
||||
if (mode=="updown") {
|
||||
if (options.drag) throw new Error("Custom drag handler not supported in mode updown!")
|
||||
var dy = 0;
|
||||
Bangle.dragHandler = e=>{
|
||||
dy += e.dy;
|
||||
|
|
@ -55,6 +56,7 @@ Bangle.setUI = (function(mode, cb) {
|
|||
setWatch(function() { b();cb(); }, BTN1, {repeat:1, edge:"rising"}),
|
||||
];
|
||||
} else if (mode=="leftright") {
|
||||
if (options.drag) throw new Error("Custom drag handler not supported in mode leftright!")
|
||||
var dx = 0;
|
||||
Bangle.dragHandler = e=>{
|
||||
dx += e.dx;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "setuichange",
|
||||
"name": "SetUI Proposals preview",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "Try out potential future changes to `Bangle.setUI`. Makes hardware button interaction snappier. Makes it possible to set custom event handlers on any type/mode, not just `\"custom\"`. Please provide feedback - see `Read more...` below.",
|
||||
"icon": "app.png",
|
||||
"tags": "",
|
||||
|
|
|
|||
|
|
@ -3,3 +3,11 @@
|
|||
0.03: minor code improvements
|
||||
0.04: make height auto-calibration useful and slow ticks to save power
|
||||
0.05: add ability to navigate to waypoints, better documentation
|
||||
0.10: lots of updates
|
||||
acknowledge commands by vibration and message
|
||||
ui tweaks -- bigger font, compressing uninteresting info
|
||||
display meters up/down
|
||||
display pressure trend
|
||||
adjust GPS on/off algorithm for more reliable fix
|
||||
display warnings when GPS altitude does not match baro
|
||||
|
||||
|
|
|
|||
|
|
@ -26,16 +26,17 @@ Useful gestures:
|
|||
|
||||
B -- "Battery", show/buzz battery info
|
||||
D -- "Down", previous waypoint
|
||||
F -- "oFf", disable GPS.
|
||||
G -- "Gps", enable GPS for 4 hours in low power mode.
|
||||
F -- "turn oFf gps", disable GPS.
|
||||
T -- "Turn on gps", enable GPS for 4 hours in low power mode.
|
||||
I -- "Info", toggle info display
|
||||
L -- "aLtimeter", load altimeter app
|
||||
M -- "Mark", create mark from current position
|
||||
N -- "Note", take a note and write it to the log.
|
||||
O -- "Orloj", run orloj app
|
||||
R -- "Run", run "runplus" app
|
||||
P -- "runPlus", run "runplus" app
|
||||
R -- "Reset" daily statistics
|
||||
S -- "Speed", enable GPS for 30 minutes in high power mode.
|
||||
T -- "Time", buzz current time
|
||||
G -- "Get time", buzz current time
|
||||
U -- "Up", next waypoint
|
||||
Y -- "compass", reset compass
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ to communicate back to the user.
|
|||
|
||||
B -- battery low.
|
||||
E -- acknowledge, gesture understood.
|
||||
I -- unknown gesture.
|
||||
T -- start of new hour.
|
||||
|
||||
Three colored dots may appear on display. North is on the 12 o'clock
|
||||
|
|
@ -73,27 +75,18 @@ Todo:
|
|||
|
||||
*) only turn on compass when needed
|
||||
|
||||
*) only warn about battery low when it crosses thresholds, update
|
||||
battery low message
|
||||
|
||||
*) rename "show" to something else -- it collides with built-in
|
||||
|
||||
*) adjust clock according to GPS
|
||||
|
||||
*) show something more reasonable than (NOTEHERE).
|
||||
|
||||
*) hide messages after timeout.
|
||||
|
||||
*) show route lengths after the fact
|
||||
|
||||
*) implement longer recording than "G".
|
||||
|
||||
*) Probably T should be G.
|
||||
|
||||
*) sum gps distances for a day
|
||||
|
||||
*) allow setting up home altitude, or at least disable auto-calibration
|
||||
|
||||
*) show time-to-sunset / sunrise?
|
||||
|
||||
*) one-second updates when gps is active
|
||||
*) "myprofile" to read step length
|
||||
|
||||
?) display gps alt + offset to baro
|
||||
|
||||
?) start logging baro pressure
|
||||
|
||||
*) compute climb/descent
|
||||
|
||||
*) switch to compensated compass
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "sixths",
|
||||
"name": "Sixth sense",
|
||||
"version": "0.05",
|
||||
"version": "0.10",
|
||||
"description": "Clock for outdoor use with GPS support",
|
||||
"icon": "app.png",
|
||||
"readme": "README.md",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,222 @@
|
|||
// Sixth sense
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
// Options you'll want to edit
|
||||
const rest_altitude = 354;
|
||||
/* fmt library v0.2.2 */
|
||||
let fmt = {
|
||||
icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3",
|
||||
icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_hpa : "\x00\x08\x16\x01\x00\x80\xb0\xc8\x88\x88\x88\x00\xf0\x88\x84\x84\x88\xf0\x80\x8c\x92\x22\x25\x19\x00\x00",
|
||||
icon_9 : "\x00\x08\x16\x01\x00\x00\x00\x00\x38\x44\x44\x4c\x34\x04\x04\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_10 : "\x00\x08\x16\x01\x00\x08\x18\x28\x08\x08\x08\x00\x00\x18\x24\x24\x24\x24\x18\x00\x00\x00\x00\x00\x00\x00",
|
||||
|
||||
/* 0 .. DD.ddddd
|
||||
1 .. DD MM.mmm'
|
||||
2 .. DD MM'ss"
|
||||
*/
|
||||
geo_mode : 1,
|
||||
|
||||
init: function() {},
|
||||
fmtDist: function(km) {
|
||||
if (km >= 1.0) return km.toFixed(1) + this.icon_km;
|
||||
return (km*1000).toFixed(0) + this.icon_m;
|
||||
},
|
||||
fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); },
|
||||
fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; },
|
||||
fmtTemp: function(c) { return c.toFixed(1) + this.icon_c; },
|
||||
fmtPress: function(p) {
|
||||
if (p < 900 || p > 1100)
|
||||
return p.toFixed(0) + this.icon_hpa;
|
||||
if (p < 1000) {
|
||||
p -= 900;
|
||||
return this.icon_9 + this.add0(p.toFixed(0)) + this.icon_hpa;
|
||||
}
|
||||
p -= 1000;
|
||||
return this.icon_10 + this.add0(p.toFixed(0)) + this.icon_hpa;
|
||||
},
|
||||
draw_dot : 1,
|
||||
add0: function(i) {
|
||||
if (i > 9) {
|
||||
return ""+i;
|
||||
} else {
|
||||
return "0"+i;
|
||||
}
|
||||
},
|
||||
fmtTOD: function(now) {
|
||||
this.draw_dot = !this.draw_dot;
|
||||
let dot = ":";
|
||||
if (!this.draw_dot)
|
||||
dot = ".";
|
||||
return now.getHours() + dot + this.add0(now.getMinutes());
|
||||
},
|
||||
fmtNow: function() { return this.fmtTOD(new Date()); },
|
||||
fmtTimeDiff: function(d) {
|
||||
if (d < 180)
|
||||
return ""+d.toFixed(0);
|
||||
d = d/60;
|
||||
return ""+d.toFixed(0)+"m";
|
||||
},
|
||||
fmtAngle: function(x) {
|
||||
switch (this.geo_mode) {
|
||||
case 0:
|
||||
return "" + x;
|
||||
case 1: {
|
||||
let d = Math.floor(x);
|
||||
let m = x - d;
|
||||
m = m*60;
|
||||
return "" + d + " " + m.toFixed(3) + "'";
|
||||
}
|
||||
case 2: {
|
||||
let d = Math.floor(x);
|
||||
let m = x - d;
|
||||
m = m*60;
|
||||
let mf = Math.floor(m);
|
||||
let s = m - mf;
|
||||
s = s*60;
|
||||
return "" + d + " " + mf + "'" + s.toFixed(0) + '"';
|
||||
}
|
||||
}
|
||||
return "bad mode?";
|
||||
},
|
||||
fmtPos: function(pos) {
|
||||
let x = pos.lat;
|
||||
let c = "N";
|
||||
if (x<0) {
|
||||
c = "S";
|
||||
x = -x;
|
||||
}
|
||||
let s = c+this.fmtAngle(x) + "\n";
|
||||
c = "E";
|
||||
if (x<0) {
|
||||
c = "W";
|
||||
x = -x;
|
||||
}
|
||||
return s + c + this.fmtAngle(x);
|
||||
},
|
||||
fmtFix: function(fix, t) {
|
||||
if (fix && fix.fix && fix.lat) {
|
||||
return this.fmtSpeed(fix.speed) + " " +
|
||||
this.fmtAlt(fix.alt);
|
||||
} else {
|
||||
return "N/FIX " + this.fmtTimeDiff(t);
|
||||
}
|
||||
},
|
||||
fmtSpeed: function(kph) {
|
||||
return kph.toFixed(1) + this.icon_kph;
|
||||
},
|
||||
radians: function(a) { return a*Math.PI/180; },
|
||||
degrees: function(a) { return a*180/Math.PI; },
|
||||
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
// (Equirectangular approximation)
|
||||
// returns value in meters
|
||||
distance: function(a,b) {
|
||||
var x = this.radians(b.lon-a.lon) * Math.cos(this.radians((a.lat+b.lat)/2));
|
||||
var y = this.radians(b.lat-a.lat);
|
||||
return Math.sqrt(x*x + y*y) * 6371000;
|
||||
},
|
||||
// thanks to waypointer
|
||||
bearing: function(a,b) {
|
||||
var delta = this.radians(b.lon-a.lon);
|
||||
var alat = this.radians(a.lat);
|
||||
var blat = this.radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat) * Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(this.degrees(Math.atan2(y, x)));
|
||||
},
|
||||
};
|
||||
|
||||
/* gps library v0.1.2 */
|
||||
let gps = {
|
||||
emulator: -1,
|
||||
init: function(x) {
|
||||
this.emulator = (process.env.BOARD=="EMSCRIPTEN"
|
||||
|| process.env.BOARD=="EMSCRIPTEN2")?1:0;
|
||||
},
|
||||
state: {},
|
||||
on_gps: function(f) {
|
||||
let fix = this.getGPSFix();
|
||||
f(fix);
|
||||
|
||||
/*
|
||||
"lat": number, // Latitude in degrees
|
||||
"lon": number, // Longitude in degrees
|
||||
"alt": number, // altitude in M
|
||||
"speed": number, // Speed in kph
|
||||
"course": number, // Course in degrees
|
||||
"time": Date, // Current Time (or undefined if not known)
|
||||
"satellites": 7, // Number of satellites
|
||||
"fix": 1 // NMEA Fix state - 0 is no fix
|
||||
"hdop": number, // Horizontal Dilution of Precision
|
||||
*/
|
||||
this.state.timeout = setTimeout(this.on_gps, 1000, f);
|
||||
},
|
||||
off_gps: function() {
|
||||
clearTimeout(this.state.timeout);
|
||||
},
|
||||
getGPSFix: function() {
|
||||
if (!this.emulator)
|
||||
return Bangle.getGPSFix();
|
||||
let fix = {};
|
||||
fix.fix = 1;
|
||||
fix.lat = 50;
|
||||
fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */
|
||||
fix.alt = 200;
|
||||
fix.speed = 5;
|
||||
fix.course = 30;
|
||||
fix.time = Date();
|
||||
fix.satellites = 5;
|
||||
fix.hdop = 12;
|
||||
return fix;
|
||||
},
|
||||
gps_start : -1,
|
||||
start_gps: function() {
|
||||
Bangle.setGPSPower(1, "libgps");
|
||||
this.gps_start = getTime();
|
||||
},
|
||||
stop_gps: function() {
|
||||
Bangle.setGPSPower(0, "libgps");
|
||||
},
|
||||
};
|
||||
|
||||
/* sun version 0.0.1 */
|
||||
let sun = {
|
||||
SunCalc: null,
|
||||
lat: 50,
|
||||
lon: 14,
|
||||
init: function() {
|
||||
try {
|
||||
this.SunCalc = require("suncalc"); // from modules folder
|
||||
} catch (e) {
|
||||
print("Require error", e);
|
||||
}
|
||||
|
||||
print("Have suncalc: ", this.SunCalc);
|
||||
},
|
||||
get_sun_pos: function() {
|
||||
let d = new Date();
|
||||
let sun = this.SunCalc.getPosition(d, this.lat, this.lon);
|
||||
print(sun.azimuth, sun.altitude);
|
||||
return sun;
|
||||
},
|
||||
get_sun_time: function() {
|
||||
let d = new Date();
|
||||
let sun = this.SunCalc.getTimes(d, this.lat, this.lon);
|
||||
print(sun.sunrise, sun.sunset);
|
||||
return sun;
|
||||
},
|
||||
};
|
||||
|
||||
sun.init();
|
||||
sun.get_sun_pos();
|
||||
sun.get_sun_time();
|
||||
fmt.init();
|
||||
gps.init();
|
||||
|
||||
var location;
|
||||
|
||||
const W = g.getWidth();
|
||||
const H = g.getHeight();
|
||||
|
|
@ -12,19 +226,28 @@ var buzz = "", /* Set this to transmit morse via vibrations */
|
|||
inm = "", l = "", /* For incoming morse handling */
|
||||
in_str = "",
|
||||
note = "",
|
||||
debug = "v0.04.1", debug2 = "(otherdb)", debug3 = "(short)";
|
||||
debug = "v0.5.11", debug2 = "(otherdb)", debug3 = "(short)";
|
||||
var note_limit = 0;
|
||||
var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note, 2.. mark name
|
||||
var disp_mode = 0; // 0 .. normal, 1 .. small time
|
||||
|
||||
var state = {
|
||||
gps_limit: 0, // timeout -- when to stop recording
|
||||
gps_speed_limit: 0,
|
||||
prev_fix: null,
|
||||
gps_dist: 0,
|
||||
|
||||
// Marks
|
||||
cur_mark: null,
|
||||
};
|
||||
|
||||
// GPS handling
|
||||
var gps_on = 0, // time GPS was turned on
|
||||
last_fix = 0, // time of last fix
|
||||
last_restart = 0, last_pause = 0, last_fstart = 0; // utime
|
||||
last_restart = 0, last_pause = 0, // utime
|
||||
last_fstart = 0; // utime, time of start of last fix
|
||||
var gps_needed = 0, // how long to wait for a fix
|
||||
gps_limit = 0, // timeout -- when to stop recording
|
||||
gps_speed_limit = 0;
|
||||
var prev_fix = null;
|
||||
var gps_dist = 0;
|
||||
keep_fix_for = 30;
|
||||
|
||||
var mark_heading = -1;
|
||||
|
||||
|
|
@ -34,19 +257,9 @@ var draw_dot = false;
|
|||
var is_level = false;
|
||||
|
||||
// For altitude handling.
|
||||
var cur_altitude = 0;
|
||||
var cur_altitude = -1;
|
||||
var cur_temperature = 0;
|
||||
|
||||
// Marks
|
||||
var cur_mark = null;
|
||||
|
||||
// Icons
|
||||
|
||||
var icon_alt = "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
//var icon_m = "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
var icon_km = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
var icon_kph = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3";
|
||||
var icon_c = "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||
var night_pressure = 0;
|
||||
|
||||
function toMorse(x) {
|
||||
let r = "";
|
||||
|
|
@ -84,34 +297,28 @@ function gpsPause() {
|
|||
last_restart = 0;
|
||||
last_pause = getTime();
|
||||
}
|
||||
function gpsReset() {
|
||||
state.prev_fix = null;
|
||||
state.gps_dist = 0;
|
||||
}
|
||||
function gpsOn() {
|
||||
gps_on = getTime();
|
||||
gps_needed = 1000;
|
||||
last_fix = 0;
|
||||
prev_fix = null;
|
||||
gps_dist = 0;
|
||||
gpsRestart();
|
||||
}
|
||||
function gpsOff() {
|
||||
Bangle.setGPSPower(0, "sixths");
|
||||
gps_on = 0;
|
||||
}
|
||||
function fmtDist(km) { return km.toFixed(1) + icon_km; }
|
||||
function fmtSteps(n) { return fmtDist(0.001 * 0.719 * n); }
|
||||
function fmtAlt(m) { return m.toFixed(0) + icon_alt; }
|
||||
function fmtTimeDiff(d) {
|
||||
if (d < 180)
|
||||
return ""+d.toFixed(0);
|
||||
d = d/60;
|
||||
return ""+d.toFixed(0)+"m";
|
||||
}
|
||||
function gpsHandleFix(fix) {
|
||||
if (!prev_fix) {
|
||||
show("GPS acquired", 10);
|
||||
if (!state.prev_fix) {
|
||||
showMsg("GPS acquired", 10);
|
||||
doBuzz(" .");
|
||||
prev_fix = fix;
|
||||
state.prev_fix = fix;
|
||||
}
|
||||
if (1) {
|
||||
if (0) {
|
||||
/* Display error between GPS and system time */
|
||||
let now1 = Date();
|
||||
let now2 = fix.time;
|
||||
let n1 = now1.getMinutes() * 60 + now1.getSeconds();
|
||||
|
|
@ -119,42 +326,55 @@ function gpsHandleFix(fix) {
|
|||
debug2 = "te "+(n2-n1)+"s";
|
||||
}
|
||||
loggps(fix);
|
||||
let d = calcDistance(fix, prev_fix);
|
||||
let d = fmt.distance(fix, state.prev_fix);
|
||||
if (d > 30) {
|
||||
prev_fix = fix;
|
||||
gps_dist += d/1000;
|
||||
state.prev_fix = fix;
|
||||
state.gps_dist += d/1000;
|
||||
}
|
||||
}
|
||||
function gpsHandle() {
|
||||
let msg = "";
|
||||
debug2 = "Ne" + gps_needed;
|
||||
debug3 = "Ke" + keep_fix_for;
|
||||
if (!last_restart) {
|
||||
let d = (getTime()-last_pause);
|
||||
if (last_fix)
|
||||
msg = "PL"+ fmtTimeDiff(getTime()-last_fix);
|
||||
msg = "PL"+ fmt.fmtTimeDiff(getTime()-last_fix);
|
||||
else
|
||||
msg = "PN"+ fmtTimeDiff(getTime()-gps_on);
|
||||
msg = "PN"+ fmt.fmtTimeDiff(getTime()-gps_on);
|
||||
|
||||
print("gps on, paused ", d, gps_needed);
|
||||
if (d > gps_needed * 2) {
|
||||
gpsRestart();
|
||||
}
|
||||
} else {
|
||||
let fix = Bangle.getGPSFix();
|
||||
let fix = gps.getGPSFix();
|
||||
if (fix && fix.fix && fix.lat) {
|
||||
gpsHandleFix(fix);
|
||||
msg = fix.speed.toFixed(1) + icon_kph;
|
||||
print("GPS FIX", msg);
|
||||
msg = "";
|
||||
if (Math.abs(fix.alt - cur_altitude) > 20)
|
||||
msg += "!";
|
||||
if (Math.abs(fix.alt - cur_altitude) > 80)
|
||||
msg += "!";
|
||||
if (Math.abs(fix.alt - cur_altitude) > 320)
|
||||
msg += "!";
|
||||
msg += fmt.fmtSpeed(fix.speed);
|
||||
|
||||
if (!last_fstart)
|
||||
last_fstart = getTime();
|
||||
last_fix = getTime();
|
||||
keep_fix_for = (last_fstart - last_restart) / 1.5;
|
||||
if (keep_fix_for < 20)
|
||||
keep_fix_for = 20;
|
||||
if (keep_fix_for > 6*60)
|
||||
keep_fix_for = 6*60;
|
||||
gps_needed = 60;
|
||||
} else {
|
||||
if (last_fix)
|
||||
msg = "L"+ fmtTimeDiff(getTime()-last_fix);
|
||||
msg = "L"+ fmt.fmtTimeDiff(getTime()-last_fix);
|
||||
else {
|
||||
msg = "N"+ fmtTimeDiff(getTime()-gps_on);
|
||||
if (fix) {
|
||||
msg = "N"+ fmt.fmtTimeDiff(getTime()-gps_on);
|
||||
if (0 && fix) {
|
||||
msg += " " + fix.satellites + "sats";
|
||||
}
|
||||
}
|
||||
|
|
@ -162,50 +382,50 @@ function gpsHandle() {
|
|||
|
||||
let d = (getTime()-last_restart);
|
||||
let d2 = (getTime()-last_fstart);
|
||||
print("gps on, restarted ", d, gps_needed, d2, fix.lat);
|
||||
if (getTime() > gps_speed_limit &&
|
||||
(d > gps_needed || (last_fstart && d2 > 10))) {
|
||||
print("gps on, restarted ", d, gps_needed, d2);
|
||||
if (getTime() > state.gps_speed_limit &&
|
||||
((d > gps_needed && !last_fstart) || (last_fstart && d2 > keep_fix_for))) {
|
||||
gpsPause();
|
||||
gps_needed = gps_needed * 1.5;
|
||||
print("Pausing, next try", gps_needed);
|
||||
}
|
||||
}
|
||||
msg += " "+gps_dist.toFixed(1)+icon_km;
|
||||
msg += " "+fmt.fmtDist(state.gps_dist);
|
||||
return msg;
|
||||
}
|
||||
function markNew() {
|
||||
let r = {};
|
||||
r.time = getTime();
|
||||
r.fix = prev_fix;
|
||||
r.fix = state.prev_fix;
|
||||
r.steps = Bangle.getHealthStatus("day").steps;
|
||||
r.gps_dist = gps_dist;
|
||||
r.gps_dist = state.gps_dist;
|
||||
r.altitude = cur_altitude;
|
||||
r.name = "auto";
|
||||
return r;
|
||||
}
|
||||
function markHandle() {
|
||||
let m = cur_mark;
|
||||
let m = state.cur_mark;
|
||||
let msg = m.name + ">";
|
||||
if (m.time) {
|
||||
msg += fmtTimeDiff(getTime()- m.time);
|
||||
msg += fmt.fmtTimeDiff(getTime()- m.time);
|
||||
}
|
||||
if (prev_fix && prev_fix.fix && m.fix && m.fix.fix) {
|
||||
let s = fmtDist(calcDistance(m.fix, prev_fix)/1000) + icon_km;
|
||||
if (state.prev_fix && state.prev_fix.fix && m.fix && m.fix.fix) {
|
||||
let s = fmt.fmtDist(fmt.distance(m.fix, state.prev_fix)/1000) + fmt.icon_km;
|
||||
msg += " " + s;
|
||||
debug = "wp>" + s;
|
||||
mark_heading = 180 + calcBearing(m.fix, prev_fix);
|
||||
mark_heading = 180 + fmt.bearing(m.fix, state.prev_fix);
|
||||
debug2 = "wp>" + mark_heading;
|
||||
} else {
|
||||
msg += " w" + fmtDist(gps_dist - m.gps_dist);
|
||||
msg += " w" + fmt.fmtDist(state.gps_dist - m.gps_dist);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
function entryDone() {
|
||||
show(":" + in_str);
|
||||
showMsg(":" + in_str);
|
||||
doBuzz(" .");
|
||||
switch (mode) {
|
||||
case 1: logstamp(">" + in_str); break;
|
||||
case 2: cur_mark.name = in_str; break;
|
||||
case 2: state.cur_mark.name = in_str; break;
|
||||
}
|
||||
in_str = 0;
|
||||
mode = 0;
|
||||
|
|
@ -225,18 +445,22 @@ function selectWP(i) {
|
|||
if (sel_wp >= waypoints.length)
|
||||
sel_wp = waypoints.length - 1;
|
||||
if (sel_wp < 0) {
|
||||
show("No WPs", 60);
|
||||
showMsg("No WPs", 60);
|
||||
}
|
||||
let wp = waypoints[sel_wp];
|
||||
cur_mark = {};
|
||||
cur_mark.name = wp.name;
|
||||
cur_mark.gps_dist = 0; /* HACK */
|
||||
cur_mark.fix = {};
|
||||
cur_mark.fix.fix = 1;
|
||||
cur_mark.fix.lat = wp.lat;
|
||||
cur_mark.fix.lon = wp.lon;
|
||||
show("WP:"+wp.name, 60);
|
||||
print("Select waypoint: ", cur_mark);
|
||||
state.cur_mark = {};
|
||||
state.cur_mark.name = wp.name;
|
||||
state.cur_mark.gps_dist = 0; /* HACK */
|
||||
state.cur_mark.fix = {};
|
||||
state.cur_mark.fix.fix = 1;
|
||||
state.cur_mark.fix.lat = wp.lat;
|
||||
state.cur_mark.fix.lon = wp.lon;
|
||||
showMsg("WP:"+wp.name, 60);
|
||||
print("Select waypoint: ", state.cur_mark);
|
||||
}
|
||||
function ack(cmd) {
|
||||
showMsg(cmd, 3);
|
||||
doBuzz(' .');
|
||||
}
|
||||
function inputHandler(s) {
|
||||
print("Ascii: ", s, s[0], s[1]);
|
||||
|
|
@ -249,7 +473,7 @@ function inputHandler(s) {
|
|||
}
|
||||
if ((mode == 1) || (mode == 2)){
|
||||
in_str = in_str + s;
|
||||
show(">"+in_str, 10);
|
||||
showMsg(">"+in_str, 10);
|
||||
mode_time = getTime();
|
||||
return;
|
||||
}
|
||||
|
|
@ -262,34 +486,37 @@ function inputHandler(s) {
|
|||
else
|
||||
s = s+(bat/5);
|
||||
doBuzz(toMorse(s));
|
||||
show("Bat "+bat+"%", 60);
|
||||
showMsg("Bat "+bat+"%", 60);
|
||||
break;
|
||||
}
|
||||
case 'D': selectWP(1); break;
|
||||
case 'F': gpsOff(); show("GPS off", 3); break;
|
||||
case 'G': gpsOn(); gps_limit = getTime() + 60*60*4; show("GPS on", 3); break;
|
||||
case 'D': doBuzz(' .'); selectWP(1); break;
|
||||
case 'F': gpsOff(); ack("GPS off"); break;
|
||||
case 'T': gpsOn(); state.gps_limit = getTime() + 60*60*4; ack("GPS on"); break;
|
||||
case 'I':
|
||||
doBuzz(' .');
|
||||
disp_mode += 1;
|
||||
if (disp_mode == 2) {
|
||||
disp_mode = 0;
|
||||
}
|
||||
break;
|
||||
case 'L': aload("altimeter.app.js"); break;
|
||||
case 'M': mode = 2; show("M>", 10); cur_mark = markNew(); mode_time = getTime(); break;
|
||||
case 'N': mode = 1; show(">", 10); mode_time = getTime(); break;
|
||||
case 'M': doBuzz(' .'); mode = 2; showMsg("M>", 10); state.cur_mark = markNew(); mode_time = getTime(); break;
|
||||
case 'N': doBuzz(' .'); mode = 1; showMsg(">", 10); mode_time = getTime(); break;
|
||||
case 'O': aload("orloj.app.js"); break;
|
||||
case 'R': aload("runplus.app.js"); break;
|
||||
case 'S': gpsOn(); gps_limit = getTime() + 60*30; gps_speed_limit = gps_limit; show("GPS on", 3); break;
|
||||
case 'T': {
|
||||
case 'R': gpsReset(); ack("GPS reset"); break;
|
||||
case 'P': aload("runplus.app.js"); break;
|
||||
case 'S': gpsOn(); state.gps_limit = getTime() + 60*30; state.gps_speed_limit = state.gps_limit; ack("GPS speed"); break;
|
||||
case 'G': {
|
||||
s = ' T';
|
||||
let d = new Date();
|
||||
s += d.getHours() % 10;
|
||||
s += add0(d.getMinutes());
|
||||
s += fmt.add0(d.getMinutes());
|
||||
doBuzz(toMorse(s));
|
||||
break;
|
||||
}
|
||||
case 'U': selectWP(-1); break;
|
||||
case 'Y': doBuzz(buzz); Bangle.resetCompass(); break;
|
||||
case 'U': doBuzz(' .'); selectWP(-1); break;
|
||||
case 'Y': ack('Compass reset'); Bangle.resetCompass(); break;
|
||||
default: doBuzz(' ..'); showMsg("Unknown "+s, 5); break;
|
||||
}
|
||||
}
|
||||
const morseDict = {
|
||||
|
|
@ -389,19 +616,15 @@ function touchHandler(d) {
|
|||
|
||||
//print(inm, "drag:", d);
|
||||
}
|
||||
function add0(i) {
|
||||
if (i > 9) {
|
||||
return ""+i;
|
||||
} else {
|
||||
return "0"+i;
|
||||
}
|
||||
}
|
||||
var lastHour = -1, lastMin = -1;
|
||||
function logstamp(s) {
|
||||
logfile.write("utime=" + getTime() + " " + s + "\n");
|
||||
logfile.write("utime=" + getTime() +
|
||||
" bele=" + cur_altitude +
|
||||
" batperc=" + E.getBattery() +
|
||||
" " + s + "\n");
|
||||
}
|
||||
function loggps(fix) {
|
||||
logfile.write(fix.lat + " " + fix.lon + " ");
|
||||
logfile.write(fix.lat + " " + fix.lon + " ele=" + fix.alt + " ");
|
||||
logstamp("");
|
||||
}
|
||||
function hourly() {
|
||||
|
|
@ -410,15 +633,19 @@ function hourly() {
|
|||
let bat = E.getBattery();
|
||||
if (bat < 25) {
|
||||
s = ' B';
|
||||
show("Bat "+bat+"%", 60);
|
||||
showMsg("Bat "+bat+"%", 60);
|
||||
}
|
||||
if (is_active)
|
||||
doBuzz(toMorse(s));
|
||||
//logstamp("");
|
||||
}
|
||||
function show(msg, timeout) {
|
||||
function showMsg(msg, timeout) {
|
||||
note_limit = getTime() + timeout;
|
||||
note = msg;
|
||||
}
|
||||
|
||||
var prev_step = -1, this_step = -1;
|
||||
|
||||
function fivemin() {
|
||||
print("fivemin");
|
||||
let s = ' B';
|
||||
|
|
@ -429,7 +656,8 @@ function fivemin() {
|
|||
} catch (e) {
|
||||
print("Altimeter error", e);
|
||||
}
|
||||
|
||||
prev_step = this_step;
|
||||
this_step = Bangle.getStepCount();
|
||||
}
|
||||
function every(now) {
|
||||
if ((mode > 0) && (getTime() - mode_time > 10)) {
|
||||
|
|
@ -438,7 +666,7 @@ function every(now) {
|
|||
}
|
||||
mode = 0;
|
||||
}
|
||||
if (gps_on && getTime() > gps_limit && getTime() > gps_speed_limit) {
|
||||
if (gps_on && getTime() > state.gps_limit && getTime() > state.gps_speed_limit) {
|
||||
Bangle.setGPSPower(0, "sixths");
|
||||
gps_on = 0;
|
||||
}
|
||||
|
|
@ -447,38 +675,17 @@ function every(now) {
|
|||
lastHour = now.getHours();
|
||||
hourly();
|
||||
}
|
||||
if (lastMin / 5 != now.getMinutes() / 5) {
|
||||
if (lastMin / 5 != now.getMinutes() / 5) { // fixme, trunc?
|
||||
lastMin = now.getMinutes();
|
||||
fivemin();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function radians(a) { return a*Math.PI/180; }
|
||||
function degrees(a) { return a*180/Math.PI; }
|
||||
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
// (Equirectangular approximation)
|
||||
function calcDistance(a,b) {
|
||||
var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
return Math.sqrt(x*x + y*y) * 6371000;
|
||||
}
|
||||
// thanks to waypointer
|
||||
function calcBearing(a,b){
|
||||
var delta = radians(b.lon-a.lon);
|
||||
var alat = radians(a.lat);
|
||||
var blat = radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
function testBearing() {
|
||||
let p1 = {}, p2 = {};
|
||||
p1.lat = 40; p2.lat = 50;
|
||||
p1.lon = 14; p2.lon = 14;
|
||||
print("bearing = ", calcBearing(p1, p2));
|
||||
print("bearing = ", fmt.bearing(p1, p2));
|
||||
}
|
||||
|
||||
function radA(p) { return p*(Math.PI*2); }
|
||||
|
|
@ -509,9 +716,9 @@ function drawBackground() {
|
|||
drawDot(h, 0.7, 10);
|
||||
}
|
||||
}
|
||||
if (prev_fix && prev_fix.fix) {
|
||||
if (state.prev_fix && state.prev_fix.fix) {
|
||||
g.setColor(0.5, 1, 0.5);
|
||||
drawDot(prev_fix.course, 0.5, 6);
|
||||
drawDot(state.prev_fix.course, 0.5, 6);
|
||||
}
|
||||
if (mark_heading != -1) {
|
||||
g.setColor(1, 0.5, 0.5);
|
||||
|
|
@ -523,13 +730,66 @@ function drawTime(now) {
|
|||
g.setFont('Vector', 60);
|
||||
else
|
||||
g.setFont('Vector', 26);
|
||||
g.setFontAlign(1, 1);
|
||||
g.setFontAlign(1, -1);
|
||||
draw_dot = !draw_dot;
|
||||
let dot = ":";
|
||||
if (!draw_dot)
|
||||
dot = ".";
|
||||
g.drawString(now.getHours() + dot + add0(now.getMinutes()), W, 90);
|
||||
let s = "";
|
||||
if (disp_mode == 1)
|
||||
s = debug;
|
||||
g.drawString(s + now.getHours() + dot + fmt.add0(now.getMinutes()), W, 28);
|
||||
}
|
||||
|
||||
var base_alt = -1, ext_alt = -1, tot_down = 0, tot_up = 0;
|
||||
|
||||
function walkHandle() {
|
||||
let msg = "";
|
||||
let step = Bangle.getStepCount();
|
||||
let cur = cur_altitude;
|
||||
if (base_alt == -1) {
|
||||
base_alt = cur;
|
||||
ext_alt = cur;
|
||||
}
|
||||
if (this_step - prev_step > 100
|
||||
|| 1
|
||||
|| step - this_step > 100) {
|
||||
//msg += fmt.fmtSteps((this_step - prev_step) * 12);
|
||||
|
||||
let dir = ext_alt > base_alt; /* 1.. climb */
|
||||
if (!dir) dir = -1;
|
||||
let hyst = 2;
|
||||
if (Math.abs(cur - base_alt) > hyst) {
|
||||
if (cur*dir > ext_alt*dir) {
|
||||
ext_alt = cur;
|
||||
}
|
||||
}
|
||||
let diff = ext_alt - base_alt;
|
||||
if (cur*dir < (ext_alt - hyst*dir)*dir) {
|
||||
if (1 == dir) {
|
||||
tot_up += diff;
|
||||
}
|
||||
if (-1 == dir) {
|
||||
tot_down += -diff;
|
||||
}
|
||||
base_alt = ext_alt;
|
||||
ext_alt = cur;
|
||||
}
|
||||
let tmp_down = tot_down, tmp_up = tot_up;
|
||||
if (1 == dir) {
|
||||
tmp_up += diff;
|
||||
}
|
||||
if (-1 == dir) {
|
||||
tmp_down += -diff;
|
||||
}
|
||||
|
||||
msg += " " + fmt.fmtAlt(tmp_down) + " " + fmt.fmtAlt(tmp_up);
|
||||
|
||||
return msg + "\n";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (disp_mode == 2) {
|
||||
draw_all();
|
||||
|
|
@ -542,16 +802,6 @@ function draw() {
|
|||
g.setColor(0.25, 1, 1);
|
||||
g.fillPoly([ W/2, 24, W, 80, 0, 80 ]);
|
||||
}
|
||||
let msg = "";
|
||||
if (gps_on) {
|
||||
msg = gpsHandle();
|
||||
} else {
|
||||
let o = Bangle.getOptions();
|
||||
msg = o.seaLevelPressure.toFixed(1) + "hPa";
|
||||
if (note != "") {
|
||||
msg = note;
|
||||
}
|
||||
}
|
||||
drawBackground();
|
||||
|
||||
let now = new Date();
|
||||
|
|
@ -562,43 +812,75 @@ function draw() {
|
|||
|
||||
//let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps;
|
||||
|
||||
g.setFontAlign(-1, 1);
|
||||
g.setFont('Vector', 26);
|
||||
// 33 still fits
|
||||
g.setFont('Vector', 30);
|
||||
|
||||
const weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
||||
|
||||
g.drawString(weekday[now.getDay()] + "" + now.getDate() + ". "
|
||||
+ fmtSteps(Bangle.getHealthStatus("day").steps), 10, 115);
|
||||
let msg = weekday[now.getDay()] + "" + now.getDate() + ". "
|
||||
+ fmt.fmtSteps(Bangle.getHealthStatus("day").steps) + "\n";
|
||||
|
||||
g.drawString(msg, 10, 145);
|
||||
if (gps_on) {
|
||||
msg += gpsHandle() + "\n";
|
||||
}
|
||||
|
||||
if (state.cur_mark) {
|
||||
msg += markHandle() + "\n";
|
||||
}
|
||||
|
||||
if (note != "") {
|
||||
if (getTime() > note_limit)
|
||||
note = "";
|
||||
else
|
||||
msg += note + "\n";
|
||||
}
|
||||
|
||||
msg += walkHandle();
|
||||
|
||||
if (getTime() - last_active > 15*60) {
|
||||
let alt_adjust = cur_altitude - rest_altitude;
|
||||
let alt_adjust = cur_altitude - location.alt;
|
||||
let abs = Math.abs(alt_adjust);
|
||||
print("adj", alt_adjust);
|
||||
let o = Bangle.getOptions();
|
||||
if (abs > 10 && abs < 150) {
|
||||
let a = 0.01;
|
||||
// FIXME: draw is called often compared to alt reading
|
||||
if (cur_altitude > rest_altitude)
|
||||
if (cur_altitude > location.alt)
|
||||
a = -a;
|
||||
o.seaLevelPressure = o.seaLevelPressure + a;
|
||||
Bangle.setOptions(o);
|
||||
}
|
||||
msg = o.seaLevelPressure.toFixed(1) + "hPa";
|
||||
let pr = o.seaLevelPressure;
|
||||
if (pr)
|
||||
msg += fmt.fmtPress(pr);
|
||||
else
|
||||
msg += "emu?";
|
||||
} else {
|
||||
msg = fmtAlt(cur_altitude);
|
||||
msg += fmt.fmtAlt(cur_altitude);
|
||||
}
|
||||
msg = msg + " " + cur_temperature.toFixed(1)+icon_c;
|
||||
if (cur_mark) {
|
||||
msg = markHandle();
|
||||
}
|
||||
g.drawString(msg, 10, 175);
|
||||
|
||||
if (disp_mode == 1) {
|
||||
g.drawString(debug, 10, 45);
|
||||
g.drawString(debug2, 10, 65);
|
||||
g.drawString(debug3, 10, 85);
|
||||
msg = msg + " " + fmt.fmtTemp(cur_temperature) + "\n";
|
||||
|
||||
{
|
||||
let o = Bangle.getOptions();
|
||||
let pr = o.seaLevelPressure;
|
||||
|
||||
if (now.getHours() < 6)
|
||||
night_pressure = pr;
|
||||
if (night_pressure)
|
||||
msg += (pr-night_pressure).toFixed(1) + fmt.icon_hpa + " ";
|
||||
if (pr)
|
||||
msg += fmt.fmtPress(pr) + "\n";
|
||||
}
|
||||
g.setFontAlign(-1, -1);
|
||||
if (disp_mode == 0)
|
||||
g.drawString(msg, 10, 85);
|
||||
else
|
||||
g.drawString(msg, 10, 60);
|
||||
|
||||
if (0 && disp_mode == 1) {
|
||||
g.setFont('Vector', 21);
|
||||
g.drawString(debug + "\n" + debug2 + "\n" + debug3, 10, 20);
|
||||
}
|
||||
|
||||
queueDraw();
|
||||
|
|
@ -611,7 +893,7 @@ function draw_all() {
|
|||
g.setColor(1, 1, 1);
|
||||
g.setFontAlign(-1, 1);
|
||||
let now = new Date();
|
||||
g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40);
|
||||
g.drawString(now.getHours() + ":" + fmt.add0(now.getMinutes()) + ":" + fmt.add0(now.getSeconds()), 10, 40);
|
||||
|
||||
let acc = Bangle.getAccel();
|
||||
let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y;
|
||||
|
|
@ -727,7 +1009,8 @@ function lockHandler(locked) {
|
|||
|
||||
function queueDraw() {
|
||||
let next;
|
||||
if (getTime() - last_unlocked > 3*60)
|
||||
if ((getTime() - last_unlocked > 3*60) &&
|
||||
(getTime() > state.gps_limit))
|
||||
next = 60000;
|
||||
else
|
||||
next = 1000;
|
||||
|
|
@ -754,6 +1037,8 @@ function start() {
|
|||
}
|
||||
|
||||
draw();
|
||||
location = require("Storage").readJSON("mylocation.json",1)||{"lat":50,"lon":14.45,"alt":354,"location":"Woods"};
|
||||
state = require("Storage").readJSON("sixths.json",1)||state;
|
||||
loadWPs();
|
||||
buzzTask();
|
||||
if (0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Trail Rail 
|
||||
|
||||
Simple app to follow GPX track
|
||||
|
||||
Written by: [Pavel Machek](https://github.com/pavelmachek)
|
||||
|
||||
After GPS fix is acquired, it displays familiar arrow with road in
|
||||
front of you. It never stores whole track in memory, so it should work
|
||||
with fairly large files.
|
||||
|
||||
GPX files can be obtained from various services, www.mapy.cz is one of
|
||||
them (actually uses openstreetmap data for most of the world).
|
||||
|
||||
## Preparing data
|
||||
|
||||
"gpx2egt.sh < file.gpx > t.name.egt" can be used to prepare data, then
|
||||
upload it to watch.
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwhHXAH4AHgAqpvownEQ4wEMEYw8YMgw4F84wwMH4woeQQvlEwwvCGFgvqE4gvDGFAvHAFQv/AH74YElYvjF3Je/FzV9F8wuLF8QhHL34u/RqSOjFxYvpF2gvlRQwuoAAIvqFoQvJFsoupF9wtFF+CNuX34xfF1YwDF9oA/AH4AyA=="))
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
grep "trkpt lat" | sed 's/.*trkpt.lat=.//' | sed 's/. lon=./ /' | sed 's/".$//'
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{ "id": "trail",
|
||||
"name": "Trail Rail",
|
||||
"version":"0.01",
|
||||
"description": "Follow a GPX track in car or on bike",
|
||||
"icon": "app.png",
|
||||
"readme": "README.md",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"tags": "outdoors,gps,osm",
|
||||
"storage": [
|
||||
{"name":"trail.app.js","url":"trail.app.js"},
|
||||
{"name":"trail.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,651 @@
|
|||
// "Rail trail"? "Trail rail"!
|
||||
|
||||
/* fmt library v0.2.2 */
|
||||
let fmt = {
|
||||
icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3",
|
||||
icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_hpa : "\x00\x08\x16\x01\x00\x80\xb0\xc8\x88\x88\x88\x00\xf0\x88\x84\x84\x88\xf0\x80\x8c\x92\x22\x25\x19\x00\x00",
|
||||
icon_9 : "\x00\x08\x16\x01\x00\x00\x00\x00\x38\x44\x44\x4c\x34\x04\x04\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
||||
icon_10 : "\x00\x08\x16\x01\x00\x08\x18\x28\x08\x08\x08\x00\x00\x18\x24\x24\x24\x24\x18\x00\x00\x00\x00\x00\x00\x00",
|
||||
|
||||
/* 0 .. DD.ddddd
|
||||
1 .. DD MM.mmm'
|
||||
2 .. DD MM'ss"
|
||||
*/
|
||||
geo_mode : 1,
|
||||
|
||||
init: function() {},
|
||||
fmtDist: function(km) {
|
||||
if (km >= 1.0) return km.toFixed(1) + this.icon_km;
|
||||
return (km*1000).toFixed(0) + this.icon_m;
|
||||
},
|
||||
fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); },
|
||||
fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; },
|
||||
fmtTemp: function(c) { return c.toFixed(1) + this.icon_c; },
|
||||
fmtPress: function(p) {
|
||||
if (p < 900 || p > 1100)
|
||||
return p.toFixed(0) + this.icon_hpa;
|
||||
if (p < 1000) {
|
||||
p -= 900;
|
||||
return this.icon_9 + this.add0(p.toFixed(0)) + this.icon_hpa;
|
||||
}
|
||||
p -= 1000;
|
||||
return this.icon_10 + this.add0(p.toFixed(0)) + this.icon_hpa;
|
||||
},
|
||||
draw_dot : 1,
|
||||
add0: function(i) {
|
||||
if (i > 9) {
|
||||
return ""+i;
|
||||
} else {
|
||||
return "0"+i;
|
||||
}
|
||||
},
|
||||
fmtTOD: function(now) {
|
||||
this.draw_dot = !this.draw_dot;
|
||||
let dot = ":";
|
||||
if (!this.draw_dot)
|
||||
dot = ".";
|
||||
return now.getHours() + dot + this.add0(now.getMinutes());
|
||||
},
|
||||
fmtNow: function() { return this.fmtTOD(new Date()); },
|
||||
fmtTimeDiff: function(d) {
|
||||
if (d < 180)
|
||||
return ""+d.toFixed(0);
|
||||
d = d/60;
|
||||
return ""+d.toFixed(0)+"m";
|
||||
},
|
||||
fmtAngle: function(x) {
|
||||
switch (this.geo_mode) {
|
||||
case 0:
|
||||
return "" + x;
|
||||
case 1: {
|
||||
let d = Math.floor(x);
|
||||
let m = x - d;
|
||||
m = m*60;
|
||||
return "" + d + " " + m.toFixed(3) + "'";
|
||||
}
|
||||
case 2: {
|
||||
let d = Math.floor(x);
|
||||
let m = x - d;
|
||||
m = m*60;
|
||||
let mf = Math.floor(m);
|
||||
let s = m - mf;
|
||||
s = s*60;
|
||||
return "" + d + " " + mf + "'" + s.toFixed(0) + '"';
|
||||
}
|
||||
}
|
||||
return "bad mode?";
|
||||
},
|
||||
fmtPos: function(pos) {
|
||||
let x = pos.lat;
|
||||
let c = "N";
|
||||
if (x<0) {
|
||||
c = "S";
|
||||
x = -x;
|
||||
}
|
||||
let s = c+this.fmtAngle(x) + "\n";
|
||||
c = "E";
|
||||
if (x<0) {
|
||||
c = "W";
|
||||
x = -x;
|
||||
}
|
||||
return s + c + this.fmtAngle(x);
|
||||
},
|
||||
fmtFix: function(fix, t) {
|
||||
if (fix && fix.fix && fix.lat) {
|
||||
return this.fmtSpeed(fix.speed) + " " +
|
||||
this.fmtAlt(fix.alt);
|
||||
} else {
|
||||
return "N/FIX " + this.fmtTimeDiff(t);
|
||||
}
|
||||
},
|
||||
fmtSpeed: function(kph) {
|
||||
return kph.toFixed(1) + this.icon_kph;
|
||||
},
|
||||
radians: function(a) { return a*Math.PI/180; },
|
||||
degrees: function(a) { return a*180/Math.PI; },
|
||||
// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
// (Equirectangular approximation)
|
||||
// returns value in meters
|
||||
distance: function(a,b) {
|
||||
var x = this.radians(b.lon-a.lon) * Math.cos(this.radians((a.lat+b.lat)/2));
|
||||
var y = this.radians(b.lat-a.lat);
|
||||
return Math.sqrt(x*x + y*y) * 6371000;
|
||||
},
|
||||
// thanks to waypointer
|
||||
bearing: function(a,b) {
|
||||
var delta = this.radians(b.lon-a.lon);
|
||||
var alat = this.radians(a.lat);
|
||||
var blat = this.radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat) * Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(this.degrees(Math.atan2(y, x)));
|
||||
},
|
||||
};
|
||||
|
||||
/* gps library v0.1.2 */
|
||||
let gps = {
|
||||
emulator: -1,
|
||||
init: function(x) {
|
||||
this.emulator = (process.env.BOARD=="EMSCRIPTEN"
|
||||
|| process.env.BOARD=="EMSCRIPTEN2")?1:0;
|
||||
},
|
||||
state: {},
|
||||
on_gps: function(f) {
|
||||
let fix = this.getGPSFix();
|
||||
f(fix);
|
||||
|
||||
/*
|
||||
"lat": number, // Latitude in degrees
|
||||
"lon": number, // Longitude in degrees
|
||||
"alt": number, // altitude in M
|
||||
"speed": number, // Speed in kph
|
||||
"course": number, // Course in degrees
|
||||
"time": Date, // Current Time (or undefined if not known)
|
||||
"satellites": 7, // Number of satellites
|
||||
"fix": 1 // NMEA Fix state - 0 is no fix
|
||||
"hdop": number, // Horizontal Dilution of Precision
|
||||
*/
|
||||
this.state.timeout = setTimeout(this.on_gps, 1000, f);
|
||||
},
|
||||
off_gps: function() {
|
||||
clearTimeout(this.state.timeout);
|
||||
},
|
||||
getGPSFix: function() {
|
||||
if (!this.emulator)
|
||||
return Bangle.getGPSFix();
|
||||
let fix = {};
|
||||
fix.fix = 1;
|
||||
fix.lat = 50;
|
||||
fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */
|
||||
fix.alt = 200;
|
||||
fix.speed = 5;
|
||||
fix.course = 30;
|
||||
fix.time = Date();
|
||||
fix.satellites = 5;
|
||||
fix.hdop = 12;
|
||||
return fix;
|
||||
},
|
||||
gps_start : -1,
|
||||
start_gps: function() {
|
||||
Bangle.setGPSPower(1, "libgps");
|
||||
this.gps_start = getTime();
|
||||
},
|
||||
stop_gps: function() {
|
||||
Bangle.setGPSPower(0, "libgps");
|
||||
},
|
||||
};
|
||||
|
||||
/* ui library 0.1.2 */
|
||||
let ui = {
|
||||
display: 0,
|
||||
numScreens: 2,
|
||||
drawMsg: function(msg) {
|
||||
g.reset().setFont("Vector", 35)
|
||||
.setColor(1,1,1)
|
||||
.fillRect(0, this.wi, 176, 176)
|
||||
.setColor(0,0,0)
|
||||
.drawString(msg, 5, 30)
|
||||
.flip();
|
||||
},
|
||||
drawBusy: function() {
|
||||
this.drawMsg("\n.oO busy");
|
||||
},
|
||||
nextScreen: function() {
|
||||
print("nextS");
|
||||
this.display = this.display + 1;
|
||||
if (this.display == this.numScreens)
|
||||
this.display = 0;
|
||||
this.drawBusy();
|
||||
},
|
||||
prevScreen: function() {
|
||||
print("prevS");
|
||||
this.display = this.display - 1;
|
||||
if (this.display < 0)
|
||||
this.display = this.numScreens - 1;
|
||||
this.drawBusy();
|
||||
},
|
||||
onSwipe: function(dir) {
|
||||
this.nextScreen();
|
||||
},
|
||||
h: 176,
|
||||
w: 176,
|
||||
wi: 32,
|
||||
last_b: 0,
|
||||
touchHandler: function(d) {
|
||||
let x = Math.floor(d.x);
|
||||
let y = Math.floor(d.y);
|
||||
|
||||
if (d.b != 1 || this.last_b != 0) {
|
||||
this.last_b = d.b;
|
||||
return;
|
||||
}
|
||||
|
||||
print("touch", x, y, this.h, this.w);
|
||||
|
||||
/*
|
||||
if ((x<this.h/2) && (y<this.w/2)) {
|
||||
}
|
||||
if ((x>this.h/2) && (y<this.w/2)) {
|
||||
}
|
||||
*/
|
||||
|
||||
if ((x<this.h/2) && (y>this.w/2)) {
|
||||
print("prev");
|
||||
this.prevScreen();
|
||||
}
|
||||
if ((x>this.h/2) && (y>this.w/2)) {
|
||||
print("next");
|
||||
this.nextScreen();
|
||||
}
|
||||
},
|
||||
init: function() {
|
||||
this.drawBusy();
|
||||
}
|
||||
};
|
||||
|
||||
/* egt 0.0.1 */
|
||||
let egt = {
|
||||
init: function() {
|
||||
},
|
||||
parse: function(l) {
|
||||
let r = {};
|
||||
let s = l.split(' ');
|
||||
|
||||
if (s === undefined)
|
||||
return r;
|
||||
|
||||
if (s[1] === undefined)
|
||||
return r;
|
||||
|
||||
if (s[1].split('=')[1] === undefined) {
|
||||
r.lat = 1 * s[0];
|
||||
r.lon = 1 * s[1];
|
||||
}
|
||||
|
||||
for (let fi of s) {
|
||||
let f = fi.split('=');
|
||||
if (f[0] == "utime") {
|
||||
r.utime = 1 * f[1];
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
},
|
||||
};
|
||||
|
||||
function toCartesian(v) {
|
||||
const R = 6371; // Poloměr Země v km
|
||||
const latRad = v.lat * Math.PI / 180;
|
||||
const lonRad = v.lon * Math.PI / 180;
|
||||
|
||||
const x = R * lonRad * Math.cos(latRad);
|
||||
const y = R * latRad;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function distSegment(x1, x2, xP) {
|
||||
// Převod zeměpisných souřadnic na kartézské souřadnice
|
||||
const p1 = toCartesian(x1);
|
||||
const p2 = toCartesian(x2);
|
||||
const p = toCartesian(xP);
|
||||
|
||||
// Vektor p1p2
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
|
||||
// Projekce bodu p na přímku definovanou body p1 a p2
|
||||
const dot = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy);
|
||||
|
||||
// Určení bodu na přímce, kde leží projekce
|
||||
let closestX, closestY;
|
||||
if (dot < 0) {
|
||||
closestX = p1.x;
|
||||
closestY = p1.y;
|
||||
} else if (dot > 1) {
|
||||
closestX = p2.x;
|
||||
closestY = p2.y;
|
||||
} else {
|
||||
closestX = p1.x + dot * dx;
|
||||
closestY = p1.y + dot * dy;
|
||||
}
|
||||
|
||||
// Vzdálenost mezi bodem p a nejbližším bodem na úsečce
|
||||
const distance = Math.sqrt((p.x - closestX) * (p.x - closestX) + (p.y - closestY) * (p.y - closestY));
|
||||
|
||||
return distance * 1000;
|
||||
}
|
||||
|
||||
function angleDifference(angle1, angle2) {
|
||||
// Compute the difference
|
||||
let difference = angle2 - angle1;
|
||||
|
||||
// Normalize the difference to be within the range -180° to 180°
|
||||
while (difference > 180) difference -= 360;
|
||||
while (difference < -180) difference += 360;
|
||||
|
||||
return difference;
|
||||
}
|
||||
|
||||
function drawThickLine(x1, y1, x2, y2, thickness) {
|
||||
// Calculate the perpendicular offset for the line thickness
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
const offsetX = (dy / length) * (thickness / 2);
|
||||
const offsetY = (dx / length) * (thickness / 2);
|
||||
|
||||
// Draw multiple lines to simulate thickness
|
||||
for (let i = -thickness / 2; i <= thickness / 2; i++) {
|
||||
g.drawLine(
|
||||
x1 + offsetX * i,
|
||||
y1 - offsetY * i,
|
||||
x2 + offsetX * i,
|
||||
y2 - offsetY * i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toxy(pp, p) {
|
||||
let r = {};
|
||||
let d = fmt.distance(pp, p);
|
||||
let h = fmt.radians(fmt.bearing(pp, p) - pp.course);
|
||||
let x = pp.x, y = pp.y;
|
||||
x += d * pp.ppm * Math.sin(h);
|
||||
y -= d * pp.ppm * Math.cos(h);
|
||||
r.x = x;
|
||||
r.y = y;
|
||||
return r;
|
||||
}
|
||||
|
||||
function paint(pp, p1, p2, thick) {
|
||||
let d1 = toxy(pp, p1);
|
||||
let d2 = toxy(pp, p2);
|
||||
drawThickLine(d1.x, d1.y, d2.x, d2.y, thick);
|
||||
}
|
||||
|
||||
var destination = {}, num = 0, dist = 0;
|
||||
|
||||
function read(pp, n) {
|
||||
g.reset().clear();
|
||||
let f = require("Storage").open(n+".st", "r");
|
||||
let l = f.readLine();
|
||||
let prev = 0;
|
||||
while (l!==undefined) {
|
||||
num++;
|
||||
l = ""+l;
|
||||
//print(l);
|
||||
let p = egt.parse(l);
|
||||
if (p.lat) {
|
||||
if (prev) {
|
||||
dist += fmt.distance(prev, p);
|
||||
//paint(pp, prev, p);
|
||||
}
|
||||
prev = p;
|
||||
}
|
||||
l = f.readLine();
|
||||
if (!(num % 100)) {
|
||||
ui.drawMsg(num + "\n" + fmt.fmtDist(dist / 1000));
|
||||
print(num, "points");
|
||||
}
|
||||
}
|
||||
ui.drawMsg(num + "\n" + fmt.fmtDist(dist / 1000));
|
||||
destination = prev;
|
||||
}
|
||||
|
||||
function time_read(n) {
|
||||
print("Converting...");
|
||||
to_storage(n);
|
||||
print("Running...");
|
||||
let v1 = getTime();
|
||||
let pp = {};
|
||||
pp.lat = 50;
|
||||
pp.lon = 14.75;
|
||||
pp.ppm = 0.08; /* Pixels per meter */
|
||||
pp.course = 270;
|
||||
read(pp, n);
|
||||
let v2 = getTime();
|
||||
print("Read took", (v2-v1), "seconds");
|
||||
step_init();
|
||||
print(num, "points", dist, "distance");
|
||||
setTimeout(step, 1000);
|
||||
}
|
||||
|
||||
var track_name = "", inf, point_num, track = [], track_points = 30, north = {};
|
||||
|
||||
function step_init() {
|
||||
inf = require("Storage").open(track_name + ".st", "r");
|
||||
north = {};
|
||||
north.lat = 89.9;
|
||||
north.lon = 0;
|
||||
point_num = 0;
|
||||
track = [];
|
||||
}
|
||||
|
||||
function load_next() {
|
||||
while (track.length < track_points) {
|
||||
let l = inf.readLine();
|
||||
if (l === undefined) {
|
||||
print("End of track");
|
||||
ui.drawMsg("End of track");
|
||||
break;
|
||||
}
|
||||
let p = egt.parse(l);
|
||||
if (!p.lat) {
|
||||
print("No latitude?");
|
||||
continue;
|
||||
}
|
||||
p.point_num = point_num++;
|
||||
p.passed = 0;
|
||||
print("Loading ", p.point_num);
|
||||
track.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
function paint_all(pp) {
|
||||
let prev = 0;
|
||||
let mDist = 99999999999, m = 0;
|
||||
const fast = 0;
|
||||
|
||||
g.setColor(1, 0, 0);
|
||||
for (let i = 1; i < track.length; i++) {
|
||||
let p = track[i];
|
||||
prev = track[i-1];
|
||||
if (0 && fmt.distance(p, pp) < 100)
|
||||
p.passed = 1;
|
||||
if (!fast) {
|
||||
let d = distSegment(prev, p, pp);
|
||||
if (d < mDist) {
|
||||
mDist = d;
|
||||
m = i;
|
||||
} else {
|
||||
g.setColor(0, 0, 0);
|
||||
}
|
||||
}
|
||||
paint(pp, prev, p, 3);
|
||||
}
|
||||
if (fast)
|
||||
return { quiet: 0, offtrack : 0 };
|
||||
print("Best segment was", m, "dist", mDist);
|
||||
let ahead = 0, a = fmt.bearing(track[m-1], track[m]), quiet = -1;
|
||||
for (let i = m+1; i < track.length; i++) {
|
||||
let a2 = fmt.bearing(track[i-1], track[i]);
|
||||
let ad = angleDifference(a, a2);
|
||||
if (Math.abs(ad) > 20) {
|
||||
if (quiet == -1)
|
||||
quiet = ahead + fmt.distance(pp, track[i-1]);
|
||||
print("...straight", ahead);
|
||||
a = a2;
|
||||
}
|
||||
ahead += fmt.distance(track[i-1], track[i]);
|
||||
}
|
||||
print("...see", ahead);
|
||||
return { quiet: quiet, offtrack: mDist };
|
||||
}
|
||||
|
||||
function step_to(pp, pass_all) {
|
||||
pp.x = ui.w/2;
|
||||
pp.y = ui.h*0.66;
|
||||
|
||||
g.setColor(0.5, 0.5, 1);
|
||||
let sc = 2.5;
|
||||
g.fillPoly([ pp.x, pp.y, pp.x - 5*sc, pp.y + 12*sc, pp.x + 5*sc, pp.y + 12*sc ]);
|
||||
|
||||
if (0) {
|
||||
g.setColor(0.5, 0.5, 1);
|
||||
paint(pp, pp, destination, 1);
|
||||
|
||||
g.setColor(1, 0.5, 0.5);
|
||||
paint(pp, pp, north, 1);
|
||||
}
|
||||
|
||||
let quiet = paint_all(pp);
|
||||
|
||||
if ((pass_all || track[0].passed) && distSegment(track[0], track[1], pp) > 150) {
|
||||
print("Dropping ", track[0].point_num);
|
||||
track.shift();
|
||||
}
|
||||
return quiet;
|
||||
}
|
||||
|
||||
var demo_mode = 0;
|
||||
|
||||
function step() {
|
||||
const fast = 0;
|
||||
let v1 = getTime();
|
||||
g.reset().clear();
|
||||
|
||||
let fix = gps.getGPSFix();
|
||||
|
||||
load_next();
|
||||
|
||||
let pp = fix;
|
||||
pp.ppm = 0.08 * 3; /* Pixels per meter */
|
||||
|
||||
if (!fix.fix) {
|
||||
let i = 2;
|
||||
pp.lat = track[i].lat;
|
||||
pp.lon = track[i].lon;
|
||||
pp.course = fmt.bearing(track[i], track[i+1]);
|
||||
}
|
||||
|
||||
let quiet = step_to(pp, 1);
|
||||
|
||||
if (!fast) {
|
||||
g.setFont("Vector", 31);
|
||||
g.setFontAlign(-1, -1);
|
||||
let msg = "\noff " + fmt.fmtDist(quiet.offtrack/1000);
|
||||
g.drawString(fmt.fmtFix(fix, getTime()-gps.gps_start) + msg, 3, 3);
|
||||
}
|
||||
if (!fast) {
|
||||
g.setFont("Vector", 23);
|
||||
g.setColor(0, 0, 0);
|
||||
g.setFontAlign(-1, 1);
|
||||
g.drawString(fmt.fmtNow(), 3, ui.h);
|
||||
g.setFontAlign(1, 1);
|
||||
g.drawString(fmt.fmtDist(quiet.quiet/1000), ui.w-3, ui.h);
|
||||
}
|
||||
|
||||
if (quiet < 200)
|
||||
Bangle.setLCDPower(1);
|
||||
|
||||
if (demo_mode)
|
||||
track.shift();
|
||||
let v2 = getTime();
|
||||
print("Step took", (v2-v1), "seconds");
|
||||
setTimeout(step, 100);
|
||||
}
|
||||
|
||||
function recover() {
|
||||
ui.drawMsg("Recover...");
|
||||
step_init();
|
||||
let fix = gps.getGPSFix();
|
||||
let pp = fix;
|
||||
pp.ppm = 0.08 * 3; /* Pixels per meter */
|
||||
if (!fix.fix) {
|
||||
print("Can't recover with no fix\n");
|
||||
fix.lat = 50.0122;
|
||||
fix.lon = 14.7780;
|
||||
}
|
||||
load_next();
|
||||
load_next();
|
||||
while(1) {
|
||||
let d = distSegment(track[0], track[1], pp);
|
||||
print("Recover, d", d);
|
||||
if (d < 400)
|
||||
break;
|
||||
track.shift();
|
||||
if (0)
|
||||
step_to(pp, 1);
|
||||
load_next();
|
||||
ui.drawMsg("Recover\n" + fmt.fmtDist(d / 1000));
|
||||
}
|
||||
}
|
||||
|
||||
function to_storage(n) {
|
||||
let f2 = require("Storage").open(n+".st", "w");
|
||||
let pos = 0;
|
||||
let size = 1024;
|
||||
while (1) {
|
||||
let d = require("Storage").read(n, pos, size);
|
||||
if (!d)
|
||||
break;
|
||||
f2.write(d);
|
||||
pos += size;
|
||||
print("Copy ", pos);
|
||||
}
|
||||
}
|
||||
|
||||
ui.init();
|
||||
fmt.init();
|
||||
egt.init();
|
||||
gps.init();
|
||||
gps.start_gps();
|
||||
|
||||
const st = require('Storage');
|
||||
|
||||
let l = /^t\..*\.egt$/;
|
||||
l = st.list(l, {sf:false});
|
||||
|
||||
print(l);
|
||||
|
||||
function load_track(x) {
|
||||
print("Loading", x);
|
||||
Bangle.buzz(50, 1); // Won't happen because load() is quicker
|
||||
g.reset().clear()
|
||||
.setFont("Vector", 40)
|
||||
.drawString("Loading", 0, 30)
|
||||
.drawString(x, 0, 80);
|
||||
g.flip();
|
||||
track_name = x;
|
||||
time_read(x);
|
||||
|
||||
Bangle.setUI("clockupdown", btn => {
|
||||
print("Button", btn);
|
||||
if (btn == -1) {
|
||||
recover();
|
||||
}
|
||||
if (btn == 1) {
|
||||
demo_mode = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var menu = {
|
||||
"< Back" : Bangle.load
|
||||
};
|
||||
if (l.length==0) menu["No tracks"] = ()=>{};
|
||||
else for (let id in l) {
|
||||
let i = id;
|
||||
menu[l[id]]=()=>{ load_track(l[i]); };
|
||||
}
|
||||
|
||||
g.clear();
|
||||
E.showMenu(menu);
|
||||
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
# TV Remote
|
||||
A [BangleJS 2](https://shop.espruino.com/banglejs2) app that allows the user to send TV input signals from their watch to their TV.
|
||||
Currenly there is only support for Panasonic viera TV's however support for other brands may be considered in interest is there.
|
||||
|
||||
# Requirements
|
||||
1. The [Bangle GadgetBridge App](https://www.espruino.com/Gadgetbridge) with permissions allowed for `http requests`.
|
||||
2. A domain name and DNS created.
|
||||
3. A webserver that the DNS points to, that is set up to receive and process the watch http requests. [Here](https://github.com/Guptilious/banglejs-tvremote-webserver) is one I have created that should complete the full set up for users - provided they have their own domain name and DNS created.
|
||||
|
||||
# Set Up
|
||||
You will need to upload the below JSON file to your BangleJS, which will be used for config settings. At minimum you must provide:
|
||||
* `webServerDNS` address, which points to your webserver.
|
||||
* `username` which should mirror what is included in your webservers auth config. If using my webserver it would be `config.json`.
|
||||
* `password` which should mirror what is included in your webservers auth config. If using my webserver it would be `config.json`.
|
||||
|
||||
`port` and `tvIp` are optional as they can be manually assigned and updated via the tvremote watch app settings.
|
||||
|
||||
## Tv remote config example
|
||||
require("Storage").write("tvremote.settings.json", {
|
||||
"webServerDns": "",
|
||||
"tvIp": "",
|
||||
"port": "",
|
||||
"username": "",
|
||||
"password": ""
|
||||
});
|
||||
|
||||
# Usage
|
||||
Main Menu
|
||||
* Select TV type (panasonic is currently the only one supported)
|
||||
* Settings takes you to the settings menu, that allows you to manually assign ports and IP's.
|
||||
|
||||
Settings Menu
|
||||
* Device Select sends a http request to the webserver for a scrollable list devices to select.
|
||||
* Manual IP takes standard number inputs and swipping up will provide a `.` for IP's.
|
||||
|
||||
Power Screen
|
||||
* Press button - on/off.
|
||||
* Swipe left - `App Menu`.
|
||||
* Swipe Right - Main Menu
|
||||
|
||||
App Menu
|
||||
* Scroll and select to send App menu input.
|
||||
* Swipe left - Selection menu.
|
||||
* Swipe right - Power Screen.
|
||||
|
||||
Selection Menu
|
||||
* ^ - up
|
||||
* ! - down
|
||||
* < - left
|
||||
* `>` - right
|
||||
* Swipe right - back
|
||||
* Swipe left - select
|
||||
* Swipe Down - Number Menu ( used for inputting key passwords).
|
||||
* Swipe Up - Vol Commands
|
||||
|
||||
Vol Commands
|
||||
* Swipe Down - Selection Menu
|
||||
* Swipe right - rewind
|
||||
* Swipe left - fast forward
|
||||
* Swipe Up - Play/Pause
|
||||
|
||||
Back Button - Should take you back to the previous menu screen.
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4kA///7fOlMytX5muVt9T5lK6eMy2l1H4nO+i3d19jq2VxdCltTMf4A/AFsFqAYWqoXXGCxIEJqZIDGiYXCgpkTIYRjUFgIuUJAQuPgMRACEQC+0aw98+3ul3m/l3xQXMjVSkQAH/QUB2c7C48fCxEil4XBnOTC4+iC5Mi0IXKoQXKoJHKuQXKuJ3K4QXK4IXKsQXKsIXK8QXK8IXXjvd7vRC9Z3iU5hHKa5gXKoQXKoJHK0QXK0IXKj4WJksRI5UaqQXI5QXLDAOHvn290u838u+KIoZHIACAX0AH4A/ABo="))
|
||||
|
|
@ -0,0 +1,655 @@
|
|||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
|
||||
let deviceFileName = "tvdevicelist.json";
|
||||
let serverDataFile = "tvremote.settings.json";
|
||||
let serverData = require("Storage").readJSON(serverDataFile, true);
|
||||
let devicefile = require("Storage").readJSON(deviceFileName, true);
|
||||
|
||||
//console.log(require("Storage").list());
|
||||
//console.log(devicefile);
|
||||
|
||||
|
||||
let serverDns = "webServerDns" in serverData ? serverData.webServerDns : 'undefined';
|
||||
|
||||
let serverPort = "port" in serverData ? serverData.port : 'undefined';
|
||||
let tvIp = "tvIp" in serverData ? serverData.tvIp : 'undefined';
|
||||
let username = "username" in serverData ? serverData.username : 'undefined';
|
||||
let password = "password" in serverData ? serverData.password : 'undefined';
|
||||
|
||||
let panaIp = tvIp;
|
||||
//"tvIp" in serverData ? serverData.tvIp : 'undefined';
|
||||
let settingsPort = "port" in serverData ? serverData.port : 'undefined';
|
||||
|
||||
let counter;
|
||||
let IPASSIGN;
|
||||
let samsIp;
|
||||
let countdownTimer = null;
|
||||
let currNumber = null;
|
||||
let selected = "NONE";
|
||||
let currentScreen = "power";
|
||||
let font = 'Vector';
|
||||
let RESULT_HEIGHT = 24;
|
||||
let RIGHT_MARGIN = 15;
|
||||
let midpoint = (g.getWidth() / 2);
|
||||
let IP_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()]; // values used for key buttons
|
||||
let KEY_AREA = [0, 24, g.getWidth(), g.getHeight()];
|
||||
let COLORS = {
|
||||
DEFAULT: ['#FF0000'],
|
||||
BLACK: ['#000000'],
|
||||
WHITE: ['#FFFFFF'],
|
||||
GREY: ['#808080', '#222222']
|
||||
}; // background
|
||||
|
||||
let sourceApps = {
|
||||
"selection": {
|
||||
'!': {
|
||||
grid: [0, 1],
|
||||
globalGrid: [0, 0],
|
||||
key: 'down'
|
||||
},
|
||||
'^': {
|
||||
grid: [0, 0],
|
||||
globalGrid: [0, 0],
|
||||
key: 'up'
|
||||
},
|
||||
'<': {
|
||||
grid: [1, 0],
|
||||
globalGrid: [1, 0],
|
||||
key: 'left'
|
||||
},
|
||||
'>': {
|
||||
grid: [1, 1],
|
||||
globalGrid: [1, 0],
|
||||
key: 'right'
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
'Vol Up': {
|
||||
grid: [0, 0],
|
||||
globalGrid: [0, 0],
|
||||
key: 'volume_up'
|
||||
},
|
||||
'Vol Dwn': {
|
||||
grid: [0, 1],
|
||||
globalGrid: [1, 0],
|
||||
key: 'volume_down'
|
||||
},
|
||||
'Mute': {
|
||||
grid: [1, 0],
|
||||
globalGrid: [2, 0],
|
||||
key: 'mute'
|
||||
},
|
||||
'Options': {
|
||||
grid: [1, 1],
|
||||
globalGrid: [2, 0],
|
||||
key: 'option'
|
||||
}
|
||||
},
|
||||
"numbers": {
|
||||
'<': {
|
||||
grid: [0, 3],
|
||||
globalGrid: [1, 4]
|
||||
},
|
||||
'0': {
|
||||
grid: [1, 3],
|
||||
globalGrid: [1, 4]
|
||||
},
|
||||
'ok': {
|
||||
grid: [2, 3],
|
||||
globalGrid: [2, 4]
|
||||
},
|
||||
'1': {
|
||||
grid: [0, 2],
|
||||
globalGrid: [0, 3]
|
||||
},
|
||||
'2': {
|
||||
grid: [1, 2],
|
||||
globalGrid: [1, 3]
|
||||
},
|
||||
'3': {
|
||||
grid: [2, 2],
|
||||
globalGrid: [2, 3]
|
||||
},
|
||||
'4': {
|
||||
grid: [0, 1],
|
||||
globalGrid: [0, 2]
|
||||
},
|
||||
'5': {
|
||||
grid: [1, 1],
|
||||
globalGrid: [1, 2]
|
||||
},
|
||||
'6': {
|
||||
grid: [2, 1],
|
||||
globalGrid: [2, 2]
|
||||
},
|
||||
'7': {
|
||||
grid: [0, 0],
|
||||
globalGrid: [0, 1]
|
||||
},
|
||||
'8': {
|
||||
grid: [1, 0],
|
||||
globalGrid: [1, 1]
|
||||
},
|
||||
'9': {
|
||||
grid: [2, 0],
|
||||
globalGrid: [2, 1]
|
||||
}
|
||||
},
|
||||
"apps": [{
|
||||
"name": "Disney +",
|
||||
"key": "disney"
|
||||
},
|
||||
{
|
||||
"name": "Netflix",
|
||||
"key": "netflix"
|
||||
},
|
||||
{
|
||||
"name": "Amazon Prime",
|
||||
"key": "prime"
|
||||
},
|
||||
{
|
||||
"name": "Youtube",
|
||||
"key": "youtube"
|
||||
},
|
||||
{
|
||||
"name": "Home",
|
||||
"key": "home"
|
||||
},
|
||||
{
|
||||
"name": "TV",
|
||||
"key": "tv"
|
||||
},
|
||||
{
|
||||
"name": "HDMI1",
|
||||
"key": "hdmi1"
|
||||
},
|
||||
{
|
||||
"name": "HDMI2",
|
||||
"key": "hdmi2"
|
||||
},
|
||||
{
|
||||
"name": "HDMI3",
|
||||
"key": "hdmi3"
|
||||
},
|
||||
{
|
||||
"name": "HDMI4",
|
||||
"key": "hdmi4"
|
||||
}
|
||||
]
|
||||
};
|
||||
let numbersGrid = [3, 4];
|
||||
let selectionGrid = [2, 2];
|
||||
let volumeGrid = [2, 2];
|
||||
let appData = sourceApps.apps;
|
||||
let volume = sourceApps.volume;
|
||||
let selection = sourceApps.selection;
|
||||
let numbers = sourceApps.numbers;
|
||||
|
||||
function assignScreen(screen) {
|
||||
currentScreen = screen;
|
||||
console.log(currentScreen);
|
||||
}
|
||||
|
||||
function sendPost(keyPress) {
|
||||
serverPort = settingsPort;
|
||||
tvIp = panaIp;
|
||||
let credentials = btoa(`${username}:${password}`);
|
||||
let serverUrl = `https://${serverDns}:${serverPort}`;
|
||||
|
||||
let keyJson = {
|
||||
"command": keyPress,
|
||||
"tvip": tvIp,
|
||||
};
|
||||
|
||||
Bangle.http(
|
||||
serverUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Basic ${credentials}`,
|
||||
},
|
||||
body: JSON.stringify(keyJson)
|
||||
})
|
||||
.then(response => {
|
||||
console.log("Response received:", response);
|
||||
}).catch(error => {
|
||||
console.error("Error sending data:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function receiveDevices() {
|
||||
let serverPort = settingsPort;
|
||||
let credentials = btoa(`${username}:${password}`);
|
||||
let serverUrl = `https://${serverDns}:${serverPort}/ssdp-devices.json`;
|
||||
return Bangle.http(
|
||||
serverUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Basic ${credentials}`
|
||||
},
|
||||
}).then(data => {
|
||||
require("Storage").write(deviceFileName, data);
|
||||
devicefile = require("Storage").readJSON(deviceFileName, true);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareScreen(screen, grid, defaultColor, area) { // grid, [3, 4], colour, grid area size
|
||||
for (let k in screen) {
|
||||
if (screen.hasOwnProperty(k)) {
|
||||
screen[k].color = screen[k].color || defaultColor;
|
||||
let position = [];
|
||||
let xGrid = (area[2] - area[0]) / grid[0];
|
||||
let yGrid = (area[3] - area[1]) / grid[1];
|
||||
|
||||
//console.log(xGrid + " " + yGrid);
|
||||
position[0] = area[0] + xGrid * screen[k].grid[0];
|
||||
position[1] = area[1] + yGrid * screen[k].grid[1];
|
||||
|
||||
position[2] = position[0] + xGrid - 1;
|
||||
position[3] = position[1] + yGrid - 1;
|
||||
|
||||
screen[k].xy = position;
|
||||
//console.log("prepared " + screen+"\n");
|
||||
}
|
||||
}
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: function() {
|
||||
appMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawKey(name, k, selected) { // number, number data, NONE
|
||||
g.setColor(COLORS.DEFAULT[0]); // set color for rectangles
|
||||
g.setFont('Vector', 20).setFontAlign(0, 0);
|
||||
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); // create rectangles based on letters xy areas
|
||||
|
||||
g.setColor(COLORS.BLACK[0]).drawRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
|
||||
|
||||
g.setColor(COLORS.WHITE[0]); // color for numbers
|
||||
g.drawString(name, (k.xy[0] + k.xy[2]) / 2, (k.xy[1] + k.xy[3]) / 2); // center the keys to the rectangle that is drawn
|
||||
}
|
||||
|
||||
function drawKeys(area, buttons) {
|
||||
g.setColor(COLORS.DEFAULT[0]); // background colour
|
||||
g.fillRect(area[0], area[1], area[2], area[3]); // number grid area
|
||||
for (let k in buttons) {
|
||||
if (buttons.hasOwnProperty(k)) {
|
||||
drawKey(k, buttons[k], k == selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function displayOutput(num, screenValue) { // top block
|
||||
num = num.toString();
|
||||
g.setFont('Vector', 18); //'7x11Numeric7Seg'
|
||||
g.setFontAlign(1, 0);
|
||||
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT - 1);
|
||||
g.setColor(-1); // value
|
||||
|
||||
g.drawString(num, g.getWidth() - RIGHT_MARGIN, RESULT_HEIGHT / 2);
|
||||
}
|
||||
|
||||
function buttonPress(val, screenValue) {
|
||||
|
||||
if (screenValue === "ip") {
|
||||
if (val === "<") currNumber = currNumber.slice(0, -1);
|
||||
else if (val === ".") currNumber = currNumber + ".";
|
||||
else currNumber = currNumber == null ? val : currNumber + val; // currNumber is null if no value pressed
|
||||
|
||||
let ipcount = (currNumber.match(/\./g) || []).length;
|
||||
if (ipcount > 3 || currNumber.length > 15) currNumber = currNumber.slice(0, -1);
|
||||
|
||||
displayOutput(currNumber, screenValue);
|
||||
}
|
||||
|
||||
let checkValue = appData.some(app => app.name === screenValue); // check app data
|
||||
|
||||
if (checkValue) sendPost(val); // app values
|
||||
|
||||
if (screenValue === "numbers") {
|
||||
if (val === '<') sendPost('back');
|
||||
else if (val === 'ok') sendPost('enter');
|
||||
else sendPost("num_" + val);
|
||||
} else if (screenValue === "selection") sendPost(selection[val].key);
|
||||
else if (screenValue === "volume") sendPost(volume[val].key);
|
||||
}
|
||||
|
||||
const powerScreen = () => {
|
||||
currentScreen = "power";
|
||||
g.setColor(COLORS.GREY[0]).fillRect(0, 24, g.getWidth(), g.getWidth()); // outer circ
|
||||
g.setColor(COLORS.WHITE[0]).fillCircle(midpoint, 76 + 24, 50); // inner circ
|
||||
g.setColor(COLORS.BLACK[0]).setFont('Vector', 25).setFontAlign(0, 0).drawString("On/Off", 88, 76 + 24); // circ text
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: function() {
|
||||
mainMenu();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const appMenu = () => {
|
||||
|
||||
assignScreen("apps");
|
||||
E.showScroller({
|
||||
h: 54,
|
||||
c: appData.length,
|
||||
draw: (i, r) => {
|
||||
let sourceOption = appData[i];
|
||||
g.setColor(COLORS.DEFAULT[0]).fillRect((r.x), (r.y), (r.x + r.w), (r.y + r.h));
|
||||
g.setColor(COLORS.BLACK[0]).drawRect((r.x), (r.y), (r.x + r.w), (r.y + r.h));
|
||||
g.setColor(COLORS.WHITE[0]).setFont(font, 20).setFontAlign(-1, 1).drawString(sourceOption.name, 15, r.y + 32);
|
||||
},
|
||||
|
||||
select: i => {
|
||||
let sourceOption = appData[i];
|
||||
let appPressed = sourceOption.name;
|
||||
let appKey = sourceOption.key;
|
||||
buttonPress(appKey, appPressed);
|
||||
},
|
||||
|
||||
back: main => {
|
||||
E.showScroller();
|
||||
powerScreen();
|
||||
},
|
||||
});
|
||||
g.flip(); // force a render before widgets have finished drawing
|
||||
};
|
||||
|
||||
function ipScreen() {
|
||||
//require("widget_utils").hide();
|
||||
assignScreen("ip");
|
||||
currNumber = "";
|
||||
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT, IP_AREA);
|
||||
drawKeys(IP_AREA, numbers);
|
||||
displayOutput(0);
|
||||
}
|
||||
|
||||
let tvSelector = {
|
||||
"": {
|
||||
title: "TV Selector",
|
||||
back: function() {
|
||||
load(); //E.showMenu(tvSelector);
|
||||
}
|
||||
},
|
||||
"Panasonic": function() {
|
||||
assignScreen("power");
|
||||
powerScreen();
|
||||
},
|
||||
"Samsung": function() {
|
||||
assignScreen("power");
|
||||
powerScreen();
|
||||
},
|
||||
"Settings": function() {
|
||||
subMenu();
|
||||
}
|
||||
};
|
||||
|
||||
function mainMenu() {
|
||||
assignScreen("mainmenu");
|
||||
E.showMenu(tvSelector);
|
||||
}
|
||||
|
||||
function clearCountdown() {
|
||||
if (countdownTimer) {
|
||||
clearTimeout(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function countDown(callback) {
|
||||
require("widget_utils").show();
|
||||
if (counter === 0) {
|
||||
callback(); // Call the callback function when countdown reaches 0
|
||||
return;
|
||||
}
|
||||
E.showMessage(`Searching for devices...\n${counter}`, "Device Search");
|
||||
counter--;
|
||||
countdownTimer = setTimeout(() => countDown(callback), 1000);
|
||||
}
|
||||
|
||||
function clearDisplayOutput() {
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
function subMenu() {
|
||||
|
||||
if (typeof IPASSIGN !== 'undefined' && currNumber !== "") {
|
||||
if (IPASSIGN === "pana") {
|
||||
console.log("current numeber = " + currNumber);
|
||||
panaIp = currNumber;
|
||||
console.log("pana ip " + panaIp);
|
||||
console.log("default ip " + serverData.tvIp);
|
||||
serverData.tvIp = panaIp;
|
||||
require("Storage").write(serverDataFile, serverData);
|
||||
console.log("tv ip is now " + serverData.tvIp);
|
||||
|
||||
} else if (IPASSIGN === "sams") {
|
||||
samsIp = currNumber;
|
||||
|
||||
} else if (IPASSIGN === "port") {
|
||||
settingsPort = currNumber;
|
||||
console.log("setting port " + settingsPort);
|
||||
console.log("server port " + serverData.port);
|
||||
serverData.port = settingsPort;
|
||||
require("Storage").write(serverDataFile, serverData);
|
||||
console.log("port is now " + serverData.port);
|
||||
}
|
||||
}
|
||||
|
||||
require("widget_utils").show();
|
||||
assignScreen("settingssub");
|
||||
clearDisplayOutput();
|
||||
|
||||
let settingssub = {
|
||||
"": {
|
||||
title: "Settings",
|
||||
back: function() {
|
||||
E.showMenu(tvSelector);
|
||||
clearCountdown();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof settingsPort !== 'undefined' && settingsPort !== 'undefined') {
|
||||
let portHeader = `Port: ${settingsPort}`;
|
||||
settingssub[portHeader] = function() {
|
||||
IPASSIGN = "port";
|
||||
ipScreen();
|
||||
};
|
||||
} else {
|
||||
settingssub["Set DNS Port"] = function() {
|
||||
IPASSIGN = "port";
|
||||
ipScreen();
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof panaIp !== 'undefined' && panaIp !== 'undefined') {
|
||||
let panaheader = `Pana IP: ${panaIp}`;
|
||||
settingssub[panaheader] = function() {
|
||||
IPASSIGN = "pana";
|
||||
E.showMenu(deviceSelect);
|
||||
devicefile = require("Storage").readJSON("tvdevicelist.json", true);
|
||||
console.log(devicefile);
|
||||
};
|
||||
} else {
|
||||
settingssub["Set Pana IP"] = function() {
|
||||
IPASSIGN = "pana";
|
||||
ipScreen();
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof samsIp !== 'undefined' && panaIp !== 'undefined') {
|
||||
let samsheader = `Sams IP: ${samsIp}`;
|
||||
settingssub[samsheader] = function() {
|
||||
IPASSIGN = "sams";
|
||||
ipScreen();
|
||||
};
|
||||
} else {
|
||||
settingssub["Set Sams IP"] = function() {
|
||||
IPASSIGN = "sams";
|
||||
ipScreen();
|
||||
};
|
||||
}
|
||||
|
||||
E.showMenu(settingssub);
|
||||
}
|
||||
|
||||
const deviceMenu = () => {
|
||||
let parsedResp = JSON.parse(devicefile.resp);
|
||||
E.showScroller({
|
||||
h: 54,
|
||||
c: parsedResp.length,
|
||||
draw: (i, r) => {
|
||||
let sourceOption = parsedResp[i];
|
||||
g.setColor(COLORS.DEFAULT[0]).fillRect((r.x), (r.y), (r.x + r.w), (r.y + r.h));
|
||||
g.setColor(COLORS.BLACK[0]).drawRect((r.x), (r.y), (r.x + r.w), (r.y + r.h));
|
||||
g.setColor(COLORS.WHITE[0]).setFont(font, 15).setFontAlign(-1, 1).drawString(sourceOption.name, 15, r.y + 32);
|
||||
},
|
||||
|
||||
select: i => {
|
||||
let sourceOption = parsedResp[i];
|
||||
//let devicePressed = sourceOption.name;
|
||||
let deviceIp = sourceOption.ip;
|
||||
|
||||
assignScreen("deviceSearch");
|
||||
serverData.tvIp = deviceIp.replace('http://', '');
|
||||
currNumber = serverData.tvIp;
|
||||
require("Storage").write(serverDataFile, serverData);
|
||||
console.log("tv ip is now " + serverData.tvIp);
|
||||
subMenu();
|
||||
},
|
||||
|
||||
back: main => {
|
||||
E.showScroller();
|
||||
E.showMenu(deviceSelect);
|
||||
},
|
||||
});
|
||||
g.flip(); // force a render before widgets have finished drawing
|
||||
};
|
||||
|
||||
|
||||
let deviceSelect = {
|
||||
"": {
|
||||
title: "Device Select",
|
||||
back: function() {
|
||||
subMenu();
|
||||
}
|
||||
},
|
||||
"Manual IP Assign": function() {
|
||||
ipScreen();
|
||||
},
|
||||
"Device Select": function() {
|
||||
receiveDevices();
|
||||
counter = 5;
|
||||
countDown(deviceMenu);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function swipeHandler(LR, UD) {
|
||||
if (LR == -1) { // swipe left
|
||||
if (currentScreen === "power") {
|
||||
assignScreen("apps");
|
||||
appMenu();
|
||||
|
||||
} else if (currentScreen === "apps") {
|
||||
//require("widget_utils").hide();
|
||||
assignScreen("selection");
|
||||
E.showScroller();
|
||||
prepareScreen(selection, selectionGrid, COLORS.DEFAULT, KEY_AREA);
|
||||
drawKeys(KEY_AREA, selection);
|
||||
|
||||
} else if (currentScreen === "volume") {
|
||||
sendPost("fast_forward");
|
||||
|
||||
} else if (currentScreen === "selection") {
|
||||
sendPost("enter");
|
||||
}
|
||||
}
|
||||
if (LR == 1) { // swipe right
|
||||
if (currentScreen === "apps") {
|
||||
assignScreen("power");
|
||||
E.showScroller();
|
||||
powerScreen();
|
||||
} else if (currentScreen === "volume") {
|
||||
sendPost("rewind");
|
||||
} else if (currentScreen === "selection") {
|
||||
sendPost("back");
|
||||
}
|
||||
}
|
||||
if (UD == -1) { // swipe up
|
||||
if (currentScreen === "selection") {
|
||||
assignScreen("volume");
|
||||
prepareScreen(volume, volumeGrid, COLORS.DEFAULT, KEY_AREA);
|
||||
drawKeys(KEY_AREA, volume);
|
||||
} else if (currentScreen === "volume") {
|
||||
sendPost("enter");
|
||||
} else if (currentScreen === "ip") {
|
||||
buttonPress(".", "ip");
|
||||
} else if (currentScreen == "numbers") {
|
||||
assignScreen("selection");
|
||||
prepareScreen(selection, selectionGrid, COLORS.DEFAULT, KEY_AREA);
|
||||
drawKeys(KEY_AREA, selection);
|
||||
}
|
||||
}
|
||||
if (UD == 1) { // swipe down
|
||||
if (currentScreen === "volume") {
|
||||
assignScreen("selection");
|
||||
prepareScreen(selection, selectionGrid, COLORS.DEFAULT, KEY_AREA);
|
||||
drawKeys(KEY_AREA, selection);
|
||||
} else if (currentScreen === "selection") {
|
||||
assignScreen("numbers");
|
||||
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT, KEY_AREA);
|
||||
drawKeys(KEY_AREA, numbers);
|
||||
}
|
||||
}
|
||||
}
|
||||
Bangle.on('swipe', swipeHandler);
|
||||
|
||||
|
||||
function touchHandler(button, e) {
|
||||
const screenActions = {
|
||||
ip: () => checkButtons(numbers),
|
||||
volume: () => checkButtons(volume),
|
||||
numbers: () => checkButtons(numbers),
|
||||
selection: () => checkButtons(selection),
|
||||
power: () => {
|
||||
if (Math.pow(e.x - 88, 2) + Math.pow(e.y - 88, 2) < 2500) {
|
||||
sendPost("power");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function checkButtons(buttonMap) {
|
||||
for (let key in buttonMap) {
|
||||
if (typeof buttonMap[key] === "undefined") continue;
|
||||
let r = buttonMap[key].xy;
|
||||
|
||||
if (e.x >= r[0] && e.y >= r[1] && e.x < r[2] && e.y < r[3]) {
|
||||
if (currentScreen === "ip" && key === "ok") {
|
||||
subMenu();
|
||||
} else {
|
||||
buttonPress("" + key, currentScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentScreen in screenActions) screenActions[currentScreen]();
|
||||
}
|
||||
Bangle.on('touch', touchHandler);
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
if (serverData === undefined) {
|
||||
E.showAlert(`No settings.\nSee READ.me`, "Config Error").then(function() {
|
||||
mainMenu();
|
||||
});
|
||||
} else {
|
||||
mainMenu();
|
||||
}
|
||||
|
After Width: | Height: | Size: 607 B |
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "tvremote",
|
||||
"name": "TV Remote",
|
||||
"shortName":"TV Remote",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "remote for controlling your tv, using a webserver and the bangle Gadget Bridge (https://www.espruino.com/Gadgetbridge).",
|
||||
"icon": "app.png",
|
||||
"tags": "remote",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"tvremote.app.js","url":"app.js"},
|
||||
{"name":"tvremote.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
@ -53,7 +53,12 @@ function bangleDownload() {
|
|||
}).then(content => {
|
||||
Progress.hide({ sticky: true });
|
||||
showToast('Backup complete!', 'success');
|
||||
Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip");
|
||||
if (typeof Android !== "undefined" && typeof Android.saveFile === 'function') {
|
||||
// Recent Gadgetbridge version that provides the saveFile interface
|
||||
Android.saveFile("Banglejs backup.zip", "application/zip", btoa(content));
|
||||
} else {
|
||||
Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip");
|
||||
}
|
||||
}).catch(err => {
|
||||
Progress.hide({ sticky: true });
|
||||
showToast('Backup failed, ' + err, 'error');
|
||||
|
|
|
|||
|
|
@ -81,7 +81,10 @@ a.btn.btn-link.dropdown-toggle {
|
|||
min-height: 8em;
|
||||
}
|
||||
|
||||
.tile-content { position: relative; }
|
||||
.tile-content {
|
||||
position: relative;
|
||||
overflow-wrap: anywhere; /* stop long text like links pushing the width out too far*/
|
||||
}
|
||||
.link-github {
|
||||
position:absolute;
|
||||
top: 36px;
|
||||
|
|
@ -137,4 +140,4 @@ Not sure how to get 'normal' wrap behaviour (eg fill up until max-width, then wr
|
|||
/*.tooltip:hover::after {
|
||||
white-space: normal;
|
||||
min-width: 160px;
|
||||
}*/
|
||||
}*/
|
||||
|
|
|
|||
|
|
@ -10,27 +10,37 @@ exports.doublePicker = function (options) {
|
|||
var v_1 = options.value_1;
|
||||
var v_2 = options.value_2;
|
||||
|
||||
function draw() {
|
||||
function draw1() {
|
||||
var txt_1 = options.format_1 ? options.format_1(v_1) : v_1;
|
||||
|
||||
g.setColor(g.theme.bg2)
|
||||
.fillRect(14, 60, 81, 166)
|
||||
.fillRect(95, 60, 162, 166);
|
||||
|
||||
g.setColor(g.theme.fg2)
|
||||
.setColor(g.theme.fg2)
|
||||
.fillPoly([47.5, 68, 62.5, 83, 32.5, 83])
|
||||
.fillPoly([47.5, 158, 62.5, 143, 32.5, 143])
|
||||
.fillPoly([128.5, 68, 143.5, 83, 113.5, 83])
|
||||
.fillPoly([128.5, 158, 143.5, 143, 113.5, 143]);
|
||||
|
||||
var txt_1 = options.format_1 ? options.format_1(v_1) : v_1;
|
||||
.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_1)))
|
||||
.drawString(txt_1, 47.5, 113);
|
||||
}
|
||||
function draw2() {
|
||||
var txt_2 = options.format_2 ? options.format_2(v_2) : v_2;
|
||||
|
||||
g.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_1)))
|
||||
.drawString(txt_1, 47.5, 113)
|
||||
g.setColor(g.theme.bg2)
|
||||
.fillRect(95, 60, 162, 166)
|
||||
.setColor(g.theme.fg2)
|
||||
.fillPoly([128.5, 68, 143.5, 83, 113.5, 83])
|
||||
.fillPoly([128.5, 158, 143.5, 143, 113.5, 143])
|
||||
.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_2)))
|
||||
.drawString(txt_2, 128.5, 113)
|
||||
.setFontVector(30)
|
||||
.drawString(options.separator ?? "", 88, 110);
|
||||
.drawString(txt_2, 128.5, 113);
|
||||
}
|
||||
function drawSeparator(){
|
||||
g.setFontVector(30).drawString(options.separator ?? "", 88, 110);
|
||||
}
|
||||
function drawAll() {
|
||||
draw1();
|
||||
draw2();
|
||||
drawSeparator()
|
||||
}
|
||||
function cb(dir, x_part) {
|
||||
if (dir) {
|
||||
|
|
@ -38,12 +48,14 @@ exports.doublePicker = function (options) {
|
|||
v_1 -= (dir || 1) * (options.step_1 || 1);
|
||||
if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1;
|
||||
if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1;
|
||||
draw1();
|
||||
} else {
|
||||
v_2 -= (dir || 1) * (options.step_2 || 1);
|
||||
if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_2;
|
||||
if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_2;
|
||||
draw2();
|
||||
}
|
||||
draw();
|
||||
drawSeparator();
|
||||
} else { // actually selected
|
||||
options.value_1 = v_1;
|
||||
options.value_2 = v_2;
|
||||
|
|
@ -52,14 +64,14 @@ exports.doublePicker = function (options) {
|
|||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
drawAll();
|
||||
var dy = 0;
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: options.back,
|
||||
remove: options.remove,
|
||||
redraw: draw,
|
||||
redraw: drawAll,
|
||||
drag: e => {
|
||||
dy += e.dy; // after a certain amount of dragging up/down fire cb
|
||||
if (!e.b) dy = 0;
|
||||
|
|
@ -101,51 +113,72 @@ exports.triplePicker = function (options) {
|
|||
var v_2 = options.value_2;
|
||||
var v_3 = options.value_3;
|
||||
|
||||
function draw() {
|
||||
function draw1() {
|
||||
var txt_1 = options.format_1 ? options.format_1(v_1) : v_1;
|
||||
|
||||
g.setColor(g.theme.bg2)
|
||||
.fillRect(8, 60, 56, 166)
|
||||
.fillRect(64, 60, 112, 166)
|
||||
.fillRect(120, 60, 168, 166);
|
||||
|
||||
g.setColor(g.theme.fg2)
|
||||
.setColor(g.theme.fg2)
|
||||
.fillPoly([32, 68, 47, 83, 17, 83])
|
||||
.fillPoly([32, 158, 47, 143, 17, 143])
|
||||
.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_1)))
|
||||
.drawString(txt_1, 32, 113);
|
||||
}
|
||||
function draw2() {
|
||||
var txt_2 = options.format_2 ? options.format_2(v_2) : v_2;
|
||||
|
||||
g.setColor(g.theme.bg2)
|
||||
.fillRect(64, 60, 112, 166)
|
||||
.setColor(g.theme.fg2)
|
||||
.fillPoly([88, 68, 103, 83, 73, 83])
|
||||
.fillPoly([88, 158, 103, 143, 73, 143])
|
||||
.fillPoly([144, 68, 159, 83, 129, 83])
|
||||
.fillPoly([144, 158, 159, 143, 129, 143]);
|
||||
|
||||
var txt_1 = options.format_1 ? options.format_1(v_1) : v_1;
|
||||
var txt_2 = options.format_2 ? options.format_2(v_2) : v_2;
|
||||
var txt_3 = options.format_3 ? options.format_3(v_3) : v_3;
|
||||
|
||||
g.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_1)))
|
||||
.drawString(txt_1, 32, 113)
|
||||
.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_2)))
|
||||
.drawString(txt_2, 88, 113)
|
||||
.drawString(txt_2, 88, 113);
|
||||
}
|
||||
function draw3() {
|
||||
var txt_3 = options.format_3 ? options.format_3(v_3) : v_3;
|
||||
|
||||
g.setColor(g.theme.bg2)
|
||||
.fillRect(120, 60, 168, 166)
|
||||
.setColor(g.theme.fg2)
|
||||
.fillPoly([144, 68, 159, 83, 129, 83])
|
||||
.fillPoly([144, 158, 159, 143, 129, 143])
|
||||
.setFontAlign(0, 0)
|
||||
.setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_3)))
|
||||
.drawString(txt_3, 144, 113)
|
||||
.setFontVector(30)
|
||||
.drawString(txt_3, 144, 113);
|
||||
}
|
||||
function drawSeparators(){
|
||||
g.setFontVector(30)
|
||||
.drawString(options.separator_1 ?? "", 60, 113)
|
||||
.drawString(options.separator_2 ?? "", 116, 113);
|
||||
}
|
||||
function drawAll() {
|
||||
draw1();
|
||||
draw2();
|
||||
draw3();
|
||||
drawSeparators();
|
||||
}
|
||||
function cb(dir, x_part) {
|
||||
if (dir) {
|
||||
if (x_part == -1) {
|
||||
v_1 -= (dir || 1) * (options.step_1 || 1);
|
||||
if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1;
|
||||
if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1;
|
||||
draw1();
|
||||
} else if (x_part == 0) {
|
||||
v_2 -= (dir || 1) * (options.step_2 || 1);
|
||||
if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_3;
|
||||
if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_3;
|
||||
draw2();
|
||||
} else {
|
||||
v_3 -= (dir || 1) * (options.step_3 || 1);
|
||||
if (options.min_3 !== undefined && v_3 < options.min_3) v_3 = options.wrap_3 ? options.max_3 : options.min_3;
|
||||
if (options.max_3 !== undefined && v_3 > options.max_3) v_3 = options.wrap_3 ? options.min_3 : options.max_3;
|
||||
draw3();
|
||||
}
|
||||
draw();
|
||||
drawSeparators();
|
||||
} else { // actually selected
|
||||
options.value_1 = v_1;
|
||||
options.value_2 = v_2;
|
||||
|
|
@ -155,14 +188,14 @@ exports.triplePicker = function (options) {
|
|||
}
|
||||
}
|
||||
|
||||
draw();
|
||||
drawAll();
|
||||
var dy = 0;
|
||||
|
||||
Bangle.setUI({
|
||||
mode: "custom",
|
||||
back: options.back,
|
||||
remove: options.remove,
|
||||
redraw: draw,
|
||||
redraw: drawAll,
|
||||
drag: e => {
|
||||
dy += e.dy; // after a certain amount of dragging up/down fire cb
|
||||
if (!e.b) dy = 0;
|
||||
|
|
|
|||
2
webtools
|
|
@ -1 +1 @@
|
|||
Subproject commit 71f271a1c7be37efe4e472b7482b08ded1d0ab6f
|
||||
Subproject commit c59402259c779b578e68995ea0237b813fab09c0
|
||||