Merge branch 'master' of github.com:espruino/BangleApps
10
android.html
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
'< Back': back,
|
||||
'Full Screen': {
|
||||
value: settings.fullscreen,
|
||||
format: () => (settings.fullscreen ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
settings.fullscreen = !settings.fullscreen;
|
||||
save();
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.01: New Widget!
|
||||
0.01: New Clock Info!
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New Clock!
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))
|
||||
|
|
@ -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();
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4cA///ov+5lChWMyGuxdzpdj4/lKf4AUkgQPgm0wAiPy2QCBsBkmS6QRNhIRBrVACJlPu2+pdICBcCrVJlvJtIRLifStMl3MtkARKydUyMkzMl0CMKyWWyUk1MkSJXkyR7BogRLgVcydSrVGzLHKgdLyfSpdE3JYKklqTwNJknJYJVkxcSp+pnygKhMs1OSEQOSYhVJl1bCIbBK5Mq7gRCyARJiVbqyPBCIKMKuVM24yBCIIiJnVOqu5CISMKp9JlvJCIRXKpP3nxoCRhUSBwSMNBwaMMgn6yp6DRhUl0mypiMMgM9ksipaMMhMtCINKRhlJmoRBpJuBCBIRGRhUE5I1CpKMLgmZn5ZDGhUAycnRoNMRhTDCsn3tfkRhLnDTwYQLNgSMMUQkyRhbGEkyMKAApFOAH4AGA"))
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -0,0 +1 @@
|
|||
aviatorclk.json
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
1.00: initial release
|
||||
1.01: added tap event to scroll METAR and toggle seconds display
|
||||
|
|
@ -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
|
||||
|
||||

|
||||

