1
0
Fork 0

New app: aviatorclk (and module avwx)

master
Flaparoo 2023-11-28 17:46:29 +08:00
parent 367ad5d285
commit a809b128bc
16 changed files with 531 additions and 0 deletions

1
apps/aviatorclk/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
aviatorclk.json

View File

@ -0,0 +1 @@
1.00: initial release

36
apps/aviatorclk/README.md Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwg96iIACCqMBCwYABiAWQiUiAAUhDBwWGDCAWHDAYuMCw4ABGBYWKGBYuLGBcBLpAXNFxhIKFxgwCIyhIJC58hC44WNC5B2NPBIXbBYIAHNgIXKCpAYEC5AhBII8SDAQXJMI5EEC6ZREC6EhFwkRO4zuCC46AFAgLYEC4YCBIoaADF4gXEKgYXDVBAcCXxBZDkcyDRAXHmILCif//4GEC5f/PQQWB//zbAX/C5gAKC78BC6K/In4WJ+YXW+QXHMAURl4XJeQYWEGALhBC4q+BYYLbDFwowCkLTCRIyNHGArNBC48SFxIXCMApHDOwQXIJAIQCAAaWCDYJGIDAipGFwQWKDAUSDAnzUoIWMDAcjn/zUgQWOPYYADOZJjKFqIAp"))

View File

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

View File

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

View File

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

1
apps/avwx/ChangeLog Normal file
View File

@ -0,0 +1 @@
1.00: initial release

41
apps/avwx/README.md Normal file
View File

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

47
apps/avwx/avwx.js Normal file
View File

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

BIN
apps/avwx/avwx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

47
apps/avwx/interface.html Normal file
View File

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

18
apps/avwx/metadata.json Normal file
View File

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