forked from FOSS/BangleApps
New app: aviatorclk (and module avwx)
parent
367ad5d285
commit
a809b128bc
|
@ -0,0 +1 @@
|
|||
aviatorclk.json
|
|
@ -0,0 +1 @@
|
|||
1.00: initial release
|
|
@ -0,0 +1,36 @@
|
|||
# Aviator Clock
|
||||
|
||||
A clock for aviators, with local time and UTC - and the latest METAR
|
||||
(Meteorological Aerodrome Report) for the nearest airport
|
||||
|
||||

|
||||

|
||||
|
||||
This app depends on the [AVWX module](?id=avwx). Make sure to configure that
|
||||
module after installing this app.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Local time (with optional seconds)
|
||||
- UTC / Zulu time
|
||||
- Weekday and day of the month
|
||||
- Latest METAR for the nearest airport (scrollable)
|
||||
|
||||
Tap the screen in the top or bottom half to scroll the METAR text (in case not
|
||||
the whole report fits on the screen).
|
||||
|
||||
The colour of the METAR text will change to orange if the report is more than
|
||||
1h old, and red if it's older than 1.5h.
|
||||
|
||||
|
||||
## Settings
|
||||
|
||||
- **Show Seconds**: to conserve battery power, you can turn the seconds display off
|
||||
- **Invert Scrolling**: swaps the METAR scrolling direction of the top and bottom taps
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
Flaparoo [github](https://github.com/flaparoo)
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwg96iIACCqMBCwYABiAWQiUiAAUhDBwWGDCAWHDAYuMCw4ABGBYWKGBYuLGBcBLpAXNFxhIKFxgwCIyhIJC58hC44WNC5B2NPBIXbBYIAHNgIXKCpAYEC5AhBII8SDAQXJMI5EEC6ZREC6EhFwkRO4zuCC46AFAgLYEC4YCBIoaADF4gXEKgYXDVBAcCXxBZDkcyDRAXHmILCif//4GEC5f/PQQWB//zbAX/C5gAKC78BC6K/In4WJ+YXW+QXHMAURl4XJeQYWEGALhBC4q+BYYLbDFwowCkLTCRIyNHGArNBC48SFxIXCMApHDOwQXIJAIQCAAaWCDYJGIDAipGFwQWKDAUSDAnzUoIWMDAcjn/zUgQWOPYYADOZJjKFqIAp"))
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Aviator Clock - Bangle.js
|
||||
*
|
||||
*/
|
||||
|
||||
const COLOUR_DARK_GREY = 0x4208; // same as: g.setColor(0.25, 0.25, 0.25)
|
||||
const COLOUR_GREY = 0x8410; // same as: g.setColor(0.5, 0.5, 0.5)
|
||||
const COLOUR_LIGHT_GREY = 0xc618; // same as: g.setColor(0.75, 0.75, 0.75)
|
||||
const COLOUR_RED = 0xf800; // same as: g.setColor(1, 0, 0)
|
||||
const COLOUR_BLUE = 0x001f; // same as: g.setColor(0, 0, 1)
|
||||
const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0)
|
||||
const COLOUR_LIGHT_CYAN = 0x87ff; // same as: g.setColor(0.5, 1, 1)
|
||||
const COLOUR_DARK_YELLOW = 0x8400; // same as: g.setColor(0.5, 0.5, 0)
|
||||
const COLOUR_DARK_CYAN = 0x0410; // same as: g.setColor(0, 0.5, 0.5)
|
||||
const COLOUR_ORANGE = 0xfc00; // same as: g.setColor(1, 0.5, 0)
|
||||
|
||||
const APP_NAME = 'aviatorclk';
|
||||
|
||||
const horizontalCenter = g.getWidth()/2;
|
||||
const mainTimeHeight = 38;
|
||||
const secondaryFontHeight = 22;
|
||||
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 );
|
||||
|
||||
const avwx = require('avwx');
|
||||
|
||||
|
||||
// read in the settings
|
||||
var settings = Object.assign({
|
||||
showSeconds: true,
|
||||
invertScrolling: false,
|
||||
}, require('Storage').readJSON(APP_NAME+'.json', true) || {});
|
||||
|
||||
|
||||
// globals
|
||||
var drawTimeout;
|
||||
var secondsInterval;
|
||||
var avwxTimeout;
|
||||
|
||||
var AVWXrequest;
|
||||
var METAR = '';
|
||||
var METARlinesCount = 0;
|
||||
var METARscollLines = 0;
|
||||
var METARts;
|
||||
|
||||
|
||||
|
||||
// date object to time string in format HH:MM[:SS]
|
||||
// (with a leading 0 for hours if required, unlike the "locale" time() function)
|
||||
function timeStr(date, seconds) {
|
||||
let timeStr = date.getHours().toString();
|
||||
if (timeStr.length == 1) timeStr = '0' + timeStr;
|
||||
let minutes = date.getMinutes().toString();
|
||||
if (minutes.length == 1) minutes = '0' + minutes;
|
||||
timeStr += ':' + minutes;
|
||||
if (seconds) {
|
||||
let seconds = date.getSeconds().toString();
|
||||
if (seconds.length == 1) seconds = '0' + seconds;
|
||||
timeStr += ':' + seconds;
|
||||
}
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
|
||||
// draw the METAR info
|
||||
function drawAVWX() {
|
||||
let now = new Date();
|
||||
let METARage = 0; // in minutes
|
||||
if (METARts) {
|
||||
METARage = Math.floor((now - METARts) / 60000);
|
||||
}
|
||||
|
||||
g.setBgColor(g.theme.bg);
|
||||
|
||||
let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight + 4;
|
||||
g.clearRect(0, y, g.getWidth(), y + (secondaryFontHeight * 4));
|
||||
|
||||
g.setFontAlign(0, -1).setFont("Vector", secondaryFontHeight);
|
||||
if (METARage > 90) { // older than 1.5h
|
||||
g.setColor(COLOUR_RED);
|
||||
} else if (METARage > 60) { // older than 1h
|
||||
g.setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW );
|
||||
} else {
|
||||
g.setColor(g.theme.fg);
|
||||
}
|
||||
let METARlines = g.wrapString(METAR, g.getWidth());
|
||||
METARlinesCount = METARlines.length;
|
||||
METARlines.splice(0, METARscollLines);
|
||||
g.drawString(METARlines.join("\n"), horizontalCenter, y, true);
|
||||
|
||||
if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); }
|
||||
}
|
||||
|
||||
// update the METAR info
|
||||
function updateAVWX() {
|
||||
if (avwxTimeout) clearTimeout(avwxTimeout);
|
||||
avwxTimeout = undefined;
|
||||
|
||||
METAR = '\nGetting GPS fix';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
drawAVWX();
|
||||
|
||||
Bangle.setGPSPower(true, APP_NAME);
|
||||
Bangle.on('GPS', fix => {
|
||||
// prevent multiple, simultaneous requests
|
||||
if (AVWXrequest) { return; }
|
||||
|
||||
if ('fix' in fix && fix.fix != 0 && fix.satellites >= 4) {
|
||||
Bangle.setGPSPower(false, APP_NAME);
|
||||
let lat = fix.lat;
|
||||
let lon = fix.lon;
|
||||
|
||||
METAR = '\nRequesting METAR';
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
drawAVWX();
|
||||
|
||||
// get latest METAR from nearest airport (via AVWX API)
|
||||
AVWXrequest = avwx.request('metar/'+lat+','+lon, 'onfail=nearest', data => {
|
||||
if (avwxTimeout) clearTimeout(avwxTimeout);
|
||||
avwxTimeout = undefined;
|
||||
|
||||
let METARjson = JSON.parse(data.resp);
|
||||
|
||||
if ('sanitized' in METARjson) {
|
||||
METAR = METARjson.sanitized;
|
||||
} else {
|
||||
METAR = 'No "sanitized" METAR data found!';
|
||||
}
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
|
||||
if ('time' in METARjson) {
|
||||
METARts = new Date(METARjson.time.dt);
|
||||
let now = new Date();
|
||||
let METARage = Math.floor((now - METARts) / 60000); // in minutes
|
||||
if (METARage <= 30) {
|
||||
// some METARs update every 30 min -> attempt to update after METAR is 35min old
|
||||
avwxTimeout = setTimeout(updateAVWX, (35 - METARage) * 60000);
|
||||
} else if (METARage <= 60) {
|
||||
// otherwise, attempt METAR update after it's 65min old
|
||||
avwxTimeout = setTimeout(updateAVWX, (65 - METARage) * 60000);
|
||||
}
|
||||
} else {
|
||||
METARts = undefined;
|
||||
}
|
||||
|
||||
drawAVWX();
|
||||
AVWXrequest = undefined;
|
||||
|
||||
}, error => {
|
||||
// AVWX API request failed
|
||||
console.log(error);
|
||||
METAR = 'ERR: ' + error;
|
||||
METARlinesCount = 0; METARscollLines = 0;
|
||||
METARts = undefined;
|
||||
drawAVWX();
|
||||
AVWXrequest = undefined;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// draw only the seconds part of the main clock
|
||||
function drawSeconds() {
|
||||
let now = new Date();
|
||||
let seconds = now.getSeconds().toString();
|
||||
if (seconds.length == 1) seconds = '0' + seconds;
|
||||
let y = Bangle.appRect.y + mainTimeHeight - 3;
|
||||
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_GREY);
|
||||
g.drawString(seconds, horizontalCenter + 54, y, true);
|
||||
}
|
||||
|
||||
// sync seconds update
|
||||
function syncSecondsUpdate() {
|
||||
drawSeconds();
|
||||
setTimeout(function() {
|
||||
drawSeconds();
|
||||
secondsInterval = setInterval(drawSeconds, 1000);
|
||||
}, 1000 - (Date.now() % 1000));
|
||||
}
|
||||
|
||||
// set timeout for per-minute updates
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
if (METARts) {
|
||||
let now = new Date();
|
||||
let METARage = Math.floor((now - METARts) / 60000);
|
||||
if (METARage > 60) {
|
||||
// the METAR colour might have to be updated:
|
||||
drawAVWX();
|
||||
}
|
||||
}
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
// draw top part of clock (main time, date and UTC)
|
||||
function draw() {
|
||||
let now = new Date();
|
||||
let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60));
|
||||
|
||||
// prepare main clock area
|
||||
let y = Bangle.appRect.y;
|
||||
|
||||
g.setBgColor(g.theme.bg);
|
||||
|
||||
// main time display
|
||||
g.setFontAlign(0, -1).setFont("Vector", mainTimeHeight).setColor(g.theme.fg);
|
||||
g.drawString(timeStr(now, false), horizontalCenter, y, true);
|
||||
|
||||
// prepare second line (UTC and date)
|
||||
y += mainTimeHeight;
|
||||
g.clearRect(0, y, g.getWidth(), y + secondaryFontHeight - 1);
|
||||
|
||||
// weekday and day of the month
|
||||
g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(dateColour);
|
||||
g.drawString(require("locale").dow(now, 1).toUpperCase() + ' ' + now.getDate(), 0, y, false);
|
||||
|
||||
// UTC
|
||||
g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(UTCColour);
|
||||
g.drawString(timeStr(nowUTC, false) + "Z", g.getWidth(), y, false);
|
||||
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
|
||||
// initialise
|
||||
g.clear(true);
|
||||
|
||||
// scroll METAR lines on taps
|
||||
Bangle.setUI("clockupdown", action => {
|
||||
switch (action) {
|
||||
case -1: // top tap
|
||||
if (settings.invertScrolling) {
|
||||
if (METARscollLines > 0)
|
||||
METARscollLines--;
|
||||
} else {
|
||||
if (METARscollLines < METARlinesCount - 4)
|
||||
METARscollLines++;
|
||||
}
|
||||
break;
|
||||
case 1: // bottom tap
|
||||
if (settings.invertScrolling) {
|
||||
if (METARscollLines < METARlinesCount - 4)
|
||||
METARscollLines++;
|
||||
} else {
|
||||
if (METARscollLines > 0)
|
||||
METARscollLines--;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
drawAVWX();
|
||||
});
|
||||
|
||||
// load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// draw static separator line
|
||||
y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight;
|
||||
g.setColor(separatorColour);
|
||||
g.drawLine(0, y, g.getWidth(), y);
|
||||
|
||||
// draw times and request METAR
|
||||
draw();
|
||||
if (settings.showSeconds)
|
||||
syncSecondsUpdate();
|
||||
updateAVWX();
|
||||
|
||||
|
||||
// TMP for debugging:
|
||||
//METAR = 'YAAA 011100Z 21014KT CAVOK 23/08 Q1018 RMK RF000/0000';
|
||||
//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW';
|
||||
//METAR = 'YAAA 020030Z VRB CAVOK';
|
||||
//METARts = new Date(Date.now() - 61 * 60000); // 61 to trigger warning, 91 to trigger alert
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,35 @@
|
|||
(function(back) {
|
||||
var FILE = "aviatorclk.json";
|
||||
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
showSeconds: true,
|
||||
invertScrolling: false,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "AV8R Clock" },
|
||||
"< Back" : () => back(),
|
||||
'Show Seconds': {
|
||||
value: !!settings.showSeconds, // !! converts undefined to false
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.showSeconds = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Invert Scrolling': {
|
||||
value: !!settings.invertScrolling, // !! converts undefined to false
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.invertScrolling = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "aviatorclk",
|
||||
"name": "Aviator Clock",
|
||||
"shortName":"AV8R Clock",
|
||||
"version":"1.00",
|
||||
"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" }],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"dependencies" : { "avwx": "module" },
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{ "name":"aviatorclk.app.js", "url":"aviatorclk.app.js" },
|
||||
{ "name":"aviatorclk.settings.js", "url":"aviatorclk.settings.js" },
|
||||
{ "name":"aviatorclk.img", "url":"aviatorclk-icon.js", "evaluate":true }
|
||||
],
|
||||
"data": [{ "name":"aviatorclk.json" }]
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
|
@ -0,0 +1 @@
|
|||
1.00: initial release
|
|
@ -0,0 +1,41 @@
|
|||
# AVWX Module
|
||||
|
||||
This is a module/library to use the [AVWX](https://account.avwx.rest/) Aviation
|
||||
Weather API. It doesn't include an app.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
You will need an AVWX account (see above for link) and generate an API token.
|
||||
The free "Hobby" plan is normally sufficient, but please consider supporting
|
||||
the AVWX project.
|
||||
|
||||
After installing the module on your Bangle, use the "interface" page (floppy
|
||||
disk icon) in the App Loader to set the API token.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Include the module in your app with:
|
||||
|
||||
const avwx = require('avwx');
|
||||
|
||||
Then use the exported function, for example to get the "sanitized" METAR from
|
||||
the nearest station to a lat/lon coordinate pair:
|
||||
|
||||
reqID = avwx.request('metar/'+lat+','+lon,
|
||||
'filter=sanitized&onfail=nearest',
|
||||
data => { console.log(data); },
|
||||
error => { console.log(error); });
|
||||
|
||||
The returned reqID can be useful to track whether a request has already been
|
||||
made (ie. the app is still waiting on a response).
|
||||
|
||||
Please consult the [AVWX documentation](https://avwx.docs.apiary.io/) for
|
||||
information about the available end-points and request parameters.
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
Flaparoo [github](https://github.com/flaparoo)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* AVWX Bangle Module
|
||||
*
|
||||
* AVWX doco: https://avwx.docs.apiary.io/
|
||||
* test AVWX API request with eg.: curl -X GET 'https://avwx.rest/api/metar/43.9844,-88.5570?token=...'
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
const AVWX_BASE_URL = 'https://avwx.rest/api/'; // must end with a slash
|
||||
const AVWX_CONFIG_FILE = 'avwx.json';
|
||||
|
||||
|
||||
// read in the settings
|
||||
var AVWXsettings = Object.assign({
|
||||
AVWXtoken: '',
|
||||
}, require('Storage').readJSON(AVWX_CONFIG_FILE, true) || {});
|
||||
|
||||
|
||||
/**
|
||||
* Make an AVWX API request
|
||||
*
|
||||
* @param {string} requestPath API path (after /api/), eg. 'meta/KOSH'
|
||||
* @param {string} params optional request parameters, eg. 'onfail=nearest' (use '&' in the string to combine multiple params)
|
||||
* @param {function} successCB callback if the API request was successful - will supply the returned data: successCB(data)
|
||||
* @param {function} failCB callback in case the API request failed - will supply the error: failCB(error)
|
||||
*
|
||||
* @returns {number} the HTTP request ID
|
||||
*
|
||||
* Example:
|
||||
* reqID = avwx.request('metar/'+lat+','+lon,
|
||||
* 'filter=sanitized&onfail=nearest',
|
||||
* data => { console.log(data); },
|
||||
* error => { console.log(error); });
|
||||
*
|
||||
*/
|
||||
exports.request = function(requestPath, optParams, successCB, failCB) {
|
||||
if (! AVWXsettings.AVWXtoken) {
|
||||
failCB('No AVWX API Token defined!');
|
||||
return undefined;
|
||||
}
|
||||
let params = 'token='+AVWXsettings.AVWXtoken;
|
||||
if (optParams)
|
||||
params += '&'+optParams;
|
||||
return Bangle.http(AVWX_BASE_URL+requestPath+'?'+params).then(successCB).catch(failCB);
|
||||
};
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,47 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>To use the <a href="https://account.avwx.rest/">AVWX</a> API, you need an account and generate an API token. The free "Hobby" plan is sufficient, but please consider supporting the AVWX project.</p>
|
||||
<p>
|
||||
<label class="form-label" for="AVWXtoken">AVWX API Token:</label>
|
||||
<input class="form-input" type="text" id="AVWXtoken" placeholder="Your personal AVWX API Token" />
|
||||
</p>
|
||||
<p>
|
||||
<button id="upload" class="btn btn-primary">Configure</button>
|
||||
</p>
|
||||
|
||||
<p><div id="status"></div></p>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
var AVWXsettings = {};
|
||||
|
||||
function onInit() {
|
||||
// read in existing settings to preserve them during an update
|
||||
try {
|
||||
Util.readStorageJSON('avwx.json', currentSettings => {
|
||||
if (currentSettings) {
|
||||
AVWXsettings = currentSettings;
|
||||
if ('AVWXtoken' in AVWXsettings) {
|
||||
document.getElementById('AVWXtoken').value = AVWXsettings.AVWXtoken;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Failed to read existing settings: "+e);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
AVWXsettings.AVWXtoken = document.getElementById('AVWXtoken').value;
|
||||
Util.writeStorage('avwx.json', JSON.stringify(AVWXsettings), () => {
|
||||
document.getElementById("status").innerHTML = 'AVWX configuration successfully uploaded to Bangle!';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "avwx",
|
||||
"name": "AVWX Module",
|
||||
"shortName":"AVWX",
|
||||
"version":"1.00",
|
||||
"description": "Module/library for the AVWX API",
|
||||
"icon": "avwx.png",
|
||||
"type": "module",
|
||||
"tags": "outdoors",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"provides_modules": ["avwx"],
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{ "name":"avwx", "url":"avwx.js" }
|
||||
],
|
||||
"data": [{ "name":"avwx.json" }]
|
||||
}
|
Loading…
Reference in New Issue