Merge branch 'espruino:master' into master

pull/3185/head
Logan B 2024-02-14 18:07:29 -06:00 committed by GitHub
commit 69257dd469
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1619 additions and 35 deletions

View File

@ -3,7 +3,7 @@
"shortName":"3DClock",
"icon": "app.png",
"version":"0.01",
"description": "This is a simple 3D scalig demo based on Anton Clock",
"description": "This is a simple 3D scaling demo based on Anton Clock",
"screenshots" : [ { "url":"screenshot.png" }],
"type":"clock",
"tags": "clock",

View File

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

76
apps/flightdash/README.md Normal file
View File

@ -0,0 +1,76 @@
# Flight Dashboard
Shows basic flight and navigation instruments.
![](screenshot.png)
Basic flight data includes:
- Ground speed
- Track
- Altimeter
- VSI
- Local time
You can also set a destination to get nav guidance:
- Distance from destination
- Bearing to destination
- Estimated Time En-route (minutes and seconds)
- Estimated Time of Arrival (in UTC)
The speed/distance and altitude units are configurable.
Altitude data can be derived from GPS or the Bangle's barometer.
## DISCLAIMER
Remember to Aviate - Navigate - Communicate! Do NOT get distracted by your
gadgets, keep your eyes looking outside and do NOT rely on this app for actual
navigation!
## Usage
After installing the app, use the "interface" page (floppy disk icon) in the
App Loader to filter and upload a list of airports (to be used as navigation
destinations). Due to memory constraints, only up to about 500 airports can be
stored on the Bangle itself (recommended is around 100 - 150 airports max.).
Then, on the Bangle, access the Flight-Dash settings, either through the
Settings app (Settings -> Apps -> Flight-Dash) or a tap anywhere in the
Flight-Dash app itself. The following settings are available:
- **Nav Dest.**: Choose the navigation destination:
- Nearest airports (from the uploaded list)
- Search the uploaded list of airports
- User waypoints (which can be set/edited through the settings)
- Nearest airports (queried online through AVWX - requires Internet connection at the time)
- **Speed** and **Altitude**: Set the preferred units of measurements.
- **Use Baro**: If enabled, altitude information is derived from the Bangle's barometer (instead of using GPS altitude).
If the barometer is used for altitude information, the current QNH value is
also displayed. It can be adjusted by swiping up/down in the app.
To query the nearest airports online through AVWX, you have to install - and
configure - the [avwx](?id=avwx) module.
The app requires a text input method (to set user waypoint names, and search
for airports), and if not already installed will automatically install the
default "textinput" app as a dependency.
## Hint
Under the bearing "band", the current nav destination is displayed. Next to
that, you'll also find the cardinal direction you are approaching **from**.
This can be useful for inbound radio calls. Together with the distance, the
current altitude and the ETA, you have all the information required to make
radio calls like a pro!
## Author
Flaparoo [github](https://github.com/flaparoo)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4"))

View File

@ -0,0 +1,527 @@
/*
* Flight Dashboard - Bangle.js
*/
const COLOUR_BLACK = 0x0000; // same as: g.setColor(0, 0, 0)
const COLOUR_WHITE = 0xffff; // same as: g.setColor(1, 1, 1)
const COLOUR_GREEN = 0x07e0; // same as: g.setColor(0, 1, 0)
const COLOUR_YELLOW = 0xffe0; // same as: g.setColor(1, 1, 0)
const COLOUR_MAGENTA = 0xf81f; // same as: g.setColor(1, 0, 1)
const COLOUR_CYAN = 0x07ff; // same as: g.setColor(0, 1, 1)
const COLOUR_LIGHT_BLUE = 0x841f; // same as: g.setColor(0.5, 0.5, 1)
const APP_NAME = 'flightdash';
const horizontalCenter = g.getWidth() / 2;
const verticalCenter = g.getHeight() / 2;
const dataFontHeight = 22;
const secondaryFontHeight = 18;
const labelFontHeight = 12;
//globals
var settings = {};
var updateInterval;
var speed = '-'; var speedPrev = -1;
var track = '-'; var trackPrev = -1;
var lat = 0; var lon = 0;
var distance = '-'; var distancePrev = -1;
var bearing = '-'; var bearingPrev = -1;
var relativeBearing = 0; var relativeBearingPrev = -1;
var fromCardinal = '-';
var ETAdate = new Date();
var ETA = '-'; var ETAPrev = '';
var QNH = Math.round(Bangle.getOptions().seaLevelPressure); var QNHPrev = -1;
var altitude = '-'; var altitudePrev = -1;
var VSI = '-'; var VSIPrev = -1;
var VSIraw = 0;
var VSIprevTimestamp = Date.now();
var VSIprevAltitude;
var VSIsamples = 0; var VSIsamplesCount = 0;
var speedUnit = 'N/A';
var distanceUnit = 'N/A';
var altUnit = 'N/A';
// date object to time string in format (HH:MM[:SS])
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;
}
// add thousands separator to number
function addThousandSeparator(n) {
let s = n.toString();
if (s.length > 3) {
return s.substr(0, s.length - 3) + ',' + s.substr(s.length - 3, 3);
} else {
return s;
}
}
// update VSI
function updateVSI(alt) {
VSIsamples += alt; VSIsamplesCount += 1;
let VSInewTimestamp = Date.now();
if (VSIprevTimestamp + 1000 <= VSInewTimestamp) { // update VSI every 1 second
let VSInewAltitude = VSIsamples / VSIsamplesCount;
if (VSIprevAltitude) {
let VSIinterval = (VSInewTimestamp - VSIprevTimestamp) / 1000;
VSIraw = (VSInewAltitude - VSIprevAltitude) * 60 / VSIinterval; // extrapolate to change / minute
}
VSIprevTimestamp = VSInewTimestamp;
VSIprevAltitude = VSInewAltitude;
VSIsamples = 0; VSIsamplesCount = 0;
}
VSI = Math.floor(VSIraw / 10) * 10; // "smooth" VSI value
if (settings.altimeterUnits == 0) { // Feet
VSI = Math.round(VSI * 3.28084);
} // nothing else required since VSI is already in meters ("smoothed")
if (VSI > 9999) VSI = 9999;
else if (VSI < -9999) VSI = -9999;
}
// update GPS-derived information
function updateGPS(fix) {
if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return;
speed = 'N/A';
if (settings.speedUnits == 0) { // Knots
speed = Math.round(fix.speed * 0.539957);
} else if (settings.speedUnits == 1) { // km/h
speed = Math.round(fix.speed);
} else if (settings.speedUnits == 2) { // MPH
speed = Math.round(fix.speed * 0.621371);
}
if (speed > 9999) speed = 9999;
if (! settings.useBaro) { // use GPS altitude
altitude = 'N/A';
if (settings.altimeterUnits == 0) { // Feet
altitude = Math.round(fix.alt * 3.28084);
} else if (settings.altimeterUnits == 1) { // Meters
altitude = Math.round(fix.alt);
}
if (altitude > 99999) altitude = 99999;
updateVSI(fix.alt);
}
track = Math.round(fix.course);
if (isNaN(track)) track = '-';
else if (track < 10) track = '00'+track;
else if (track < 100) track = '0'+track;
lat = fix.lat;
lon = fix.lon;
// calculation from https://www.movable-type.co.uk/scripts/latlong.html
const latRad1 = lat * Math.PI/180;
const latRad2 = settings.destLat * Math.PI/180;
const lonRad1 = lon * Math.PI/180;
const lonRad2 = settings.destLon * Math.PI/180;
// distance (using "Equirectangular approximation")
let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2);
let y = (latRad2 - latRad1);
let distanceNumber = Math.sqrt(x*x + y*y) * 6371; // in km - 6371 = mean Earth radius
if (settings.speedUnits == 0) { // NM
distanceNumber = distanceNumber * 0.539957;
} else if (settings.speedUnits == 2) { // miles
distanceNumber = distanceNumber * 0.621371;
}
if (distanceNumber > 99.9) {
distance = '>100';
} else {
distance = (Math.round(distanceNumber * 10) / 10).toString();
if (! distance.includes('.'))
distance += '.0';
}
// bearing
y = Math.sin(lonRad2 - lonRad1) * Math.cos(latRad2);
x = Math.cos(latRad1) * Math.sin(latRad2) -
Math.sin(latRad1) * Math.cos(latRad2) * Math.cos(lonRad2 - lonRad1);
let nonNormalisedBearing = Math.atan2(y, x);
bearing = Math.round((nonNormalisedBearing * 180 / Math.PI + 360) % 360);
if (bearing > 337 || bearing < 23) {
fromCardinal = 'S';
} else if (bearing < 68) {
fromCardinal = 'SW';
} else if (bearing < 113) {
fromCardinal = 'W';
} else if (bearing < 158) {
fromCardinal = 'NW';
} else if (bearing < 203) {
fromCardinal = 'N';
} else if (bearing < 248) {
fromCardinal = 'NE';
} else if (bearing < 293) {
fromCardinal = 'E';
} else{
fromCardinal = 'SE';
}
if (bearing < 10) bearing = '00'+bearing;
else if (bearing < 100) bearing = '0'+bearing;
relativeBearing = parseInt(bearing) - parseInt(track);
if (isNaN(relativeBearing)) relativeBearing = 0;
if (relativeBearing > 180) relativeBearing -= 360;
else if (relativeBearing < -180) relativeBearing += 360;
// ETA
if (speed) {
let ETE = distanceNumber * 3600 / speed;
let now = new Date();
ETAdate = new Date(now + (now.getTimezoneOffset() * 1000 * 60) + ETE*1000);
if (ETE < 86400) {
ETA = timeStr(ETAdate, false);
} else {
ETA = '>24h';
}
} else {
ETAdate = new Date();
ETA = '-';
}
}
// update barometric information
function updatePressure(e) {
altitude = 'N/A';
if (settings.altimeterUnits == 0) { // Feet
altitude = Math.round(e.altitude * 3.28084);
} else if (settings.altimeterUnits == 1) { // Meters
altitude = Math.round(e.altitude); // altitude is given in meters
}
if (altitude > 99999) altitude = 99999;
updateVSI(e.altitude);
}
// (re-)draw all read-outs
function draw(initial) {
g.setBgColor(COLOUR_BLACK);
// speed
if (speed != speedPrev || initial) {
g.setFontAlign(-1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_GREEN);
g.clearRect(0, 0, 55, dataFontHeight);
g.drawString(speed.toString(), 0, 0, false);
if (initial) {
g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(speedUnit, 0, dataFontHeight, false);
}
speedPrev = speed;
}
// distance
if (distance != distancePrev || initial) {
g.setFontAlign(1, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
g.clearRect(g.getWidth() - 58, 0, g.getWidth(), dataFontHeight);
g.drawString(distance, g.getWidth(), 0, false);
if (initial) {
g.setFontAlign(1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(distanceUnit, g.getWidth(), dataFontHeight, false);
}
distancePrev = distance;
}
// track (+ static track/bearing content)
let trackY = 18;
let destInfoY = trackY + 53;
if (track != trackPrev || initial) {
g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
g.clearRect(horizontalCenter - 29, trackY, horizontalCenter + 28, trackY + dataFontHeight);
g.drawString(track.toString() + "\xB0", horizontalCenter + 3, trackY, false);
if (initial) {
let y = trackY + dataFontHeight + 1;
g.setColor(COLOUR_YELLOW);
g.drawRect(horizontalCenter - 30, trackY - 3, horizontalCenter + 29, y);
g.drawLine(0, y, g.getWidth(), y);
y += dataFontHeight + 5;
g.drawLine(0, y, g.getWidth(), y);
g.setFontAlign(1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA);
g.drawString(settings.destID, horizontalCenter, destInfoY, false);
}
trackPrev = track;
}
// bearing
if (bearing != bearingPrev || relativeBearing != relativeBearingPrev || initial) {
let bearingY = trackY + 27;
g.clearRect(0, bearingY, g.getWidth(), bearingY + dataFontHeight);
g.setColor(COLOUR_YELLOW);
for (let i = Math.floor(relativeBearing * 2.5) % 25; i <= g.getWidth(); i += 25) {
g.drawLine(i, bearingY + 3, i, bearingY + 16);
}
let bearingX = horizontalCenter + relativeBearing * 2.5;
if (bearingX > g.getWidth() - 26) bearingX = g.getWidth() - 26;
else if (bearingX < 26) bearingX = 26;
g.setFontAlign(0, -1).setFont("Vector", dataFontHeight).setColor(COLOUR_MAGENTA);
g.drawString(bearing.toString() + "\xB0", bearingX + 3, bearingY, false);
g.clearRect(horizontalCenter + 42, destInfoY, horizontalCenter + 69, destInfoY + secondaryFontHeight);
g.setFontAlign(-1, -1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_MAGENTA);
g.drawString(fromCardinal, horizontalCenter + 42, destInfoY, false);
if (initial) {
g.setFontAlign(-1, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(' from', horizontalCenter, destInfoY, false);
}
bearingPrev = bearing;
relativeBearingPrev = relativeBearing;
}
let row3y = g.getHeight() - 48;
// QNH
if (settings.useBaro) {
if (QNH != QNHPrev || initial) {
let QNHy = row3y - secondaryFontHeight - 2;
g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.clearRect(horizontalCenter - 29, QNHy - secondaryFontHeight, horizontalCenter + 22, QNHy);
g.drawString(QNH.toString(), horizontalCenter - 3, QNHy, false);
if (initial) {
g.setFontAlign(0, -1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('QNH', horizontalCenter - 3, QNHy, false);
}
QNHPrev = QNH;
}
}
// VSI
if (VSI != VSIPrev || initial) {
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.clearRect(0, row3y - secondaryFontHeight, 51, row3y);
g.drawString(VSI.toString(), 0, row3y, false);
if (initial) {
g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(altUnit + '/min', 0, row3y - secondaryFontHeight, false);
}
let VSIarrowX = 6;
let VSIarrowY = row3y - 42;
g.clearRect(VSIarrowX - 7, VSIarrowY - 10, VSIarrowX + 6, VSIarrowY + 10);
g.setColor(COLOUR_WHITE);
if (VSIraw > 30) { // climbing
g.fillRect(VSIarrowX - 1, VSIarrowY, VSIarrowX + 1, VSIarrowY + 10);
g.fillPoly([ VSIarrowX , VSIarrowY - 11,
VSIarrowX + 7, VSIarrowY,
VSIarrowX - 7, VSIarrowY]);
} else if (VSIraw < -30) { // descending
g.fillRect(VSIarrowX - 1, VSIarrowY - 10, VSIarrowX + 1, VSIarrowY);
g.fillPoly([ VSIarrowX , VSIarrowY + 11,
VSIarrowX + 7, VSIarrowY,
VSIarrowX - 7, VSIarrowY ]);
}
}
// altitude
if (altitude != altitudePrev || initial) {
g.setFontAlign(1, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.clearRect(g.getWidth() - 65, row3y - secondaryFontHeight, g.getWidth(), row3y);
g.drawString(addThousandSeparator(altitude), g.getWidth(), row3y, false);
if (initial) {
g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString(altUnit, g.getWidth(), row3y - secondaryFontHeight, false);
}
altitudePrev = altitude;
}
// time
let now = new Date();
let nowUTC = new Date(now + (now.getTimezoneOffset() * 1000 * 60));
g.setFontAlign(-1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_LIGHT_BLUE);
let timeStrMetrics = g.stringMetrics(timeStr(now, false));
g.drawString(timeStr(now, false), 0, g.getHeight(), true);
let seconds = now.getSeconds().toString();
if (seconds.length == 1) seconds = '0' + seconds;
g.setFontAlign(-1, 1).setFont("Vector", secondaryFontHeight);
g.drawString(seconds, timeStrMetrics.width + 2, g.getHeight() - 1, true);
if (initial) {
g.setFontAlign(-1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('LOCAL', 0, g.getHeight() - dataFontHeight, false);
}
// ETE
let ETEy = g.getHeight() - dataFontHeight;
let ETE = '-';
if (ETA != '-') {
let ETEseconds = Math.floor((ETAdate - nowUTC) / 1000);
if (ETEseconds < 0) ETEseconds = 0;
ETE = ETEseconds % 60;
if (ETE < 10) ETE = '0' + ETE;
ETE = Math.floor(ETEseconds / 60) + ':' + ETE;
if (ETE.length > 6) ETE = '>999m';
}
g.clearRect(horizontalCenter - 35, ETEy - secondaryFontHeight, horizontalCenter + 29, ETEy);
g.setFontAlign(0, 1).setFont("Vector", secondaryFontHeight).setColor(COLOUR_WHITE);
g.drawString(ETE, horizontalCenter - 3, ETEy, false);
if (initial) {
g.setFontAlign(0, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('ETE', horizontalCenter - 3, ETEy - secondaryFontHeight, false);
}
// ETA
if (ETA != ETAPrev || initial) {
g.clearRect(g.getWidth() - 63, g.getHeight() - dataFontHeight, g.getWidth(), g.getHeight());
g.setFontAlign(1, 1).setFont("Vector", dataFontHeight).setColor(COLOUR_WHITE);
g.drawString(ETA, g.getWidth(), g.getHeight(), false);
if (initial) {
g.setFontAlign(1, 1).setFont("Vector", labelFontHeight).setColor(COLOUR_CYAN);
g.drawString('UTC ETA', g.getWidth(), g.getHeight() - dataFontHeight, false);
}
ETAPrev = ETA;
}
}
function handleSwipes(directionLR, directionUD) {
if (directionUD == -1) { // up -> increase QNH
QNH = Math.round(Bangle.getOptions().seaLevelPressure);
QNH++;
Bangle.setOptions({'seaLevelPressure': QNH});
} else if (directionUD == 1) { // down -> decrease QNH
QNH = Math.round(Bangle.getOptions().seaLevelPressure);
QNH--;
Bangle.setOptions({'seaLevelPressure': QNH});
}
}
function handleTouch(button, xy) {
if ('handled' in xy && xy.handled) return;
Bangle.removeListener('touch', handleTouch);
if (settings.useBaro) {
Bangle.removeListener('swipe', handleSwipes);
}
// any touch -> show settings
clearInterval(updateTimeInterval);
Bangle.setGPSPower(false, APP_NAME);
if (settings.useBaro)
Bangle.setBarometerPower(false, APP_NAME);
eval(require("Storage").read(APP_NAME+'.settings.js'))( () => {
E.showMenu();
// "clear" values potentially affected by a settings change
speed = '-'; distance = '-';
altitude = '-'; VSI = '-';
// re-launch
start();
});
}
/*
* main
*/
function start() {
// read in the settings
settings = Object.assign({
useBaro: false,
speedUnits: 0, // KTS
altimeterUnits: 0, // FT
destID: 'KOSH',
destLat: 43.9844,
destLon: -88.5570,
}, require('Storage').readJSON(APP_NAME+'.json', true) || {});
// set units
if (settings.speedUnits == 0) { // Knots
speedUnit = 'KTS';
distanceUnit = 'NM';
} else if (settings.speedUnits == 1) { // km/h
speedUnit = 'KPH';
distanceUnit = 'KM';
} else if (settings.speedUnits == 2) { // MPH
speedUnit = 'MPH';
distanceUnit = 'SM';
}
if (settings.altimeterUnits == 0) { // Feet
altUnit = 'FT';
} else if (settings.altimeterUnits == 1) { // Meters
altUnit = 'M';
}
// initialise
g.reset();
g.setBgColor(COLOUR_BLACK);
g.clear();
// draw incl. static components
draw(true);
// enable timeout/interval and sensors
setTimeout(function() {
draw();
updateTimeInterval = setInterval(draw, 1000);
}, 1000 - (Date.now() % 1000));
Bangle.setGPSPower(true, APP_NAME);
Bangle.on('GPS', updateGPS);
if (settings.useBaro) {
Bangle.setBarometerPower(true, APP_NAME);
Bangle.on('pressure', updatePressure);
}
// handle interaction
if (settings.useBaro) {
Bangle.on('swipe', handleSwipes);
}
Bangle.on('touch', handleTouch);
setWatch(e => { Bangle.showClock(); }, BTN1); // exit on button press
}
start();
/*
// TMP for testing:
//settings.speedUnits = 1;
//settings.altimeterUnits = 1;
QNH = 1013;
updateGPS({"fix":1,"speed":228,"alt":3763,"course":329,"lat":36.0182,"lon":-75.6713});
updatePressure({"altitude":3700});
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

View File

@ -0,0 +1,328 @@
(function(back) {
const APP_NAME = 'flightdash';
const FILE = APP_NAME+'.json';
// if the avwx module is available, include an extra menu item to query nearest airports via AVWX
var avwx;
try {
avwx = require('avwx');
} catch (error) {
// avwx module not installed
}
// Load settings
var settings = Object.assign({
useBaro: false,
speedUnits: 0, // KTS
altimeterUnits: 0, // FT
destID: 'KOSH',
destLat: 43.9844,
destLon: -88.5570,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// update the nav destination
function updateNavDest(destID, destLat, destLon) {
settings.destID = destID.replace(/[\W]+/g, '').slice(0, 7);
settings.destLat = parseFloat(destLat);
settings.destLon = parseFloat(destLon);
writeSettings();
createDestMainMenu();
}
var airports; // cache list of airports
function readAirportsList(empty_cb) {
if (airports) { // airport list has already been read in
return true;
}
airports = require('Storage').readJSON(APP_NAME+'.airports.json', true);
if (! airports) {
E.showPrompt('No airports stored - download from the Bangle Apps Loader!',
{title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
empty_cb();
});
return false;
}
return true;
}
// use GPS fix
var afterGPSfixMenu = 'destNearest';
function getLatLon(fix) {
if (!('fix' in fix) || fix.fix == 0 || fix.satellites < 4) return;
Bangle.setGPSPower(false, APP_NAME+'-settings');
Bangle.removeListener('GPS', getLatLon);
switch (afterGPSfixMenu) {
case 'destNearest':
loadNearest(fix.lat, fix.lon);
break;
case 'createUserWaypoint':
{
if (!('userWaypoints' in settings))
settings.userWaypoints = [];
let newIdx = settings.userWaypoints.length;
settings.userWaypoints[newIdx] = {
'ID': 'USER'+(newIdx + 1),
'lat': fix.lat,
'lon': fix.lon,
};
writeSettings();
showUserWaypoints();
break;
}
case 'destAVWX':
// the free ("hobby") account of AVWX is limited to 10 nearest stations
avwx.request('station/near/'+fix.lat+','+fix.lon, 'n=10&airport=true&reporting=false', data => {
loadAVWX(data);
}, error => {
console.log(error);
E.showPrompt('AVWX query failed: '+error, {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
createDestMainMenu();
});
});
break;
default:
back();
}
}
// find nearest airports
function loadNearest(lat, lon) {
if (! readAirportsList(createDestMainMenu))
return;
const latRad1 = lat * Math.PI/180;
const lonRad1 = lon * Math.PI/180;
for (let i = 0; i < airports.length; i++) {
const latRad2 = airports[i].la * Math.PI/180;
const lonRad2 = airports[i].lo * Math.PI/180;
let x = (lonRad2 - lonRad1) * Math.cos((latRad1 + latRad2) / 2);
let y = (latRad2 - latRad1);
airports[i].distance = Math.sqrt(x*x + y*y) * 6371;
}
let nearest = airports.sort((a, b) => a.distance - b.distance).slice(0, 14);
let destNearest = {
'' : { 'title' : 'Nearest' },
'< Back' : () => createDestMainMenu(),
};
for (let i in nearest) {
let airport = nearest[i];
destNearest[airport.i+' - '+airport.n] =
() => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo);
}
E.showMenu(destNearest);
}
// process the data returned by AVWX
function loadAVWX(data) {
let AVWXairports = JSON.parse(data.resp);
let destAVWX = {
'' : { 'title' : 'Nearest (AVWX)' },
'< Back' : () => createDestMainMenu(),
};
for (let i in AVWXairports) {
let airport = AVWXairports[i].station;
let airport_id = ( airport.icao ? airport.icao : airport.gps );
destAVWX[airport_id+' - '+airport.name] =
() => setTimeout(updateNavDest, 10, airport_id, airport.latitude, airport.longitude);
}
E.showMenu(destAVWX);
}
// individual user waypoint menu
function showUserWaypoint(idx) {
let wayptID = settings.userWaypoints[idx].ID;
let wayptLat = settings.userWaypoints[idx].lat;
let wayptLon = settings.userWaypoints[idx].lon;
let destUser = {
'' : { 'title' : wayptID },
'< Back' : () => showUserWaypoints(),
};
destUser['Set as Dest.'] =
() => setTimeout(updateNavDest, 10, wayptID, wayptLat, wayptLon);
destUser['Edit ID'] = function() {
require('textinput').input({text: wayptID}).then(result => {
if (result) {
if (result.length > 7) {
console.log('test');
E.showPrompt('ID is too long!\n(max. 7 chars)',
{title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
showUserWaypoint(idx);
});
} else {
settings.userWaypoints[idx].ID = result;
writeSettings();
showUserWaypoint(idx);
}
} else {
showUserWaypoint(idx);
}
});
};
destUser['Delete'] = function() {
E.showPrompt('Delete user waypoint '+wayptID+'?',
{'title': 'Flight-Dash'}).then((v) => {
if (v) {
settings.userWaypoints.splice(idx, 1);
writeSettings();
showUserWaypoints();
} else {
showUserWaypoint(idx);
}
});
};
E.showMenu(destUser);
}
// user waypoints menu
function showUserWaypoints() {
let destUser = {
'' : { 'title' : 'User Waypoints' },
'< Back' : () => createDestMainMenu(),
};
for (let i in settings.userWaypoints) {
let waypt = settings.userWaypoints[i];
let idx = i;
destUser[waypt.ID] =
() => setTimeout(showUserWaypoint, 10, idx);
}
destUser['Create New'] = function() {
E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'});
afterGPSfixMenu = 'createUserWaypoint';
Bangle.setGPSPower(true, APP_NAME+'-settings');
Bangle.on('GPS', getLatLon);
};
E.showMenu(destUser);
}
// destination main menu
function createDestMainMenu() {
let destMainMenu = {
'' : { 'title' : 'Nav Dest.' },
'< Back' : () => E.showMenu(mainMenu),
};
destMainMenu['Is: '+settings.destID] = {};
destMainMenu['Nearest'] = function() {
E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'});
afterGPSfixMenu = 'destNearest';
Bangle.setGPSPower(true, APP_NAME+'-settings');
Bangle.on('GPS', getLatLon);
};
destMainMenu['Search'] = function() {
require('textinput').input({text: ''}).then(result => {
if (result) {
if (! readAirportsList(createDestMainMenu))
return;
result = result.toUpperCase();
let matches = [];
let tooManyFound = false;
for (let i in airports) {
if (airports[i].i.toUpperCase().includes(result) ||
airports[i].n.toUpperCase().includes(result)) {
matches.push(airports[i]);
if (matches.length >= 15) {
tooManyFound = true;
break;
}
}
}
if (! matches.length) {
E.showPrompt('No airports found!', {title: 'Flight-Dash', buttons: {OK: true} }).then((v) => {
createDestMainMenu();
});
return;
}
let destSearch = {
'' : { 'title' : 'Search Results' },
'< Back' : () => createDestMainMenu(),
};
for (let i in matches) {
let airport = matches[i];
destSearch[airport.i+' - '+airport.n] =
() => setTimeout(updateNavDest, 10, airport.i, airport.la, airport.lo);
}
if (tooManyFound) {
destSearch['More than 15 airports found!'] = {};
}
E.showMenu(destSearch);
} else {
createDestMainMenu();
}
});
};
destMainMenu['User waypts'] = function() { showUserWaypoints(); };
if (avwx) {
destMainMenu['Nearest (AVWX)'] = function() {
E.showMessage('Waiting for GPS fix', {title: 'Flight-Dash'});
afterGPSfixMenu = 'destAVWX';
Bangle.setGPSPower(true, APP_NAME+'-settings');
Bangle.on('GPS', getLatLon);
};
}
E.showMenu(destMainMenu);
}
// main menu
mainMenu = {
'' : { 'title' : 'Flight-Dash' },
'< Back' : () => {
Bangle.setGPSPower(false, APP_NAME+'-settings');
Bangle.removeListener('GPS', getLatLon);
back();
},
'Nav Dest.': () => createDestMainMenu(),
'Speed': {
value: parseInt(settings.speedUnits) || 0,
min: 0,
max: 2,
format: v => {
switch (v) {
case 0: return 'Knots';
case 1: return 'km/h';
case 2: return 'MPH';
}
},
onchange: v => {
settings.speedUnits = v;
writeSettings();
}
},
'Altitude': {
value: parseInt(settings.altimeterUnits) || 0,
min: 0,
max: 1,
format: v => {
switch (v) {
case 0: return 'Feet';
case 1: return 'Meters';
}
},
onchange: v => {
settings.altimeterUnits = v;
writeSettings();
}
},
'Use Baro': {
value: !!settings.useBaro, // !! converts undefined to false
format: v => v ? 'On' : 'Off',
onchange: v => {
settings.useBaro = v;
writeSettings();
}
},
};
E.showMenu(mainMenu);
})

View File

@ -0,0 +1,186 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="jquery-csv.min.js"></script>
</head>
<body>
<p>You can upload a list of airports, which can then be used as the
navigation destinations in the Flight-Dash. It is recommended to only
upload up to 100 - 150 airports max. Due to memory contraints on the
Bangle, no more than 500 airports can be uploaded.</p>
<p>The database of airports is based on <a href="https://ourairports.com/data/">OurAirports</a>.
<h2>Filter Airports</h2>
<div class="form-group row">
<label for="filter_range">Within:</label>
<input type="text" id="filter_range" size="4" />nm of
<label for="filter_lat">Lat:</label>
<input type="text" id="filter_lat" size="10" /> /
<label for="filter_lon">Lon:</label>
<input type="text" id="filter_lon" size="10" />
<div>
<small class="text-muted">This is using a simple lat/lon "block" - and
not within a proper radius around the given lat/lon position. An easy
way to find a lat/lon pair is to search for an airport based on ident
or name, and then use the found coordinates.</small>
</div>
</div>
<p>- or -</p>
<p>
<label for="filter_ident">Ident:</label>
<input type="text" id="filter_ident" />
</p>
<p>- or -</p>
<p>
<label for="filter_name">Name:</label>
<input type="text" id="filter_name" />
</p>
<p>Only 1 of the above filters is applied, with higher up in the list taking precedence.</p>
<div class="form-group row">
<label for="filter_country">Limit airports to within this country:</label>
<input type="text" id="filter_country" size="2" />
<div>
<small class="form-text text-muted">Use the
<a href="https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes">ISO-3166 2-letter code</a>,
eg. "AU"</small>
</div>
</div>
<p>
<button id="getAndFilter" class="btn btn-primary" onClick="getAndFilter();">Filter</button>
<button id="uploadButton" class="btn btn-primary" onClick="uploadAirports();" style="display: none;">Upload to Bangle</button>
</p>
<hr />
<h2>Results:</h2>
<p><div id="status"></div></p>
<div id="resultsTable"></div>
<script src="../../core/lib/interface.js"></script>
<script>
var airports = [];
function getAndFilter() {
let filterRange = $("#filter_range").val();
let filterLat = $("#filter_lat").val();
let filterLatMin, filterLatMax;
let filterLon = $("#filter_lon").val();
let filterLonMin, filterLonMax;
let filterIdent = $("#filter_ident").val().toUpperCase();
let filterName = $("#filter_name").val().toUpperCase();
let filterCountry = $("#filter_country").val().toUpperCase();
if (filterRange && (! filterLat || ! filterLon)) {
alert('When filtering by Range, set both a Latitude and a Longitude!');
return;
}
if (filterRange) {
filterLatMin = parseFloat(filterLat) - (parseInt(filterRange) / 60);
filterLatMax = parseFloat(filterLat) + (parseInt(filterRange) / 60);
filterLonMin = parseFloat(filterLon) - (parseInt(filterRange) / 60);
filterLonMax = parseFloat(filterLon) + (parseInt(filterRange) / 60);
}
$("#status").html($("<em>").text('Fetching and filtering airports ...'));
$.get('https://davidmegginson.github.io/ourairports-data/airports.csv', function (data) {
let allAirports = $.csv.toObjects(data);
airports = allAirports.filter((item) => {
if (filterRange) {
let lat = parseFloat(item.latitude_deg);
let lon = parseFloat(item.longitude_deg);
if (lat > filterLatMin && lat < filterLatMax &&
lon > filterLonMin && lon < filterLonMax) {
if (filterCountry) {
return item.iso_country == filterCountry;
} else {
return true;
}
} else {
return false;
}
}
if (filterIdent) {
if (item.ident.toUpperCase().includes(filterIdent)) {
if (filterCountry) {
return item.iso_country == filterCountry;
} else {
return true;
}
} else {
return false;
}
}
if (filterName) {
if (item.name.toUpperCase().includes(filterName)) {
if (filterCountry) {
return item.iso_country == filterCountry;
} else {
return true;
}
} else {
return false;
}
}
if (filterCountry) {
return item.iso_country == filterCountry;
}
}).map((item) => {
return {
'i': item.ident,
'n': item.name,
'la': item.latitude_deg,
'lo': item.longitude_deg
};
});
let container = $("#resultsTable");
if (airports.length == 0) {
$("#status").html($("<strong>").text('No airports matched the filter criteria!'));
return;
} else if (airports.length > 500) {
$("#status").html($("<strong>").text(airports.length+' airports matched the filter criteria - your Bangle can only handle a maximum of 500!'));
return;
} else if (airports.length > 150) {
$("#status").html($("<strong>").text(airports.length+" airports matched the filter criteria - your Bangle will struggle with more than 150 airports. You can try, but it's recommended to reduce the number of airports."));
}
container.html($("<p>").text('Number of matching airports: '+airports.length));
let table = $("<table>");
table.addClass('table');
let cols = Object.keys(airports[0]);
$.each(airports, function(i, item){
let tr = $("<tr>");
let vals = Object.values(item);
$.each(vals, (i, elem) => {
tr.append($("<td>").text(elem));
});
table.append(tr);
});
container.append(table)
$("#status").html('');
$("#uploadButton").show();
});
}
function uploadAirports() {
$("#status").html($("<em>").text('Uploading airports to Bangle ...'));
Util.writeStorage('flightdash.airports.json', JSON.stringify(airports), () => {
$('#status').html('Airports successfully uploaded to Bangle!');
});
}
</script>
</body>
</html>

1
apps/flightdash/jquery-csv.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
{
"id": "flightdash",
"name": "Flight Dashboard",
"shortName":"Flight-Dash",
"version":"1.00",
"description": "Basic flight and navigation instruments",
"icon": "flightdash.png",
"screenshots": [{ "url": "screenshot.png" }],
"type": "app",
"tags": "outdoors",
"supports": ["BANGLEJS2"],
"dependencies": { "textinput": "type" },
"readme": "README.md",
"interface": "interface.html",
"storage": [
{ "name":"flightdash.app.js", "url":"flightdash.app.js" },
{ "name":"flightdash.settings.js", "url":"flightdash.settings.js" },
{ "name":"flightdash.img", "url":"flightdash-icon.js", "evaluate":true }
],
"data": [{ "name":"flightdash.json" },{ "name":"flightdash.airports.json" }]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -13,6 +13,7 @@ const speedUnits = { // how many kph per X?
"kph": 1,
"km/h": 1,
"kmt": 1,
"km/t": 1,
"km/tim": 1,
"mph": 1.60934,
"kts": 1.852
@ -318,6 +319,42 @@ var locales = {
day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
// No translation for english...
},
"da_DK": {
lang: "da_DK",
decimal_point: ",",
thousands_sep: ".",
currency_symbol: "kr",
int_curr_symbol: "DKK",
speed: "km/t",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b. %Y", 1: "%d/%m %Y" }, // 1. feb. 2020 // 01/02 2020 // a better short ver. is 1/2 2020 but its not supported
abmonth: "jan,feb,mar,apr,maj,jun,jul,aug,sep,okt,nov,dec",
month: "januar,februar,marts,april,maj,juni,juli,august,september,oktober,november,december",
abday: "søn,man,tir,ons,tor,fre,lør",
day: "søndag,mandag,tirsdag,onsdag,torsdag,fredag,lørdag",
trans: { yes: "ja", Yes: "Ja", no: "nej", No: "Nej", ok: "ok", on: "tændt", off: "slukket" } // no single danish translation for "on"/"off", should not be used
},
"en_DK": { // Danish units with english language
lang: "en_DK",
decimal_point: ",",
thousands_sep: ".",
currency_symbol: "kr",
int_curr_symbol: "DKK",
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b. %Y", 1: "%d/%m %Y" }, // 1. feb. 2020 // 01/02 2020 // a better short ver. is 1/2 2020 but its not supported
abmonth: "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",
month: "January,February,March,April,May,June,July,August,September,October,November,December",
abday: "Sun,Mon,Tue,Wed,Thu,Fri,Sat",
day: "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday",
// No translation for english...
},
"en_NZ": {
lang: "en_NZ",
decimal_point: ".",

View File

@ -1 +1,2 @@
0.01: Initial Medical Information application!
0.02: Read height and weight from myprofile

View File

@ -10,12 +10,12 @@ The file has the following contents:
```
{
"bloodType": "",
"height": "",
"weight": "",
"medicalAlert": [ "" ]
}
```
Weight and height are read from myprofile.
## Medical information editor
Clicking on the download icon of `Medical Information` in the app loader invokes the editor.

View File

@ -1,8 +1,7 @@
const medicalinfo = require('medicalinfo').load();
const myprofile = require("Storage").readJSON("myprofile.json",1)||{};
// const medicalinfo = {
// bloodType: "O+",
// height: "166cm",
// weight: "73kg"
// };
function hasAlert(info) {
@ -12,7 +11,7 @@ function hasAlert(info) {
// No space for widgets!
// TODO: no padlock widget visible so prevent screen locking?
g.clear();
g.reset().clear();
const bodyFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
g.setFont(bodyFont);
@ -33,10 +32,14 @@ if (hasAlert(medicalinfo)) {
if (medicalinfo.bloodType) {
lines = lines.concat(g.wrapString("Blood group: " + medicalinfo.bloodType, g.getWidth() - 10));
}
if (medicalinfo.height) {
if (myprofile.height) { // Prefer height from myprofile if set
lines = lines.concat(g.wrapString("Height: " + require("locale").distance(myprofile.height, 2), g.getWidth() - 10));
} else if (medicalinfo.height) { // read height from own settings if previously stored here
lines = lines.concat(g.wrapString("Height: " + medicalinfo.height, g.getWidth() - 10));
}
if (medicalinfo.weight) {
if (myprofile.weight) { // Prefer weight from myprofile if set
lines = lines.concat(g.wrapString("Weight: " + myprofile.weight + "kg", g.getWidth() - 10));
} else if (medicalinfo.weight) { // read weight from own settings if previously stored here
lines = lines.concat(g.wrapString("Weight: " + medicalinfo.weight, g.getWidth() - 10));
}

View File

@ -3,8 +3,6 @@ const storage = require('Storage');
exports.load = function () {
const medicalinfo = storage.readJSON('medicalinfo.json') || {
bloodType: "",
height: "",
weight: "",
medicalAlert: [""]
};

View File

@ -1,6 +1,4 @@
{
"bloodType": "",
"height": "",
"weight": "",
"medicalAlert": [ "" ]
}

View File

@ -1,6 +1,6 @@
{ "id": "medicalinfo",
"name": "Medical Information",
"version":"0.01",
"version":"0.02",
"description": "Provides 'medicalinfo.json' used by various health apps, as well as a way to edit it from the App Loader",
"icon": "app.png",
"tags": "health,medical",
@ -16,5 +16,6 @@
],
"data": [
{"name":"medicalinfo.json","url":"medicalinfo.json"}
]
],
"dependencies": {"myprofile":"app"}
}

2
apps/myprofile/ChangeLog Normal file
View File

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

View File

@ -10,6 +10,7 @@ Configure your personal profile. All settings are optional and are only stored o
| HR max | maximum heart rate | BPM | BPM | 60 | Use maximum value when exercising.<br/> If unsure set to 220-age. |
| HR min | minimum heart rate | BPM | BPM | 200 | Measure your heart rate after waking up |
| Height | Body height | local length unit | meter | 0 (=not set) | - |
| Weight | Body weight | kg | kf | 0 (=not set) | - |
| Stride length | distance traveled with one step | local length unit | meter | 0 (=not set) | Walk 10 steps and divide the travelled distance by 10 |
## Developer notes

View File

@ -0,0 +1,125 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css" />
<style type="text/css">
.alert {
padding: 20px;
background-color: #f44336; /* Red */
color: white;
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id="info"></div>
<button id="btnReload" class="btn btn-primary">Reload from watch</button>
<button id="btnUpload" class="btn btn-primary">Upload to watch</button>
<button id="btnDownload" class="btn btn-primary">Download</button>
<pre id="myprofile" contenteditable></pre>
<script src="../../core/lib/interface.js"></script>
<script>
const myProfileFile = "myprofile.json";
function errorFormat() {
var date = new Date();
var error =
'<p class="alert">' +
date.toUTCString() +
" : Wrong format, it should be JSON" +
"</p>";
return error;
}
function getEditableContent() {
return document.getElementById("myprofile").innerHTML.replace(/<[^>]*>/g, '');;
}
function isJsonString(str) {
try {
JSON.parse(str);
} catch (e) {
console.log(str)
console.log(e)
return false;
}
return true;
}
function uploadFile(fileid, contents) {
Puck.write(
`\x10(function() {
require("Storage").write("${fileid}",'${contents}');
Bluetooth.print("OK");
})()\n`,
(ret) => {
console.log("uploadFile", ret);
}
);
}
/* Load settings JSON file from the watch.
*/
function loadMyProfile() {
document.getElementById("info").innerHTML = "";
Util.showModal("Loading...");
Puck.eval(`require('Storage').readJSON("${myProfileFile}")`, (data) => {
document.getElementById("myprofile").innerHTML = JSON.stringify(
data,
null,
2
);
Util.hideModal();
});
}
/* Save settings as a JSON file on the watch.
*/
function uploadMyProfile() {
document.getElementById("info").innerHTML = "";
Util.showModal("Uploading...");
let myProfileJson = getEditableContent();
if (isJsonString(myProfileJson)) {
let shortMedicalInfoJson = JSON.stringify(JSON.parse(myProfileJson));
uploadFile(myProfileFile, shortMedicalInfoJson);
} else {
document.getElementById("info").innerHTML = errorFormat();
}
Util.hideModal();
}
function downloadMyProfile() {
document.getElementById("info").innerHTML = "";
Util.showModal("Downloading...");
let myProfileJson = getEditableContent();
if (isJsonString(myProfileJson)) {
Util.saveFile(myProfileFile, "application/json", myProfileJson);
} else {
document.getElementById("info").innerHTML = errorFormat();
}
Util.hideModal();
}
document
.getElementById("btnUpload")
.addEventListener("click", function () {
uploadMyProfile();
});
document
.getElementById("btnDownload")
.addEventListener("click", function () {
downloadMyProfile();
});
document
.getElementById("btnReload")
.addEventListener("click", function () {
loadMyProfile();
});
function onInit() {
loadMyProfile();
}
</script>
</body>
</html>

View File

@ -3,11 +3,12 @@
"shortName":"My Profile",
"icon": "app.png",
"type": "settings",
"version":"0.01",
"version":"0.02",
"description": "Configure your personal profile. All settings are optional and only stored on the watch.",
"readme": "README.md",
"tags": "tool,utility",
"supports": ["BANGLEJS", "BANGLEJS2"],
"interface": "interface.html",
"storage": [
{"name":"myprofile.settings.js","url":"settings.js"}
],
@ -15,4 +16,3 @@
{"name":"myprofile.json"}
]
}

View File

@ -7,6 +7,7 @@
strideLength: 0, // 0 = not set
birthday: '1970-01-01',
height: 0, // 0 = not set
weight: 0, // 0 = not set
}, require('Storage').readJSON(FILE, true) || {});
function writeProfile() {
@ -101,6 +102,17 @@
}
},
/*LANG*/"Weight": {
value: myprofile.weight,
min:0,
step:1,
format: v => v ? v + "kg" : '-',
onchange: v => {
myprofile.weight=v;
writeProfile();
},
},
/*LANG*/'HR max': {
format: v => /*LANG*/`${v} BPM`,
value: myprofile.maxHrm,

View File

@ -1,2 +1,3 @@
0.01: New widget
0.02: Use default Bangle formatter for booleans
0.03: Add option to hide widget

View File

@ -2,7 +2,7 @@
"id": "widadjust",
"name": "Adjust Clock",
"icon": "icon.png",
"version": "0.02",
"version": "0.03",
"description": "Adjusts clock continually in the background to counter clock drift",
"type": "widget",
"tags": "widget",

View File

@ -108,6 +108,11 @@
value: settings.debugLog,
onchange: v => settings.debugLog = v,
},
'Hide Widget': {
value: settings.hide || false,
onchange: v => settings.hide = v,
},
};
E.showMenu(mainMenu);

View File

@ -78,13 +78,16 @@
}
function debug(line) {
console.log(line);
//console.log(line);
if (debugLogFile !== null) {
debugLogFile.write(line + '\n');
}
}
function draw() {
if (settings.hide === true) {
return;
}
g.reset().setFont('6x8').setFontAlign(0, 0);
g.clearRect(this.x, this.y, this.x + WIDTH - 1, this.y + 23);
g.drawString(Math.round(clockError), this.x + WIDTH/2, this.y + 9);
@ -208,7 +211,7 @@
let updatedClockError = clockError + (now - lastClockErrorUpdateTime) * ppm / 1000000;
return now - updatedClockError;
},
width: WIDTH,
width: settings.hide === true ? 0 : WIDTH,
};
if (settings.saveState) {

View File

@ -9,3 +9,5 @@
0.06: Fix exception
0.07: Ensure barometer gets turned off after a few readings (isBarometerOn broken in 2v16)
0.08: Compatibility with hideable Widgets
0.09: Do not immediately measure on start, keep interval
Add plot history to settings

View File

@ -2,7 +2,7 @@
"id": "widbaroalarm",
"name": "Barometer Alarm Widget",
"shortName": "Barometer Alarm",
"version": "0.08",
"version": "0.09",
"description": "A widget that can alarm on when the pressure reaches defined thresholds.",
"icon": "widget.png",
"type": "widget",

View File

@ -107,9 +107,52 @@
return x + " min";
}
},
'Plot history': () => {E.showMenu(); draw();},
};
E.showMenu(menu);
}
function draw() {
const history3 = require('Storage').readJSON("widbaroalarm.log.json", true) || []; // history of recent 3 hours
const now = new Date()/(1000);
let curtime = now-3*60*60; // 3h ago
const data = [];
while (curtime <= now) {
// find closest value in history for this timestamp
const closest = history3.reduce((prev, curr) => {
return (Math.abs(curr.ts - curtime) < Math.abs(prev.ts - curtime) ? curr : prev);
});
data.push(closest.p);
curtime += settings.interval*60;
}
Bangle.setUI({
mode: "custom",
back: () => showMainMenu(),
});
g.reset().setFont("6x8",1);
require("graph").drawLine(g, data, {
axes: true,
x: 4,
y: Bangle.appRect.y+8,
height: Bangle.appRect.h-20,
gridx: 1,
gridy: 1,
miny: Math.min.apply(null, data)-1,
maxy: Math.max.apply(null, data)+1,
title: /*LANG*/"Barometer history (mBar)",
ylabel: y => y,
xlabel: i => {
const t = -3*60 + settings.interval*i;
if (t % 60 === 0) {
return "-" + t/60 + "h";
}
return "";
},
});
}
showMainMenu();
});