|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwg96iIACCqMBCwYABiAWQiUiAAUhDBwWGDCAWHDAYuMCw4ABGBYWKGBYuLGBcBLpAXNFxhIKFxgwCIyhIJC58hC44WNC5B2NPBIXbBYIAHNgIXKCpAYEC5AhBII8SDAQXJMI5EEC6ZREC6EhFwkRO4zuCC46AFAgLYEC4YCBIoaADF4gXEKgYXDVBAcCXxBZDkcyDRAXHmILCif//4GEC5f/PQQWB//zbAX/C5gAKC78BC6K/In4WJ+YXW+QXHMAURl4XJeQYWEGALhBC4q+BYYLbDFwowCkLTCRIyNHGArNBC48SFxIXCMApHDOwQXIJAIQCAAaWCDYJGIDAipGFwQWKDAUSDAnzUoIWMDAcjn/zUgQWOPYYADOZJjKFqIAp"))
|
||||
|
|
@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
@ -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" }]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
|
@ -0,0 +1 @@
|
|||
1.00: initial release
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# AVWX Module
|
||||
|
||||
This is a module/library to use the [AVWX](https://account.avwx.rest/) Aviation
|
||||
Weather API. It doesn't include an app.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
You will need an AVWX account (see above for link) and generate an API token.
|
||||
The free "Hobby" plan is normally sufficient, but please consider supporting
|
||||
the AVWX project.
|
||||
|
||||
After installing the module on your Bangle, use the "interface" page (floppy
|
||||
disk icon) in the App Loader to set the API token.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Include the module in your app with:
|
||||
|
||||
const avwx = require('avwx');
|
||||
|
||||
Then use the exported function, for example to get the "sanitized" METAR from
|
||||
the nearest station to a lat/lon coordinate pair:
|
||||
|
||||
reqID = avwx.request('metar/'+lat+','+lon,
|
||||
'filter=sanitized&onfail=nearest',
|
||||
data => { console.log(data); },
|
||||
error => { console.log(error); });
|
||||
|
||||
The returned reqID can be useful to track whether a request has already been
|
||||
made (ie. the app is still waiting on a response).
|
||||
|
||||
Please consult the [AVWX documentation](https://avwx.docs.apiary.io/) for
|
||||
information about the available end-points and request parameters.
|
||||
|
||||
|
||||
## Author
|
||||
|
||||
Flaparoo [github](https://github.com/flaparoo)
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* AVWX Bangle Module
|
||||
*
|
||||
* AVWX doco: https://avwx.docs.apiary.io/
|
||||
* test AVWX API request with eg.: curl -X GET 'https://avwx.rest/api/metar/43.9844,-88.5570?token=...'
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
const AVWX_BASE_URL = 'https://avwx.rest/api/'; // must end with a slash
|
||||
const AVWX_CONFIG_FILE = 'avwx.json';
|
||||
|
||||
|
||||
// read in the settings
|
||||
var AVWXsettings = Object.assign({
|
||||
AVWXtoken: '',
|
||||
}, require('Storage').readJSON(AVWX_CONFIG_FILE, true) || {});
|
||||
|
||||
|
||||
/**
|
||||
* Make an AVWX API request
|
||||
*
|
||||
* @param {string} requestPath API path (after /api/), eg. 'meta/KOSH'
|
||||
* @param {string} params optional request parameters, eg. 'onfail=nearest' (use '&' in the string to combine multiple params)
|
||||
* @param {function} successCB callback if the API request was successful - will supply the returned data: successCB(data)
|
||||
* @param {function} failCB callback in case the API request failed - will supply the error: failCB(error)
|
||||
*
|
||||
* @returns {number} the HTTP request ID
|
||||
*
|
||||
* Example:
|
||||
* reqID = avwx.request('metar/'+lat+','+lon,
|
||||
* 'filter=sanitized&onfail=nearest',
|
||||
* data => { console.log(data); },
|
||||
* error => { console.log(error); });
|
||||
*
|
||||
*/
|
||||
exports.request = function(requestPath, optParams, successCB, failCB) {
|
||||
if (! AVWXsettings.AVWXtoken) {
|
||||
failCB('No AVWX API Token defined!');
|
||||
return undefined;
|
||||
}
|
||||
let params = 'token='+AVWXsettings.AVWXtoken;
|
||||
if (optParams)
|
||||
params += '&'+optParams;
|
||||
return Bangle.http(AVWX_BASE_URL+requestPath+'?'+params).then(successCB).catch(failCB);
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,47 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>To use the <a href="https://account.avwx.rest/">AVWX</a> API, you need an account and generate an API token. The free "Hobby" plan is sufficient, but please consider supporting the AVWX project.</p>
|
||||
<p>
|
||||
<label class="form-label" for="AVWXtoken">AVWX API Token:</label>
|
||||
<input class="form-input" type="text" id="AVWXtoken" placeholder="Your personal AVWX API Token" />
|
||||
</p>
|
||||
<p>
|
||||
<button id="upload" class="btn btn-primary">Configure</button>
|
||||
</p>
|
||||
|
||||
<p><div id="status"></div></p>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
var AVWXsettings = {};
|
||||
|
||||
function onInit() {
|
||||
// read in existing settings to preserve them during an update
|
||||
try {
|
||||
Util.readStorageJSON('avwx.json', currentSettings => {
|
||||
if (currentSettings) {
|
||||
AVWXsettings = currentSettings;
|
||||
if ('AVWXtoken' in AVWXsettings) {
|
||||
document.getElementById('AVWXtoken').value = AVWXsettings.AVWXtoken;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("Failed to read existing settings: "+e);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
AVWXsettings.AVWXtoken = document.getElementById('AVWXtoken').value;
|
||||
Util.writeStorage('avwx.json', JSON.stringify(AVWXsettings), () => {
|
||||
document.getElementById("status").innerHTML = 'AVWX configuration successfully uploaded to Bangle!';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "avwx",
|
||||
"name": "AVWX Module",
|
||||
"shortName":"AVWX",
|
||||
"version":"1.00",
|
||||
"description": "Module/library for the AVWX API",
|
||||
"icon": "avwx.png",
|
||||
"type": "module",
|
||||
"tags": "outdoors",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"provides_modules": ["avwx"],
|
||||
"readme": "README.md",
|
||||
"interface": "interface.html",
|
||||
"storage": [
|
||||
{ "name":"avwx", "url":"avwx.js" }
|
||||
],
|
||||
"data": [{ "name":"avwx.json" }]
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
0.01: Added app
|
||||
0.02: Removed unneeded squares
|
||||
0.03: Added settings with fullscreen option
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA"))
|
||||
|
|
@ -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();
|
||||
|
||||
|
||||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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!
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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"}]
|
||||
}
|
||||
|
|
@ -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();
|
||||
})
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"] = () => {
|
||||
|
|
|
|||
|
|
@ -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"}],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
1.00: initial release
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Flight Dashboard
|
||||
|
||||
Shows basic flight and navigation instruments.
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4"))
|
||||
|
|
@ -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});
|
||||
*/
|
||||
|
After Width: | Height: | Size: 1020 B |
|
|
@ -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);
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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" }]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
0.01: New App
|
||||
|
|
@ -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".
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEw4UA////e3uUt+cVEjEK0ALJlWqAAv4BYelqoAEBa/61ALRrQDCBY9q1ILCLYQLD0EKHZFawECAgILGFIYvHwAFBgQLGqwLDyoLGSwYLBI4gLFHYojFI4wdCMAJHGtEghEpBY9YkWIkoLNR4oLEHYwLMHYILJAoILIrWq1SzIBZYjE/gXKBYwAEEYwAEC67LGHQIABZY4jWF9FXBZVfBZX/BYmv/4AEBZ8KKIYACwALCACwA=="))
|
||||
|
|
@ -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();
|
||||
|
After Width: | Height: | Size: 931 B |
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 405 KiB |
|
|
@ -1 +1,2 @@
|
|||
0.01: attempt to import
|
||||
0.02: Make it possible for Fastload Utils to fastload into this app.
|
||||
|
|
|
|||
|
|
@ -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$/;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
'< Back': back,
|
||||
'Show Widgets': {
|
||||
value: settings.showWidgets,
|
||||
format: () => (settings.showWidgets ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
settings.showWidgets = !settings.showWidgets;
|
||||
save();
|
||||
|
|
|
|||
|
|
@ -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 += `(⚠ 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);
|
||||
|
||||
|
|
|
|||