Merge branch 'master' of github.com:espruino/BangleApps

master
frederic wagner 2024-02-22 10:55:14 +01:00
commit 1a7a070d82
275 changed files with 8892 additions and 787 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=0.8,maximum-scale=0.8, minimum-scale=0.8, shrink-to-fit=no">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<link rel="stylesheet" href="css/spectre.min.css">
<link rel="stylesheet" href="css/spectre-exp.min.css">
<link rel="stylesheet" href="css/spectre-icons.min.css">
@ -248,12 +248,14 @@ if (typeof Android!=="undefined") {
hadData : false,
handlers : []
}
connection.on("data", function(d) {
connection.received += d;
connection.hadData = true;
if (connection.cb) connection.cb(d);
});
function bangleRx(data) {
// document.getElementById("status").innerText = "RX:"+data;
connection.received += data;
connection.hadData = true;
if (connection.cb) connection.cb(data);
// call data event
if (connection.handlers["data"])
connection.handlers["data"](data);

View File

@ -142,13 +142,11 @@
"SwitchCase": 1
}
],
"no-case-declarations": "off",
"no-constant-condition": "off",
"no-delete-var": "off",
"no-empty": "off",
"no-global-assign": "off",
"no-inner-declarations": "off",
"no-octal": "off",
"no-prototype-builtins": "off",
"no-redeclare": "off",
"no-unreachable": "warn",

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

@ -21,7 +21,6 @@
'< Back': back,
'Full Screen': {
value: settings.fullscreen,
format: () => (settings.fullscreen ? 'Yes' : 'No'),
onchange: () => {
settings.fullscreen = !settings.fullscreen;
save();

View File

@ -1 +1 @@
0.01: New Widget!
0.01: New Clock Info!

View File

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

View File

@ -0,0 +1,25 @@
# Clock Name
More info on making Clock Faces: https://www.espruino.com/Bangle.js+Clock
Describe the Clock...
## Usage
Describe how to use it
## Features
Name the function
## Controls
Name the buttons and what they are used for
## Requests
Name who should be contacted for support/update requests
## Creator
Your name

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))

View File

@ -0,0 +1,44 @@
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function draw() {
// queue next draw in one minute
queueDraw();
// Work out where to draw...
var x = g.getWidth()/2;
var y = g.getHeight()/2;
g.reset();
// work out locale-friendly date/time
var date = new Date();
var timeStr = require("locale").time(date,1);
var dateStr = require("locale").date(date);
// draw time
g.setFontAlign(0,0).setFont("Vector",48);
g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background
g.drawString(timeStr,x,y);
// draw date
y += 35;
g.setFontAlign(0,0).setFont("6x8");
g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
g.drawString(dateStr,x,y);
}
// Clear the screen once, at startup
g.clear();
// draw immediately at first, queue update
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");
// Load widgets
Bangle.loadWidgets();
Bangle.drawWidgets();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
{ "id": "7chname",
"name": "My clock human readable name",
"shortName":"Short Name",
"version":"0.01",
"description": "A detailed description of my clock",
"icon": "icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"7chname.app.js","url":"app.js"},
{"name":"7chname.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -32,3 +32,4 @@
Allow alarm enable/disable
0.31: Implement API for activity fetching
0.32: Added support for loyalty cards from gadgetbridge
0.33: Fix alarms created in Gadgetbridge not repeating

View File

@ -81,7 +81,12 @@
for (var j = 0; j < event.d.length; j++) {
// prevents all alarms from going off at once??
var dow = event.d[j].rep;
if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
var rp = false;
if (!dow) {
dow = 127; //if no DOW selected, set alarm to all DOW
} else {
rp = true;
}
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
@ -89,6 +94,7 @@
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.rp = rp;
a.last = last;
alarms.push(a);
}

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.32",
"version": "0.33",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",

1
apps/angles/ChangeLog Normal file
View File

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

49
apps/angles/app.js Normal file
View File

@ -0,0 +1,49 @@
g.clear().setRotation(1);
// g.setRotation ALSO changes accelerometer axes
var avrAngle = undefined;
var history = [];
var R = Bangle.appRect;
var W = g.getWidth();
var H = g.getHeight();
var relativeTo = undefined;
function draw(v) {
if (v===undefined) v = Bangle.getAccel();
// current angle
var d = Math.sqrt(v.y*v.y + v.z*v.z);
var ang = Math.atan2(-v.x, d)*180/Math.PI;
// Median filter
if (history.length > 10) history.shift(); // pull old reading off the start
history.push(ang);
avrAngle = history.slice().sort()[(history.length-1)>>1]; // median filter
// Render
var x = R.x + R.w/2;
var y = R.y + R.h/2;
g.reset().clearRect(R).setFontAlign(0,0);
var displayAngle = avrAngle;
g.setFont("6x15").drawString("ANGLE (DEGREES)", x, R.y2-8);
if (relativeTo!==undefined) {
g.drawString("RELATIVE TO", x,y-50);
g.setFont("Vector:30").drawString(relativeTo.toFixed(1),x,y-30);
y += 20;
displayAngle = displayAngle-relativeTo;
}
g.setFont("Vector:60").drawString(displayAngle.toFixed(1),x,y);
}
draw();
Bangle.on('accel',draw);
// Pressing the button turns relative angle on/off
Bangle.setUI({
mode : "custom",
btn : function(n) {
if (relativeTo===undefined)
relativeTo = avrAngle;
else
relativeTo = undefined;
draw();
}
});

1
apps/angles/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4cA///ov+5lChWMyGuxdzpdj4/lKf4AUkgQPgm0wAiPy2QCBsBkmS6QRNhIRBrVACJlPu2+pdICBcCrVJlvJtIRLifStMl3MtkARKydUyMkzMl0CMKyWWyUk1MkSJXkyR7BogRLgVcydSrVGzLHKgdLyfSpdE3JYKklqTwNJknJYJVkxcSp+pnygKhMs1OSEQOSYhVJl1bCIbBK5Mq7gRCyARJiVbqyPBCIKMKuVM24yBCIIiJnVOqu5CISMKp9JlvJCIRXKpP3nxoCRhUSBwSMNBwaMMgn6yp6DRhUl0mypiMMgM9ksipaMMhMtCINKRhlJmoRBpJuBCBIRGRhUE5I1CpKMLgmZn5ZDGhUAycnRoNMRhTDCsn3tfkRhLnDTwYQLNgSMMUQkyRhbGEkyMKAApFOAH4AGA"))

BIN
apps/angles/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

15
apps/angles/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "angles",
"name": "Angles (Spirit Level)",
"shortName": "Angles",
"version": "0.01",
"description": "Shows Angle or Relative angle in degrees (Digital Protractor/Inclinometer). Place Bangle sideways against a surface with the button facing away for best readings.",
"icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}],
"tags": "tool",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"angles.app.js","url":"app.js"},
{"name":"angles.img","url":"icon.js","evaluate":true}
]
}

BIN
apps/angles/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

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

View File

@ -0,0 +1,2 @@
1.00: initial release
1.01: added tap event to scroll METAR and toggle seconds display

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