View File

@ -293,8 +293,18 @@ WIDGETS["baroalarm"] = {
draw : draw
};
if (interval > 0) {
setInterval(getPressureValue, interval * 60000);
// delay pressure measurement by interval-lastrun
const lastRun = history3.length > 0 ? history3[history3.length-1].ts : 0;
const lastRunAgo = Math.round(Date.now() / 1000) - lastRun;
let diffNextRun = interval*60-lastRunAgo;
if (diffNextRun < 0) {
diffNextRun = 0; // run asap
}
getPressureValue();
setTimeout(() => {
if (interval > 0) {
setInterval(getPressureValue, interval * 60000);
}
getPressureValue();
}, diffNextRun*1000);
})();

200
lang/ca_ES.json Normal file
View File

@ -0,0 +1,200 @@
{
"//": "Catalan language translations",
"GLOBAL": {
"//": "Translations that apply for all apps",
"Alarms": "Alarmes",
"Hours": "Hores",
"Minutes": "Minuts",
"Enabled": "Activat",
"Save": "Desa",
"Back": "Enrere",
"Repeat": "Repetició",
"Delete": "Esborra",
"ALARM!": "ALARMA!",
"Sleep": "Dormir",
"New Alarm": "Alarma nova",
"Yes": "Sí",
"No": "No",
"On": "Encès",
"Off": "Apagat",
"Ok": "D'acord",
"(repeat)": "(repeteix)",
"New Timer": "Temporitzador nou",
"music": "música",
"circle 2": "cercle 2",
"circle 1": "cercle 1",
"Keep Msgs": "Mantén Msgs",
"circle 3": "cercle 3",
"week": "setmana",
"Auto snooze": "Repetició automàtica de l'alarma",
"show widgets": "Mostra ginys",
"min. confidence": "confiança mínima",
"circle 4": "cercle 4",
"circle count": "recompte de cercles",
"heartrate": "ritme cardíac",
"Heartrate": "Ritme cardíac",
"weather circle": "cercle meteorològic",
"battery warn": "avís de bateria",
"minimum": "mínim",
"distance goal": "objectiu de distància",
"valid period": "període de validesa",
"maximum": "màxim",
"step length": "longitud del pas",
"data": "dades",
"colorize icon": "acoloriment de la icona",
"Circle": "Cercle",
"Launcher Settings": "Configuració del llançador",
"App Source\nNot found": "Fuent de l'aplicació\nNo s'ha trobat",
"Show clocks": "Mostra rellotges",
"Font": "Font",
"TAP right top/bottom": "TAP dret superior/inferior",
"Yes\ndefinitely": "Sí\ndefinitivament",
"View Message": "Veure missatges",
"Delete all messages": "Esborra tots els missatges",
"STEPS": "PASSOS",
"BTNs 1:startlap 2:exit 3:reset": "BTNs 1:startlap 2:exit 3:reset",
"start&lap/reset, BTN1: EXIT": "start&lap/reset, BTN1: EXIT",
"Are you sure": "N'esteu segur",
"Vector font size": "Mida de la font vectorial",
"Mark Unread": "Marca com a no llegit",
"No Messages": "No hi ha missatges",
"Delete All Messages": "Esborra tots els missatges",
"LCD": "LCD",
"Apps": "Aplicacions",
"Unread timer": "Temporitzador no llegit",
"Record Run": "Record de carrera",
"Bluetooth": "Bluetooth",
"Quiet Mode": "Mode silenciós",
"Piezo": "Piezo",
"Make Connectable": "Fes-ho connectable",
"Programmable": "Programable",
"Vibration": "Vibració",
"Passkey BETA": "Passkey BETA",
"Customize": "Personalitza",
"HID": "HID",
"Utils": "Utilitats",
"Light BW": "Llum BW",
"BLE": "BLE",
"Dark BW": "BW fosc",
"Background 2": "Fons 2",
"Foreground 2": "Prime pla 2",
"Foreground": "Primer pla",
"Highlight BG": "Ressalta BG",
"Connect device\nto add to\nwhitelist": "Connecta dispositiu\nper afegir-ho a la\nllista blanca",
"Highlight FG": "Ressalta FG",
"Background": "Fons",
"Add Device": "Afegir dispositiu",
"Remove": "Suprimeix",
"Wake on BTN3": "Desperta en BTN3",
"Twist Max Y": "Gir màx Y",
"LCD Timeout": "Temps d'espera del LCD",
"Twist Threshold": "Llindar de gir",
"Wake on BTN2": "Desperta en BTN2",
"Wake on BTN1": "Desperta en BTN1",
"Wake on Twist": "Desperta en girar",
"LCD Brightness": "Brillantor del LCD",
"Log": "Registre",
"Time Zone": "Fus horari",
"Wake on FaceUp": "Desperta en cara amunt",
"Wake on Touch": "Desperta en tocar",
"Twist Timeout": "Temps d'espera en girar",
"Compact Storage": "Emmagatzematge compacte",
"Clock Style": "Estil del rellotge",
"Storage": "Emmagatzematge",
"Utilities": "Utilitats",
"Compacting...\nTakes approx\n1 minute": "S'està compactant...\nTriga aproximadament\n1 minut",
"Debug Info": "Informació de la depuració",
"Rewrite Settings": "Reescriure la configuració",
"Flatten Battery": "Descarrega la bateria",
"Turn Off": "Apaga",
"This will remove everything": "Això ho eliminarà tot",
"Reset Settings": "Reinicia la configuració",
"Month": "Mes",
"Second": "Segon",
"Date": "Data",
"Reset to Defaults": "Reinicia als valors per defecte",
"Hour": "Hora",
"Flattening battery - this can take hours.\nLong-press button to cancel": "Descarrega la bateria - pot trigar hores.\nPremeu prolongadament el botó per cancel·lar",
"Stay Connectable": "Mantén connectat",
"Minute": "Minut",
"No Clocks Found": "No s'ha trobat cap rellotge",
"Connectable": "Connectable",
"No app has settings": "Cap aplicació té configuració",
"Invalid settings": "Ajustes no vàlids",
"App Settings": "Configuració de l'aplicació",
"Side": "Costat",
"OFF": "APAGAT",
"Sleep Phase Alarm": "Alarma de fase de son",
"Widgets": "Ginys",
"Left": "Esquerra",
"Sort Order": "Criteri d'ordenació",
"TIMER": "TEMPORITZADOR",
"goal": "objectiu",
"Right": "A la dreta",
"on": "en",
"Alarm": "Alarma",
"Reset All": "Reinicia-ho tot",
"Reset all widgets": "Reinicia tots els ginys",
"Reset": "Reinicia",
"Beep": "So",
"System": "Sistema",
"Locale": "Configuració regional",
"Message": "Missatge",
"Set Time": "Estableix l'hora",
"Vibrate": "Vibració",
"Alerts": "Alertes",
"Timer": "Temporitzador",
"Error in settings": "Error en la configuració",
"Select Clock": "Selecciona rellotge",
"Whitelist": "Llista blanca",
"Disable": "Desactiva",
"BACK": "ENRERE",
"Factory Reset": "Restableix la configuració de fàbrica",
"Connected": "Connectat",
"ALARM": "ALARMA",
"Messages": "Missatges",
"Settings": "Configuració",
"Show": "Mostra",
"Hide": "Amaga",
"steps": "passos",
"back": "enrere",
"Steps": "Passos",
"Year": "Any",
"Loading": "S'està carregant",
"Music": "Música",
"color": "color",
"off": "apagat",
"Theme": "Tema",
"one": "un",
"two": "dos",
"three": "tres",
"four": "quatre",
"five": "cinc",
"six": "sis",
"seven": "set",
"eight": "vuit",
"nine": "nou",
"ten": "deu",
"eleven": "onze",
"twelve": "dotze"
},
"alarm": {
"//": "App-specific overrides",
"rpt": "rep."
},
"fuzzyw": {
"//": "App-specific overrides",
"*$1 o'clock": "*$1 en punt",
"five past *$1": "*$1 i cinc",
"ten past *$1": "*$1 i deu",
"quarter past *$1": "Un quart de *$2",
"twenty past *$1": "Un quart i cinc de *$2",
"twenty five past *$1": "Un quart i deu de *$2",
"half past *$1": "Dos quarts de *$2",
"twenty five to *$2": "Dos quarts i cinc de *$2",
"twenty to *$2": "Dos quarts i deu de *$2",
"quarter to *$2": "Tres quarts de *$2",
"ten to *$2": "Tres quarts i cinc de *$2",
"five to *$2": "Tres quarts ben tocats de *$2"
}
}

