Merge branch 'espruino:master' into master

master
aemkai 2024-09-12 16:47:17 +02:00 committed by GitHub
commit 3b37203584
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 3436 additions and 775 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "%"
}

Binary file not shown.

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
0.20: First release

22
apps/dutchclock/README.md Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgP/AE0/Ao/4sccAoX79NtAofttIFD8dsAof3t1/GZ397oGE/YLE6IFDloFE1vbAoeNAondAon/z4FE356U/nNxhZC/drlpLDscNAoX4ue9C4f3L4oAKt4FEQ4qxE/0skIGDtg7DAoNtAocsAogAX94POA"))

260
apps/dutchclock/app.js Normal file
View File

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

BIN
apps/dutchclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,6 @@
{
"variant": "approximate",
"showWidgets": true,
"showTime": false,
"showDate": true
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
apps/gbdiscon/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwYHEgMkyVAkmQDJYREyQRRoARQpARQpIRRkARNggRBkgRNgARCwARNiQRBSRIREgQRBSRIREgARCSRARFhKSKCIoFCSRAjISQ0BAQJZHCI6ZBTwKPEI44tBTIMSYoZ9IBIYyEWZCHEKwbXIDwZ6MBghjBWBR7DIQbmJAAJ7BexYRHGZZHEchRrGNJYRIRpARJWI7XDCIrVHLIeACIpuIgKwBR4RcQyDLFCJbLGCJcAZZgLEiRcLCIkCZZYvFCKAjDI6BZOPqD+PWaUJa6ARCTxARICBQRFPRIRHPRIRHBg4A="))

7
apps/gbdiscon/app.js Normal file
View File

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

BIN
apps/gbdiscon/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"id": "locale",
"name": "Languages",
"version": "0.19",
"version": "0.20",
"description": "Translations for different countries",
"icon": "locale.png",
"type": "locale",

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhHXAB+sr1dnMzmcPh4DBnNdr2sDyAsOropCABczroyaxE5FhoAFnOIFqutFqgxE1ouSrwdHnWIxGz2YPBAYIHBnQTHrwuQLgwsBURwyGnIWN2bmFnR4S1oxFmZyCFx0zLY2r0md4AACzuk1ZjGDoowKCAk6JwuBFggAFzuBOApiEmYuIBwjSE2ddvOcFxIABzl5rpWErxQJN4QuIPIN5FpYADvKlFGAivGHZAuSGBCDEFwg6EOoZnBFyQwCK4mzQg4IECIYuBqzqKehVWDwxWFLwc5M4czLxPB5HP5/I4JgJmYfDnJgFEwJeFG4MyLw4tB6wPB6wxBMA8yRAhgDHAOtY44FBq2cFw3Qa4nX6AwGziQBEIwAB1o1DHoyOG4PPFwoAB56SGSAKBGA4SVDBgc6AwKOG45eGMAXHSAwbBnSQGnIvD1psFF43IXgQAF6yQGF4SQDXQczdwezF5pfJF5uzF4bED1gACF5KPSKoMzEY0PgAAJuQdFMAIvH5wQGuQkKABVOzhgG57BE63PLw2cpwvVkgvGGAPI5/Q6HP5AuGF4MkF6pgIAAPB4/H4ILHLxsrMBbBHABlyLxcrF5ZgKABJeOqyRMMCVyEBlWwLyNywuPyzsNwPXeS6NTAAPX67AMSKCNNXwIvBSBqRBGBlyRpqOCAAIRNSJiNPRwQABqwwPF5IuPqwvD1gUOSJKNPgGsF4bBPgEqSI2clQYOXoYADlbCUXiErFwyRQgCREuQVPRoqRTkmWFwOWXh6NHGCaRBRqAuLGCIAQFxowgFx70ClYtZlbqJMUZcRAA1WFqdWFq5jESp0rLbAyJq0rGggFBqwsSA=="))

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

14
apps/pomodo/README.md Normal file
View File

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

View File

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

View File

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

42
apps/pomoplus/README.md Normal file
View File

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

View File

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

View File

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

View File

@ -1 +1,2 @@
0.01: New Library!
0.01: New Library!
0.02: Minor bug fixes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
apps/trail/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!

18
apps/trail/README.md Normal file
View File

@ -0,0 +1,18 @@
# Trail Rail ![](app.png)
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.

1
apps/trail/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhHXAH4AHgAqpvownEQ4wEMEYw8YMgw4F84wwMH4woeQQvlEwwvCGFgvqE4gvDGFAvHAFQv/AH74YElYvjF3Je/FzV9F8wuLF8QhHL34u/RqSOjFxYvpF2gvlRQwuoAAIvqFoQvJFsoupF9wtFF+CNuX34xfF1YwDF9oA/AH4AyA=="))

BIN
apps/trail/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

2
apps/trail/gpx2egt.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
grep "trkpt lat" | sed 's/.*trkpt.lat=.//' | sed 's/. lon=./ /' | sed 's/".$//'

13
apps/trail/metadata.json Normal file
View File

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

651
apps/trail/trail.app.js Normal file
View File

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

62
apps/tvremote/README.md Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4kA///7fOlMytX5muVt9T5lK6eMy2l1H4nO+i3d19jq2VxdCltTMf4A/AFsFqAYWqoXXGCxIEJqZIDGiYXCgpkTIYRjUFgIuUJAQuPgMRACEQC+0aw98+3ul3m/l3xQXMjVSkQAH/QUB2c7C48fCxEil4XBnOTC4+iC5Mi0IXKoQXKoJHKuQXKuJ3K4QXK4IXKsQXKsIXK8QXK8IXXjvd7vRC9Z3iU5hHKa5gXKoQXKoJHK0QXK0IXKj4WJksRI5UaqQXI5QXLDAOHvn290u838u+KIoZHIACAX0AH4A/ABo="))

655
apps/tvremote/app.js Normal file
View File

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

BIN
apps/tvremote/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

View File

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

View File

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

View File

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

View File

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

@ -1 +1 @@
Subproject commit 71f271a1c7be37efe4e472b7482b08ded1d0ab6f
Subproject commit c59402259c779b578e68995ea0237b813fab09c0