@ -0,0 +1,41 @@
# 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). You can also tap the watch from the top
or bottom to scroll, which works even with the screen locked.
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.
To toggle the seconds display, double tap the watch from either the left or
right. This only changes the display "temporarily" (ie. it doesn't change the
default configured through the settings).
## Settings
- **Show Seconds**: to conserve battery power, you can turn the seconds display off (as the default)
- **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,314 @@
/*
* 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.setBgColor(g.theme.bg);
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 (either by touch or tap)
function scrollAVWX(action) {
switch (action) {
case -1: // top touch/tap
if (settings.invertScrolling) {
if (METARscollLines > 0)
METARscollLines--;
} else {
if (METARscollLines < METARlinesCount - 4)
METARscollLines++;
}
break;
case 1: // bottom touch/tap
if (settings.invertScrolling) {
if (METARscollLines < METARlinesCount - 4)
METARscollLines++;
} else {
if (METARscollLines > 0)
METARscollLines--;
}
break;
default:
// ignore other actions
}
drawAVWX();
}
Bangle.on('tap', data => {
switch (data.dir) {
case 'top':
scrollAVWX(-1);
break;
case 'bottom':
scrollAVWX(1);
break;
case 'left':
case 'right':
// toggle seconds display on double taps left or right
if (data.double) {
if (settings.showSeconds) {
clearInterval(secondsInterval);
let y = Bangle.appRect.y + mainTimeHeight - 3;
g.clearRect(horizontalCenter + 54, y - secondaryFontHeight, g.getWidth(), y);
settings.showSeconds = false;
} else {
settings.showSeconds = true;
syncSecondsUpdate();
}
}
break;
default:
// ignore other taps
}
});
Bangle.setUI("clockupdown", scrollAVWX);
// 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'; drawAVWX();
//METAR = 'YAAA 150900Z 14012KT 9999 SCT045 BKN064 26/14 Q1012 RMK RF000/0000 DL-W/DL-NW'; drawAVWX();
//METAR = 'YAAA 020030Z VRB CAVOK'; drawAVWX();
//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,33 @@
(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
onchange: v => {
settings.showSeconds = v;
writeSettings();
}
},
'Invert Scrolling': {
value: !!settings.invertScrolling, // !! converts undefined to false
onchange: v => {
settings.invertScrolling = v;
writeSettings();
}
},
});
})

View File

@ -0,0 +1,20 @@
{
"id": "aviatorclk",
"name": "Aviator Clock",
"shortName":"AV8R Clock",
"version":"1.01",
"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" }]
}

View File

@ -11,7 +11,6 @@
'< Back': back,
'Buzz': {
value: "buzz" in settings ? settings.buzz : false,
format: () => (settings.buzz ? 'Yes' : 'No'),
onchange: () => {
settings.buzz = !settings.buzz;
save('buzz', settings.buzz);

View File

@ -1,2 +1,3 @@
0.01: Added app
0.02: Removed unneeded squares
0.03: Added settings with fullscreen option

View File

@ -1,3 +1,7 @@
var settings = Object.assign({
fullscreen: false,
}, require('Storage').readJSON("binaryclk.json", true) || {});
function draw() {
var dt = new Date();
var h = dt.getHours(), m = dt.getMinutes();
@ -11,10 +15,14 @@ function draw() {
g.clearRect(Bangle.appRect);
let i = 0;
var gap = 8;
var mgn = 20;
if (settings.fullscreen) {
gap = 12;
mgn = 0;
}
const sq = 29;
const gap = 8;
const mgn = 20;
const pos = sq + gap;
var pos = sq + gap;
for (let r = 3; r >= 0; r--) {
for (let c = 0; c < 4; c++) {
@ -26,14 +34,15 @@ function draw() {
}
i++;
}
g.clearRect(mgn/2 + gap, mgn + gap, mgn/2 + gap + sq, mgn + 2 * gap + 2 * sq);
g.clearRect(mgn/2 + 3 * gap + 2 * sq, mgn + gap, mgn/2 + 3 * gap + 3 * sq, mgn + gap + sq);
g.clearRect(mgn/2 + gap, mgn + gap, mgn/2 + gap + sq, mgn + 2 * gap + 2 * sq);
g.clearRect(mgn/2 + 3 * gap + 2 * sq, mgn + gap, mgn/2 + 3 * gap + 3 * sq, mgn + gap + sq);
}
g.clear();
draw();
var secondInterval = setInterval(draw, 60000);
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}

View File

@ -1,7 +1,7 @@
{
"id": "binaryclk",
"name": "Bin Clock",
"version": "0.02",
"version": "0.03",
"description": "Clock face to show binary time in 24 hr format",
"icon": "app-icon.png",
"screenshots": [{"url":"screenshot.png"}],
@ -11,6 +11,8 @@
"allow_emulator": true,
"storage": [
{"name":"binaryclk.app.js","url":"app.js"},
{"name":"binaryclk.settings.js","url":"settings.js"},
{"name":"binaryclk.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"binaryclk.json"}]
}

View File

@ -0,0 +1,22 @@
(function(back) {
var FILE = "binaryclk.json";
var settings = Object.assign({
fullscreen: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
E.showMenu({
"" : { "title" : "Bin Clock" },
"< Back" : () => back(),
'Fullscreen': {
value: settings.fullscreen,
onchange: v => {
settings.fullscreen = v;
writeSettings();
}
},
});
})

1
apps/bthome/ChangeLog Normal file
View File

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

26
apps/bthome/README.md Normal file
View File

@ -0,0 +1,26 @@
# BTHome
This uses BTHome (https://bthome.io/) to allow easy control of [Home Assistant](https://www.home-assistant.io/) via Bluetooth advertisements.
Other apps like [the Home Assistant app](https://banglejs.com/apps/?id=ha) communicate with Home Assistant
via your phone so work from anywhere, but require being in range of your phone.
## Usage
When the app is installed, go to the `BTHome` app and click Settings.
Here, you can choose if you want to advertise your Battery status, but can also click `Add Button`.
You can then add a custom button event:
* `Icon` - the picture for the button
* `Name` - the name associated with the button
* `Action` - the action that Home Assistant will see when this button is pressed
* `Button #` - the button event 'number' - keep this at 0 for now
Once you've saved, you will then get your button shown in the BTHome app. Tapping it will make Bangle.js advertise via BTHome that the button has been pressed.
## ClockInfo
When you've added one or more buttons, they will appear in a ClockInfo under the main `Bangle.js` heading. You can just tap to select the ClockInfo, scroll down until a BTHome one is visible and then tap again. It will immediately send the Advertisement.

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

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA"))

27
apps/bthome/app.js Normal file
View File

@ -0,0 +1,27 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
function showMenu() {
var settings = require("Storage").readJSON("bthome.json",1)||{};
if (!(settings.buttons instanceof Array))
settings.buttons = [];
var menu = { "": {title:"BTHome", back:load} };
settings.buttons.forEach((button,idx) => {
var img = require("icons").getIcon(button.icon);
menu[/*LANG*/"\0"+img+" "+button.name] = function() {
Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true});
E.showMenu();
E.showMessage("Sending Event");
Bangle.buzz();
setTimeout(showMenu, 500);
};
});
menu[/*LANG*/"Settings"] = function() {
eval(require("Storage").read("bthome.settings.js"))(()=>showMenu());
};
E.showMenu(menu);
}
showMenu();

68
apps/bthome/boot.js Normal file
View File