View File

@ -13,7 +13,7 @@
"ALARM!": "ALARM!",
"Sleep": "Dormir",
"New Alarm": "Nueva alarma",
"Yes": "Si",
"Yes": "Sí",
"No": "No",
"On": "Encendido",
"Off": "Apagado",
@ -76,13 +76,13 @@
"Light BW": "Luz BW",
"BLE": "BLE",
"Dark BW": "BW oscuro",
"Background 2": "Antecedentes 2",
"Background 2": "Fondo 2",
"Foreground 2": "Primer plano 2",
"Foreground": "Primer plano",
"Highlight BG": "Resaltar BG",
"Connect device\nto add to\nwhitelist": "Conectar dispositivo\npara añadirlo a la\nlista blanca",
"Highlight FG": "Destacar FG",
"Background": "Antecedentes",
"Background": "Fondo",
"Add Device": "Añadir dispositivo",
"Remove": "Eliminar",
"Wake on BTN3": "Wake en BTN3",
@ -101,11 +101,11 @@
"Compact Storage": "Almacenamiento compacto",
"Clock Style": "Estilo de reloj",
"Storage": "Almacenamiento",
"Utilities": "Servicios públicos",
"Utilities": "Utilidades",
"Compacting...\nTakes approx\n1 minute": "La compactación...\nTarda aproximadamente\n1 minuto",
"Debug Info": "Información de depuración",
"Rewrite Settings": "Reescribir la configuración",
"Flatten Battery": "Aplastar la batería",
"Flatten Battery": "Descargar la batería",
"Turn Off": "Apagar",
"This will remove everything": "Esto eliminará todo",
"Reset Settings": "Restablecer la configuración",
@ -114,9 +114,9 @@
"Date": "Fecha",
"Reset to Defaults": "Restablecer los valores predeterminados",
"Hour": "Hora",
"Flattening battery - this can take hours.\nLong-press button to cancel": "Aplastar la batería - esto puede llevar horas.\nPulsar prolongadamente el botón para cancelar",
"Flattening battery - this can take hours.\nLong-press button to cancel": "Descargar la batería - esto puede llevar horas.\nPulsar prolongadamente el botón para cancelar",
"Stay Connectable": "Manténgase conectado",
"Minute": "Minuta",
"Minute": "Minuto",
"No Clocks Found": "No se han encontrado relojes",
"Connectable": "Conectable",
"No app has settings": "Ninguna aplicación tiene ajustes",
@ -138,9 +138,9 @@
"Reset": "Reiniciar",
"Beep": "Bip",
"System": "Sistema",
"Locale": "Localidad",
"Locale": "Configuración regional",
"Message": "Mensaje",
"Set Time": "Hora de la cita",
"Set Time": "Definir la hora",
"Vibrate": "Vibrar",
"Alerts": "Alertas",
"Timer": "Temporizador",
@ -185,7 +185,7 @@
"fuzzyw": {
"//": "App-specific overrides",
"*$1 o'clock": "*$1 en punto",
"five past *$1": "*$1 y cincq",
"five past *$1": "*$1 y cinco",
"ten past *$1": "*$1 y diez",
"quarter past *$1": "*$1 y cuarto",
"twenty past *$1": "*$1 y veinte",
@ -197,4 +197,4 @@
"ten to *$2": "*$2 menos diez",
"five to *$2": "*$2 menos cinco"
}
}
}

View File

@ -25,5 +25,6 @@
{"code":"sl_SL","name":"Slovenian","url":"sl_SL.json"},
{"code":"nn_NO","name":"Norwegian (Nynorsk)","url":"nn_NO.json"},
{"code":"hr_HR","name":"Croatian","url":"hr_HR.json"},
{"code":"ja_JA","name":"Japanese (beta)","url":"ja_JA.json"}
{"code":"ja_JA","name":"Japanese (beta)","url":"ja_JA.json"},
{"code":"ca_ES","name":"Catalan","url":"ca_ES.json"}
]