@ -0,0 +1,68 @@
// Ensure we have the bleAdvert global (to play well with other stuff)
if (!Bangle.bleAdvert) Bangle.bleAdvert = {};
Bangle.btHomeData = [];
{
require("BTHome").packetId = 0|(Math.random()*256); // random packet id so new packets show up
let settings = require("Storage").readJSON("bthome.json",1)||{};
if (settings.showBattery)
Bangle.btHomeData.push({
type : "battery",
v : E.getBattery()
});
// If buttons defined, add events for them
if (settings.buttons instanceof Array) {
let n = settings.buttons.reduce((n,b)=>b.n>n?b.n:n,-1);
for (var i=0;i<=n;i++)
Bangle.btHomeData.push({type:"button_event",v:"none",n:n});
}
}
/* Global function to allow advertising BTHome adverts
extras = array of extra data, see require("BTHome").getAdvertisement - can add {n:0/1/2} for different instances
options = { event : an event - advertise fast, and when connected
}
*/
Bangle.btHome = function(extras, options) {
options = options||{};
if(extras) { // update with extras
extras.forEach(extra => {
var n = Bangle.btHomeData.find(b=>b.type==extra.type && b.n==extra.n);
if (n) Object.assign(n, extra);
else Bangle.btHomeData.push(extra);
});
}
var bat = Bangle.btHomeData.find(b=>b.type=="battery");
if (bat) bat.v = E.getBattery();
var advert = require("BTHome").getAdvertisement(Bangle.btHomeData)[0xFCD2];
// Add to the list of available advertising
if(Array.isArray(Bangle.bleAdvert)){
var found = false;
for(var ad in Bangle.bleAdvert){
if(ad[0xFCD2]){
ad[0xFCD2] = advert;
found = true;
break;
}
}
if(!found)
Bangle.bleAdvert.push({ 0xFCD2: advert });
} else {
Bangle.bleAdvert[0xFCD2] = advert;
}
var advOptions = {};
var updateTimeout = 10*60*1000; // update every 10 minutes
if (options.event) { // if it's an event...
advOptions.interval = 50;
advOptions.whenConnected = true;
updateTimeout = 30000; // slow down in 30 seconds
}
NRF.setAdvertising(Bangle.bleAdvert, advOptions);
if (Bangle.btHomeTimeout) clearTimeout(Bangle.btHomeTimeout);
Bangle.btHomeTimeout = setTimeout(function() {
delete Bangle.btHomeTimeout;
// clear events
Bangle.btHomeData.forEach(d => {if (d.type=="button_event") d.v="none";});
// update
Bangle.btHome();
},updateTimeout);
};

17
apps/bthome/clkinfo.js Normal file
View File

@ -0,0 +1,17 @@
(function() {
var settings = require("Storage").readJSON("bthome.json",1)||{};
if (!(settings.buttons instanceof Array))
settings.buttons = [];
return {
name: "Bangle",
items: settings.buttons.map(button => {
return { name : button.name,
get : function() { return { text : button.name,
img : require("icons").getIcon(button.icon) }},
show : function() {},
hide : function() {},
run : function() { Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); }
}
})
};
}) // must not have a semi-colon!

BIN
apps/bthome/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

20
apps/bthome/metadata.json Normal file
View File

@ -0,0 +1,20 @@
{ "id": "bthome",
"name": "BTHome",
"shortName":"BTHome",
"version":"0.01",
"description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth",
"icon": "icon.png",
"type": "app",
"tags": "clkinfo,bthome,bluetooth",
"supports" : ["BANGLEJS2"],
"dependencies": {"textinput":"type", "icons":"module"},
"readme": "README.md",
"storage": [
{"name":"bthome.img","url":"app-icon.js","evaluate":true},
{"name":"bthome.clkinfo.js","url":"clkinfo.js"},
{"name":"bthome.boot.js","url":"boot.js"},
{"name":"bthome.app.js","url":"app.js"},
{"name":"bthome.settings.js","url":"settings.js"}
],
"data":[{"name":"bthome.json"}]
}

91
apps/bthome/settings.js Normal file
View File

@ -0,0 +1,91 @@
(function(back) {
var settings = require("Storage").readJSON("bthome.json",1)||{};
if (!(settings.buttons instanceof Array))
settings.buttons = [];
function saveSettings() {
require("Storage").writeJSON("bthome.json",settings)
}
function showButtonMenu(button, isNew) {
var isNew = false;
if (!button) {
button = {name:"home", icon:"home", n:0, v:"press"};
isNew = true;
}
var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"];
var menu = {
"":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back:showMenu},
/*LANG*/"Icon" : {
value : "\0"+require("icons").getIcon(button.icon),
onchange : () => {
require("icons").showIconChooser().then(function(iconName) {
button.icon = iconName;
button.name = iconName;
showButtonMenu(button, isNew);
}, function() {
showButtonMenu(button, isNew);
});
}
},
/*LANG*/"Name" : {
value : button.name,
onchange : () => {
require("textinput").input({text:button.name}).then(function(name) {
button.name = name;
showButtonMenu(button, isNew);
}, function() {
showButtonMenu(button, isNew);
});
}
},
/*LANG*/"Action" : {
value : Math.max(0,actions.indexOf(button.v)), min:0, max:actions.length-1,
format : v => actions[v],
onchange : v => button.v=actions[v]
},
/*LANG*/"Button #" : {
value : button.n, min:0, max:3,
onchange : v => button.n=v
},
/*LANG*/"Save" : () => {
settings.buttons.push(button);
saveSettings();
showMenu();
}
};
if (!isNew) menu[/*LANG*/"Delete"] = function() {
E.showPrompt("Delete Button?").then(function(yes) {
if (yes) {
settings.buttons.splice(settings.buttons.indexOf(button),1);
saveSettings();
}
showMenu();
});
}
E.showMenu(menu);
}
function showMenu() {
var menu = { "": {title:"BTHome", back:back},
/*LANG*/"Show Battery" : {
value : !!settings.showBattery,
onchange : v=>{
settings.showBattery = v;
saveSettings();
}
}
};
settings.buttons.forEach((button,idx) => {
var img = require("icons").getIcon(button.icon);
menu[/*LANG*/"Button"+(img ? " \0"+img : (idx+1))] = function() {
showButtonMenu(button, false);
};
});
menu[/*LANG*/"Add Button"] = function() {
showButtonMenu(undefined, true);
};
E.showMenu(menu);
}
showMenu();
})

View File

@ -2,7 +2,7 @@
"name": "BTHome Temperature and Pressure",
"shortName":"BTHome T",
"version":"0.02",
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
"description": "Displays temperature and pressure, and advertises them over bluetooth for Home Assistant using BTHome.io standard",
"icon": "app.png",
"tags": "bthome,bluetooth,temperature",
"supports" : ["BANGLEJS2"],

View File

@ -32,7 +32,6 @@
},
'Show Lock': {
value: settings.showLock,
format: () => (settings.showLock ? 'Yes' : 'No'),
onchange: () => {
settings.showLock = !settings.showLock;
save();
@ -40,7 +39,6 @@
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();

View File

@ -32,7 +32,6 @@
},
'Show Lock': {
value: settings.showLock,
format: () => (settings.showLock ? 'Yes' : 'No'),
onchange: () => {
settings.showLock = !settings.showLock;
save();
@ -40,7 +39,6 @@
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();

View File

@ -369,7 +369,7 @@ function buttonPress(val) {
}
hasPressedNumber = false;
break;
default:
default: {
specials.R.val = 'C';
if (!swipeEnabled) drawKey('R', specials.R);
const is0Negative = (currNumber === 0 && 1/currNumber === -Infinity);
@ -385,6 +385,7 @@ function buttonPress(val) {
hasPressedNumber = currNumber;
displayOutput(currNumber);
break;
}
}
}

View File

@ -17,3 +17,4 @@
0.15: Edit holidays on device in settings
0.16: Add menu to fast open settings to edit holidays
Display Widgets in menus
0.17: Load holidays before events so the latter is not overpainted

View File

@ -43,24 +43,24 @@ const dowLbls = function() {
}();
const loadEvents = () => {
// add holidays & other events
events = (require("Storage").readJSON("calendar.days.json",1) || []).map(d => {
const date = new Date(d.date);
const o = {date: date, msg: d.name, type: d.type};
if (d.repeat) {
o.repeat = d.repeat;
}
return o;
});
// all alarms that run on a specific date
events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
events = events.concat((require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date).map(a => {
const date = new Date(a.date);
const time = timeutils.decodeTime(a.t);
date.setHours(time.h);
date.setMinutes(time.m);
date.setSeconds(time.s);
return {date: date, msg: a.msg, type: "e"};
});
// add holidays & other events
(require("Storage").readJSON("calendar.days.json",1) || []).forEach(d => {
const date = new Date(d.date);
const o = {date: date, msg: d.name, type: d.type};
if (d.repeat) {
o.repeat = d.repeat;
}
events.push(o);
});
}));
};
const loadSettings = () => {
@ -99,7 +99,7 @@ const sameDay = function(d1, d2) {
const drawEvent = function(ev, curDay, x1, y1, x2, y2) {
"ram";
switch(ev.type) {
case "e": // alarm/event
case "e": { // alarm/event
const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0;
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
const height = (y2-2) - (y1+2); // height of a cell
@ -107,6 +107,7 @@ const drawEvent = function(ev, curDay, x1, y1, x2, y2) {
const ystart = (y1+2) + slice*sliceHeight;
g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
break;
}
case "h": // holiday
g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
break;
@ -280,14 +281,12 @@ const showMenu = function() {
setUI();
},
/*LANG*/"Exit": () => load(),
/*LANG*/"Settings": () => {
const appSettings = eval(require('Storage').read('calendar.settings.js'));
appSettings(() => {
/*LANG*/"Settings": () =>
eval(require('Storage').read('calendar.settings.js'))(() => {
loadSettings();
loadEvents();
showMenu();
});
},
}),
};
if (require("Storage").read("alarm.app.js")) {
menu[/*LANG*/"Launch Alarms"] = () => {

View File

@ -1,7 +1,7 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.16",
"version": "0.17",
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Bugfixes
0.03: Use Bangle.setBacklight()
0.04: Add option to buzz after computer move

View File

@ -7,16 +7,17 @@ const FIELD_WIDTH = Bangle.appRect.w/8;
const FIELD_HEIGHT = Bangle.appRect.h/8;
const SETTINGS_FILE = "chess.json";
const ICON_SIZE=45;
const ICON_BISHOP = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA="));
const ICON_PAWN = require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA="));
const ICON_KING = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA=="));
const ICON_QUEEN = require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA="));
const ICON_ROOK = require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA="));
const ICON_KNIGHT = require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA"));
const get_icon_bishop = () => require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA="));
const get_icon_pawn = () => require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA="));
const get_icon_king = () => require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA=="));
const get_icon_queen = () => require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA="));
const get_icon_rook = () => require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA="));
const get_icon_knight = () => require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA"));
const settings = Object.assign({
state: engine.P4_INITIAL_BOARD,
computer_level: 0, // default to "stupid" which is the fastest
buzz: false, // Buzz when computer move is done
}, require("Storage").readJSON(SETTINGS_FILE,1) || {});
const ovr = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,2,{msb:true});
@ -56,22 +57,22 @@ const drawPiece = (buf, x, y, piece) => {
switch(piece & ~0x1) {
case engine.P4_PAWN:
icon = ICON_PAWN;
icon = get_icon_pawn();
break;
case engine.P4_BISHOP:
icon = ICON_BISHOP;
icon = get_icon_bishop();
break;
case engine.P4_KING:
icon = ICON_KING;
icon = get_icon_king();
break;
case engine.P4_QUEEN:
icon = ICON_QUEEN;
icon = get_icon_queen();
break;
case engine.P4_ROOK:
icon = ICON_ROOK;
icon = get_icon_rook();
break;
case engine.P4_KNIGHT:
icon = ICON_KNIGHT;
icon = get_icon_knight();
break;
}
@ -177,7 +178,7 @@ const move = (from,to,cbok) => {
};
const showMessage = (msg) => {
g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10);
g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10).flip();
};
// Run
@ -223,32 +224,31 @@ Bangle.on('touch', (button, xy) => {
showMessage(/*LANG*/"Moving..");
const posFrom = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT);
const posTo = idx2Pos(colTo/FIELD_WIDTH, rowTo/FIELD_HEIGHT);
setTimeout(() => {
const cb = () => {
// human move ok, update
drawBoard();
drawSelectedField();
if (!finished) {
// do computer move
Bangle.setBacklight(false); // this can take some time, turn off to save power
showMessage(/*LANG*/"Calculating..");
setTimeout(() => {
const compMove = state.findmove(settings.computer_level+1);
const result = move(compMove[0], compMove[1]);
if (result.ok) {
writeSettings();
}
Bangle.setLCDPower(true);
Bangle.setLocked(false);
Bangle.setBacklight(true);
if (!showmenu) {
showAlert(result.string);
}
}, 300); // execute after display update
const cb = () => {
// human move ok, update
drawBoard();
drawSelectedField();
if (!finished) {
// do computer move
Bangle.setBacklight(false); // this can take some time, turn off to save power
showMessage(/*LANG*/"Calculating..");
const compMove = state.findmove(settings.computer_level+1);
const result = move(compMove[0], compMove[1]);
if (result.ok) {
writeSettings();
}
};
move(posFrom, posTo,cb);
}, 100); // execute after display update
Bangle.setLCDPower(true);
Bangle.setLocked(false);
Bangle.setBacklight(true);
if (settings.buzz) {
Bangle.buzz(500);
}
if (!showmenu) {
showAlert(result.string);
}
}
};
move(posFrom, posTo,cb);
} // piece_sel === 0
startfield[0] = startfield[1] = undefined;
piece_sel = 0;
@ -298,5 +298,12 @@ setWatch(() => {
writeSettings();
}
},
/*LANG*/'Buzz on next turn': {
value: !!settings.buzz,
onchange: v => {
settings.buzz = v;
writeSettings();
}
},
});
}, BTN, { repeat: true, edge: "falling" });

View File

@ -2,7 +2,7 @@
"id": "chess",
"name": "Chess",
"shortName": "Chess",
"version": "0.03",
"version": "0.04",
"description": "Chess game based on the [p4wn engine](https://p4wn.sourceforge.net/). Drag on the touchscreen to move the green cursor onto a piece, select it with a single touch and drag the now red cursor around. Release the piece with another touch to finish the move. The button opens a menu.",
"icon": "app.png",
"tags": "game",

View File

@ -30,7 +30,6 @@
},
/*LANG*/'show widgets': {
value: !!settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x),
},
/*LANG*/'update interval': {
@ -45,7 +44,6 @@
},
/*LANG*/'show big weather': {
value: !!settings.showBigWeather,
format: () => (settings.showBigWeather ? 'Yes' : 'No'),
onchange: x => save('showBigWeather', x),
},
/*LANG*/'colorize icons': ()=>showCircleMenus()
@ -87,8 +85,7 @@
const colorizeIconKey = circleName + "colorizeIcon";
menu[/*LANG*/'circle ' + circleId] = {
value: settings[colorizeIconKey] || false,
format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'),
onchange: x => save(colorizeIconKey, x),
onchange: x => save(colorizeIconKey, x),
};
}
E.showMenu(menu);

View File

@ -9,7 +9,6 @@
'': { 'title': 'CLI complete clk' },
'Show battery': {
value: "battery" in settings ? settings.battery : false,
format: () => (settings.battery ? 'Yes' : 'No'),
onchange: () => {
settings.battery = !settings.battery;
save('battery', settings.battery);
@ -27,7 +26,6 @@
},
'Show weather': {
value: "weather" in settings ? settings.weather : false,
format: () => (settings.weather ? 'Yes' : 'No'),
onchange: () => {
settings.weather = !settings.weather;
save('weather', settings.weather);
@ -35,7 +33,6 @@
},
'Show steps': {
value: "steps" in settings ? settings.steps : false,
format: () => (settings.steps ? 'Yes' : 'No'),
onchange: () => {
settings.steps = !settings.steps;
save('steps', settings.steps);
@ -43,7 +40,6 @@
},
'Show heartrate': {
value: "heartrate" in settings ? settings.heartrate : false,
format: () => (settings.heartrate ? 'Yes' : 'No'),
onchange: () => {
settings.heartrate = !settings.heartrate;
save('heartrate', settings.heartrate);

View File

@ -64,13 +64,14 @@
switch (true) {
case (Radius > outerRadius): Color = '#000000'; break;
case (Radius < innerRadius): Color = '#FFFFFF'; break;
default:
default: {
let Phi = Math.atan2(dy,dx) + halfPi;
if (Phi < 0) { Phi += twoPi; }
if (Phi > twoPi) { Phi -= twoPi; }
let Index = Math.floor(12*Phi/twoPi);
Color = ColorList[Index];
}
}
g.setColor(1,1,1);
g.fillCircle(CenterX,CenterY, innerRadius);

View File

@ -894,7 +894,7 @@
g.setFontAlign(-1,0);
g.drawString('9', CenterX-outerRadius,CenterY);
break;
case '1-12':
case '1-12': {
let innerRadius = outerRadius * 0.9 - 10;
let dark = g.theme.dark;
@ -942,6 +942,7 @@
g.drawString(i == 0 ? '12' : '' + i, x,y);
}
}
}
let now = new Date();

View File

@ -70,7 +70,7 @@ function drawSimpleClock() {
var dom = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate();
//Days since full moon
var knownnew = new Date(2020,02,24,09,28,0);
var knownnew = new Date(2020,2,24,9,28,0);
// Get millisecond difference and divide down to cycles
var cycles = (d.getTime()-knownnew.getTime())/1000/60/60/24/29.53;

View File

@ -8,3 +8,4 @@
0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting
edge 2v18 ones), allowing compatability with the Back Swipe app.
0.09: Fix colors settings, where color was stored as string instead of the expected int.
0.10: Fix touch region for letters

View File

@ -107,7 +107,7 @@ exports.input = function(options) {
"ram";
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( (R.y+R.h) - 12 )) {
if (event.y >= ( (R.y+R.h) - 26 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
@ -139,7 +139,7 @@ exports.input = function(options) {
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
else if ((event.y < ( (R.y+R.h) - 26 )) && (event.y > ( (R.y+R.h) - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }

View File

@ -1,6 +1,6 @@
{ "id": "dragboard",
"name": "Dragboard",
"version":"0.09",
"version":"0.10",
"description": "A library for text input via swiping keyboard",
"icon": "app.png",
"type":"textinput",

View File

@ -2,3 +2,4 @@
0.02: Allow boot exceptions, e.g. to load DST
0.03: Permit exceptions to load in low-power mode, e.g. daylight saving time.
Also avoid polluting global scope.
0.04: Enhance menu: enable bluetooth, visit settings & visit recovery

View File

@ -61,14 +61,13 @@ var reload = function () {
nextDraw = undefined;
},
btn: function () {
E.showPrompt("Restore watch to full power?").then(function (v) {
if (v) {
drainedRestore();
}
else {
reload();
}
});
var menu = {
"Restore to full power": drainedRestore,
"Enable BLE": function () { return NRF.wake(); },
"Settings": function () { return load("setting.app.js"); },
"Recovery": function () { return Bangle.showRecoveryMenu(); },
};
E.showMenu(menu);
}
});
Bangle.CLOCK = 1;

View File

@ -79,13 +79,13 @@ const reload = () => {
nextDraw = undefined;
},
btn: () => {
E.showPrompt("Restore watch to full power?").then(v => {
if(v){
drainedRestore();
}else{
reload();
}
})
const menu = {
"Restore to full power": drainedRestore,
"Enable BLE": () => NRF.wake(),
"Settings": () => load("setting.app.js"),
"Recovery": () => Bangle.showRecoveryMenu(),
};
E.showMenu(menu);
}
});
Bangle.CLOCK=1;

View File

@ -1,12 +1,10 @@
{
"id": "drained",
"name": "Drained",
"version": "0.03",
"version": "0.04",
"description": "Switches to displaying a simple clock when the battery percentage is low, and disables some peripherals",
"readme": "README.md",
"icon": "icon.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Disable not existing BTN3 on Bangle.js 2, set maximum transmit power
0.03: Now use BTN2 on Bangle.js 1, and on Bangle.js 2 use the middle button to return to the menu

View File

@ -14,7 +14,8 @@ with 4 options:
with this address will be connected to directly. If not specified a menu
showing available Espruino devices is popped up.
* **RX** - If checked, the app will display any data received from the
device being connected to. Use this if you want to print data - eg: `print(E.getBattery())`
device being connected to (waiting 500ms after the last data before disconnecting).
Use this if you want to print data - eg: `print(E.getBattery())`
When done, click 'Upload'. Your changes will be saved to local storage
so they'll be remembered next time you upload from the same device.
@ -25,4 +26,9 @@ Simply load the app and you'll see a menu with the menu items
you defined. Select one and you'll be able to connect to the device
and send the command.
If a command should wait for a response then
The Bangle will connect to the device, send the command, and if:
* `RX` isn't set it will disconnect immediately and return to the menu
* `RX` is set it will listen for a response and write it to the screen, before
disconnecting after 500ms of inactivity. To return to the menu after this, press the button.

View File

@ -194,16 +194,14 @@ function sendCommandRX(device, text, callback) {
function done() {
Terminal.println("\\n============\\n Disconnected");
device.disconnect();
if (global.BTN3 !== undefined) {
setTimeout(function() {
setWatch(function() {
if (callback) callback();
resolve();
}, BTN3);
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
g.drawString("Back", g.getWidth()-10, g.getHeight()-50);
}, 200);
}
setTimeout(function() {
setWatch(function() {
if (callback) callback();
resolve();
}, (process.env.HWVERSION==2) ? BTN1 : BTN2);
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
g.drawString("Back", g.getWidth()-10, g.getHeight()/2);
}, 200);
}
device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
service = s;

View File

@ -2,7 +2,7 @@
"id": "espruinoctrl",
"name": "Espruino Control",
"shortName": "Espruino Ctrl",
"version": "0.02",
"version": "0.03",
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
"icon": "app.png",
"tags": "tool,bluetooth",

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

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

View File

@ -0,0 +1,32 @@
# Follow the recipe
A simple app using Gadgetbridge internet access to fetch a recipe and follow it step by step.
For now, if you are connected to Gadgetbridge, it display a random recipe whenever you restart the app.
Else, a stored recipe is displayed.
You can go to the next screen via tab right and go the previous screen via tab left.
You can choose a recipe via the App Loader:
Select the recipe then click on "Save recipe onto BangleJs".
![](screenshot1.png)
Make sure that you allowed 'Internet Access' via the Gadgetbridge app before using Follow The Recipe.
If you run the app via web IDE, connect your Banglejs via Gadgetbridge app then in the web IDE connect via Android.
For more informations, [see the documentation about Gadgetbridge](https://www.espruino.com/Gadgetbridge)
TO-DOs:
- [X] Display random recipe on start
- [ ] Choose between some recipe previously saved or random on start
- [ ] Edit the recipe and save it to BangleJs
- [ ] improve GUI (color, fonts, ...)
## Contributors
Written by [Mel-Levesque](https://github.com/Mel-Levesque)
## Thanks To
- Design taken from the [Info application](https://github.com/espruino/BangleApps/tree/master/apps/info) by [David Peer](https://github.com/peerdavid)
- App icon from [icons8.com](https://icons8.com)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA////e3uUt+cVEjEK0ALJlWqAAv4BYelqoAEBa/61ALRrQDCBY9q1ILCLYQLD0EKHZFawECAgILGFIYvHwAFBgQLGqwLDyoLGSwYLBI4gLFHYojFI4wdCMAJHGtEghEpBY9YkWIkoLNR4oLEHYwLMHYILJAoILIrWq1SzIBZYjE/gXKBYwAEEYwAEC67LGHQIABZY4jWF9FXBZVfBZX/BYmv/4AEBZ8KKIYACwALCACwA=="))

229
apps/followtherecipe/app.js Normal file
View File

@ -0,0 +1,229 @@
const storage = require("Storage");
const settings = require("Storage").readJSON("followtherecipe.json");
const locale = require('locale');
var ENV = process.env;
var W = g.getWidth(), H = g.getHeight();
var screen = 0;
var Layout = require("Layout");
let maxLenghtHorizontal = 16;
let maxLenghtvertical = 6;
let uri = "https://www.themealdb.com/api/json/v1/1/random.php";
var colors = {0: "#70f", 1:"#70d", 2: "#70g", 3: "#20f", 4: "#30f"};
var screens = [];
function drawData(name, value, y){
g.drawString(name, 10, y);
g.drawString(value, 100, y);
}
function drawInfo() {
g.reset().clearRect(Bangle.appRect);
var h=18, y = h;
// Header
g.drawLine(0,25,W,25);
g.drawLine(0,26,W,26);
// Info body depending on screen
g.setFont("Vector",15).setFontAlign(-1,-1).setColor("#0ff");
screens[screen].items.forEach(function (item, index){
g.setColor(colors[index]);
drawData(item.name, item.fun, y+=h);
});
// Bottom
g.setColor(g.theme.fg);
g.drawLine(0,H-h-3,W,H-h-3);
g.drawLine(0,H-h-2,W,H-h-2);
g.setFont("Vector",h-2).setFontAlign(-1,-1);
g.drawString(screens[screen].name, 2, H-h+2);
g.setFont("Vector",h-2).setFontAlign(1,-1);
g.drawString((screen+1) + "/" + screens.length, W, H-h+2);
}
// Change page if user touch the left or the right of the screen
Bangle.on('touch', function(btn, e){
var left = parseInt(g.getWidth() * 0.3);
var right = g.getWidth() - left;
var isLeft = e.x < left;
var isRight = e.x > right;
if(isRight){
screen = (screen + 1) % screens.length;
}
if(isLeft){
screen -= 1;
screen = screen < 0 ? screens.length-1 : screen;
}
Bangle.buzz(40, 0.6);
drawInfo();
});
function infoIngredients(ingredients, measures){
let combinedList = [];
let listOfString = [];
let lineBreaks = 0;
// Iterate through the arrays and combine the ingredients and measures
for (let i = 0; i < ingredients.length; i++) {
const combinedString = `${ingredients[i]}: ${measures[i]}`;
lineBreaks += 1;
// Check if the line is more than 16 characters
if (combinedString.length > maxLenghtHorizontal) {
// Add line break and update lineBreaks counter
combinedList.push(`${ingredients[i]}:\n${measures[i]}`);
lineBreaks += 1;
} else {
// Add to the combinedList array
combinedList.push(combinedString);
}
// Check the total line breaks
if (lineBreaks >= maxLenghtvertical) {
const resultString = combinedList.join('\n');
listOfString.push(resultString);
combinedList = [];
lineBreaks = 0;
}
if(i == ingredients.length){
listOfString.push(combinedList.join('\n'));
}
}
for(let i = 0; i < listOfString.length; i++){
let screen = {
name: "Ingredients",
items: [
{name: listOfString[i], fun: ""},
]
};
screens.push(screen);
}
}
// Format instructions to display on screen
function infoInstructions(instructionsString){
let item = [];
let chunkSize = 22;
//remove all space line and other to avoid problem with text
instructionsString = instructionsString.replace(/[\n\r]/g, '');
for (let i = 0; i < instructionsString.length; i += chunkSize) {
const chunk = instructionsString.substring(i, i + chunkSize).trim();
item.push({ name: chunk, fun: "" });
if (item.length === maxLenghtvertical) {
let screen = {
name: "Instructions",
items: item,
};
screens.push(screen);
item = [];
}
}
if (item.length > 0) {
let screen = {
name: "Instructions",
items: item,
};
screens.push(screen);
}
}
// Get json format and parse it into Strings
function getRecipeData(data) {
let mealName = data.strMeal;
let category = data.strCategory;
let area = data.strArea;
let instructions = data.strInstructions;
const ingredients = [];
const measures = [];
for (let i = 1; i <= 20; i++) {
const ingredient = data["strIngredient" + i];
const measure = data["strMeasure" + i];
if (ingredient && ingredient.trim() !== "") {
ingredients.push(ingredient);
if (measure && measure.trim() !== ""){
measures.push(measure);
}else{
measures.push("¯\\_(ツ)_/¯");
}
} else { // If no more ingredients are found
screens = [
{
name: "General",
items: [
{name: mealName, fun: ""},
{name: "", fun: ""},
{name: "Category", fun: category},
{name: "", fun: ""},
{name: "Area: ", fun: area},
]
}
];
infoIngredients(ingredients, measures);
infoInstructions(instructions);
drawInfo();
break;
}
}
}
function jsonData(){
let json = '{"meals":[{"idMeal":"52771","strMeal":"Spicy Arrabiata Penne","strDrinkAlternate":null,"strCategory":"Vegetarian","strArea":"Italian","strInstructions":"Bring a large pot of water to a boil. Add kosher salt to the boiling water, then add the pasta. Cook according to the package instructions, about 9 minutes.\\r\\nIn a large skillet over medium-high heat, add the olive oil and heat until the oil starts to shimmer. Add the garlic and cook, stirring, until fragrant, 1 to 2 minutes. Add the chopped tomatoes, red chile flakes, Italian seasoning and salt and pepper to taste. Bring to a boil and cook for 5 minutes. Remove from the heat and add the chopped basil.\\r\\nDrain the pasta and add it to the sauce. Garnish with Parmigiano-Reggiano flakes and more basil and serve warm.","strMealThumb":"https://www.themealdb.com/images/media/meals/ustsqw1468250014.jpg","strTags":"Pasta,Curry","strYoutube":"https://www.youtube.com/watch?v=1IszT_guI08","strIngredient1":"penne rigate","strIngredient2":"olive oil","strIngredient3":"garlic","strIngredient4":"chopped tomatoes","strIngredient5":"red chile flakes","strIngredient6":"italian seasoning","strIngredient7":"basil","strIngredient8":"Parmigiano-Reggiano","strIngredient9":"","strIngredient10":"","strIngredient11":"","strIngredient12":"","strIngredient13":"","strIngredient14":"","strIngredient15":"","strIngredient16":null,"strIngredient17":null,"strIngredient18":null,"strIngredient19":null,"strIngredient20":null,"strMeasure1":"1 pound","strMeasure2":"1/4 cup","strMeasure3":"3 cloves","strMeasure4":"1 tin ","strMeasure5":"1/2 teaspoon","strMeasure6":"1/2 teaspoon","strMeasure7":"6 leaves","strMeasure8":"spinkling","strMeasure9":"","strMeasure10":"","strMeasure11":"","strMeasure12":"","strMeasure13":"","strMeasure14":"","strMeasure15":"","strMeasure16":null,"strMeasure17":null,"strMeasure18":null,"strMeasure19":null,"strMeasure20":null,"strSource":null,"strImageSource":null,"strCreativeCommonsConfirmed":null,"dateModified":null}]}';
if(settings != null){
json = JSON.stringify({ meals: [settings] });
}
const obj = JSON.parse(json);
getRecipeData(obj.meals[0]);
}
function initData(retryCount) {
if (!Bangle.http) {
console.log("No http method found");
jsonData();
return;
}
jsonData();
Bangle.http(uri, { timeout: 1000 })
.then(event => {
try {
const obj = JSON.parse(event.resp);
if (obj.meals && obj.meals.length > 0) {
getRecipeData(obj.meals[0]);
} else {
console.log("Invalid JSON structure: meals array is missing or empty");
}
} catch (error) {
console.log("JSON Parse Error: " + error.message);
}
})
.catch(e => {
console.log("Request Error:", e);
if (e === "Timeout" && retryCount > 0) {
setTimeout(() => initData(retryCount - 1), 1000); // Optional: Add a delay before retrying
}else{
jsonData();
}
});
}
initData(3);
Bangle.on('lock', function(isLocked) {
drawInfo();
});
Bangle.loadWidgets();
Bangle.drawWidgets();

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

View File

@ -0,0 +1,146 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<style>
#responseContainer {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.meal {
flex: 1 0 calc(33.333% - 20px);
box-sizing: border-box;
}
.meal:hover {
background-color: cornflowerblue;
}
.meal img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<h3>Choose your recipe</h3>
<p>
<input id="recipeLink" type="text" autocomplete="off" placeholder="Search a Recipe" onkeyup="checkInput()" style="width:90%; margin: 3px"></input>
<p>Recipe to be imported to BangleJs: <span id="mealSelected">-</span></p>
<button id="upload" class="btn btn-primary">Save recipe into BangleJs</button>
</p>
<p id="testUtil">
</p>
<div id="responseContainer">
</div>
<script src="../../core/lib/interface.js"></script>
<script>
let uri = "";
let recipe = null;
const fileRecipeJson = "followtherecipe.json";
function checkInput(){
let inputStr = document.getElementById("recipeLink").value;
if(inputStr != "") {
getRecipe(inputStr);
}
}
function getRecipe(inputStr){
const Http = new XMLHttpRequest();
const url='https://www.themealdb.com/api/json/v1/1/search.php?s='+inputStr;
Http.open("GET", url);
Http.send();
Http.onreadystatechange = (e) => {
try{
const obj = JSON.parse(Http.response);
console.log("debug");
console.log(obj);
displayResponseData(obj)
}catch(e){
console.log("Error: "+e);
}
}
}
function displayResponseData(data){
const mealsContainer = document.getElementById('responseContainer');
while (mealsContainer.firstChild) {
mealsContainer.removeChild(mealsContainer.firstChild);
}
data.meals.forEach((meal) => {
const mealDiv = document.createElement('div');
mealDiv.classList.add('meal');
const imgElement = document.createElement('img');
imgElement.src = meal.strMealThumb;
imgElement.alt = meal.strMeal;
const titleP = document.createElement('p');
titleP.textContent = meal.strMeal;
// Append the image and title to the meal div
mealDiv.appendChild(imgElement);
mealDiv.appendChild(titleP);
mealDiv.onclick = function () {
document.getElementById("mealSelected").innerText = meal.strMeal;
let linkMeal = meal.strMeal.replaceAll(" ", "_");
uri = 'https://www.themealdb.com/api/json/v1/1/search.php?s='+linkMeal;
recipe = meal;
};
// Append the meal div to the container
mealsContainer.appendChild(mealDiv);
});
}
var settings = {};
function loadRecipe(){
try {
Util.showModal("Loading...");
Util.readStorageJSON(`${fileRecipeJson}`, data=>{
if(data){
settings = data;
document.getElementById("mealSelected").innerHTML = settings.strMeal;
checkInput();
}else{
console.log("NO data found");
}
});
} catch(ex) {
console.log("(Warning) Could not load data from BangleJs.");
console.log(ex);
}
Util.hideModal();
}
document.getElementById("upload").addEventListener("click", function() {
if(recipe != null){
try {
settings = recipe;
Util.showModal("Saving...");
Util.writeStorage("followtherecipe.json", JSON.stringify(settings), ()=>{
Util.hideModal();
});
console.log("Sent settings!");
} catch(ex) {
console.log("(Warning) Could not write settings to BangleJs.");
console.log(ex);
}
}
});
function onInit() {
loadRecipe();
}
onInit();
</script>
</body>
</html>

View File

@ -0,0 +1,30 @@
{
"id": "followtherecipe",
"name": "Follow The Recipe",
"shortName":"FTR",
"icon": "icon.png",
"version": "0.01",
"description": "Follow The Recipe (FTR) is a bangle.js app to follow a recipe step by step",
"type": "app",
"tags": "tool, tools, cook",
"supports": [
"BANGLEJS2"
],
"allow_emulator": true,
"interface": "interface.html",
"readme": "README.md",
"data": [
{"name":"followtherecipe.json"}
],
"storage": [
{
"name": "followtherecipe.app.js",
"url": "app.js"
},
{
"name": "followtherecipe.img",
"url": "app-icon.js",
"evaluate": true
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

View File

@ -1 +1,2 @@
0.01: attempt to import
0.02: Make it possible for Fastload Utils to fastload into this app.

View File

@ -1,5 +1,7 @@
// App Forge
"Bangle.loadWidgets()"; // Facilitates fastloading to this app via Fastload Utils, while still not loading widgets on standard `load` calls.
st = require('Storage');
l = /^a\..*\.js$/;

View File

@ -1,6 +1,6 @@
{ "id": "forge",
"name": "App Forge",
"version":"0.01",
"version":"0.02",
"description": "Easy way to run development versions of your apps",
"icon": "app.png",
"readme": "README.md",

View File

@ -23,7 +23,6 @@
'< Back': back,
'Show Widgets': {
value: settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: () => {
settings.showWidgets = !settings.showWidgets;
save();

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p>This tool allows you to update the firmware on <a href="https://www.espruino.com/Bangle.js2">Bangle.js 2</a> devices
<p>This tool allows you to update the firmware on <a href="https://www.espruino.com/Bangle.js2" target="_blank">Bangle.js 2</a> devices
from within the App Loader.</p>
<div id="fw-unknown">
@ -17,7 +17,7 @@
</ul>
<div id="fw-ok" style="display:none">
<p id="fw-old-bootloader-msg">If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
will fail with a message about the DFU version. If so, please <a href="bootloader_espruino_2v12_banglejs2.hex" class="fw-link">click here to update to DFU 2v12</a> and then click the 'Upload' button that appears.</p>
will fail with a message about the DFU version. If so, please <a href="bootloader_espruino_2v20_banglejs2.hex" class="fw-link">click here to update to DFU 2v20</a> and then click the 'Upload' button that appears.</p>
<div id="latest-firmware" style="display:none">
<p>The currently available Espruino firmware releases are:</p>
<ul id="latest-firmware-list">
@ -32,7 +32,8 @@
bit of code that runs when Bangle.js starts, and it is able to update the
Bangle.js firmware. Normally you would update firmware via this Firmware
Updater app, but if for some reason Bangle.js will not boot, you can
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">always use DFU to to the update manually</a>.</p>
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">always use DFU to do the update manually</a>.
On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 fixes this.</p>
<p>DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p>
</div>
@ -41,7 +42,7 @@
<div id="advanced-div" style="display:none">
<p><b>Advanced</b></p>
<p>Firmware updates via this tool work differently to the NRF Connect method mentioned on
<a href="https://www.espruino.com/Bangle.js2#firmware-updates">the Bangle.js 2 page</a>. Firmware
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">the Bangle.js 2 page</a>. Firmware
is uploaded to a file on the Bangle. Once complete the Bangle reboots and DFU copies
the new firmware into internal Storage.</p>
<p>In addition to the links above, you can upload a hex or zip file directly below. This file should be an <code>.app_hex</code>
@ -57,6 +58,15 @@
<pre id="log"></pre>
<p><a href="#" id="changelog-btn">Firmware ChangeLog ▼</a></p>
<div id="changelog-div" style="display:none">
<p><b>Firmware ChangeLog</b></p>
<ul>
<li><a href="https://www.espruino.com/ChangeLog" target="_blank">Released</a></li>
<li><a href="https://github.com/espruino/Espruino/blob/master/ChangeLog" target="_blank">Cutting Edge</a></li>
</ul>
</div>
<script src="../../core/lib/customize.js"></script>
<script src="../../core/lib/espruinotools.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.js"></script>
@ -87,28 +97,34 @@ function onInit(device) {
document.getElementById("fw-unknown").style = "display:none";
document.getElementById("fw-ok").style = "";
}
Puck.eval("E.CRC32(E.memoryArea(0xF7000,0x7000))", crc => {
console.log("DFU CRC = "+crc);
var version = `unknown (CRC ${crc})`;
Puck.eval("[E.CRC32(E.memoryArea(0xF7000,0x6000)),E.CRC32(E.memoryArea(0xF7000,0x7000))]", crcs => {
console.log("DFU CRC (6 pages) = "+crcs[0]);
console.log("DFU CRC (7 pages) = "+crcs[1]);
var version = `unknown (CRC ${crcs[1]})`;
var ok = true;
if (crc==1339551013) { version = "2v10.219"; ok = false; }
if (crc==1207580954) { version = "2v10.236"; ok = false; }
if (crc==3435933210) version = "2v11.52";
if (crc==46757280) version = "2v11.58";
if (crc==3508163280 || crc==1418074094) version = "2v12";
if (crc==4056371285) version = "2v13";
if (crc==1038322422) version = "2v14";
if (crc==2560806221) version = "2v15";
if (crc==2886730689) version = "2v16";
if (crc==156320890) version = "2v17";
if (crc==4012421318) version = "2v18";
if (crc==1856454048) version = "2v19";
if (crcs[0] == 1787004733) version = "2v20"; // check 6 page CRCs - the 7th page isn't used in 2v20+
else if (crcs[0] == 3816337552) version = "2v21";
else { // for other versions all 7 pages are used, check those
var crc = crcs[1];
if (crc==1339551013) { version = "2v10.219"; ok = false; }
if (crc==1207580954) { version = "2v10.236"; ok = false; }
if (crc==3435933210) version = "2v11.52";
if (crc==46757280) version = "2v11.58";
if (crc==3508163280 || crc==1418074094) version = "2v12";
if (crc==4056371285) version = "2v13";
if (crc==1038322422) version = "2v14";
if (crc==2560806221) version = "2v15";
if (crc==2886730689) version = "2v16";
if (crc==156320890) version = "2v17";
if (crc==4012421318) version = "2v18";
if (crc==1856454048) version = "2v19";
}
if (!ok) {
version += `(&#9888; update required)`;
}
document.getElementById("boot-version").innerHTML = version;
var versionNumber = parseFloat(version.replace(".","").replace("v","."));
if (versionNumber>=2.15)
if (versionNumber>=2.20)
document.getElementById("fw-old-bootloader-msg").style.display = "none";
});
}
@ -424,18 +440,25 @@ function handleUpload() {
storage:[
{name:"RAM", content:hexJS},
]
});
}, { noFinish: true });
}
document.getElementById('fileLoader').addEventListener('change', handleFileSelect, false);
document.getElementById("upload").addEventListener("click", handleUpload);
document.getElementById("info-btn").addEventListener("click", function() {
document.getElementById("info-btn").addEventListener("click", function(e) {
document.getElementById("info-btn").style = "display:none";
document.getElementById("info-div").style = "";
e.preventDefault();
});
document.getElementById("advanced-btn").addEventListener("click", function() {
document.getElementById("advanced-btn").addEventListener("click", function(e) {
document.getElementById("advanced-btn").style = "display:none";
document.getElementById("advanced-div").style = "";
e.preventDefault();
});
document.getElementById("changelog-btn").addEventListener("click", function(e) {
document.getElementById("changelog-btn").style = "display:none";
document.getElementById("changelog-div").style = "";
e.preventDefault();
});
setTimeout(checkForFileOnServer, 10);

Some files were not shown because too many files have changed in this diff Show More