diff --git a/android.html b/android.html index d1d5b400a..9f75c4507 100644 --- a/android.html +++ b/android.html @@ -2,7 +2,7 @@
- + @@ -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); diff --git a/apps/.eslintrc.json b/apps/.eslintrc.json index d656c2555..d5a4bf2a3 100644 --- a/apps/.eslintrc.json +++ b/apps/.eslintrc.json @@ -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", diff --git a/apps/3dclock/metadata.json b/apps/3dclock/metadata.json index 266a1faa6..a32a80a0d 100644 --- a/apps/3dclock/metadata.json +++ b/apps/3dclock/metadata.json @@ -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", diff --git a/apps/90sclk/settings.js b/apps/90sclk/settings.js index 8f97cd317..74241d603 100644 --- a/apps/90sclk/settings.js +++ b/apps/90sclk/settings.js @@ -21,7 +21,6 @@ '< Back': back, 'Full Screen': { value: settings.fullscreen, - format: () => (settings.fullscreen ? 'Yes' : 'No'), onchange: () => { settings.fullscreen = !settings.fullscreen; save(); diff --git a/apps/_example_clkinfo/ChangeLog b/apps/_example_clkinfo/ChangeLog index 4c21f3ace..78ba28f3b 100644 --- a/apps/_example_clkinfo/ChangeLog +++ b/apps/_example_clkinfo/ChangeLog @@ -1 +1 @@ -0.01: New Widget! +0.01: New Clock Info! diff --git a/apps/_example_clock/ChangeLog b/apps/_example_clock/ChangeLog new file mode 100644 index 000000000..09953593e --- /dev/null +++ b/apps/_example_clock/ChangeLog @@ -0,0 +1 @@ +0.01: New Clock! diff --git a/apps/_example_clock/README.md b/apps/_example_clock/README.md new file mode 100644 index 000000000..5d750a965 --- /dev/null +++ b/apps/_example_clock/README.md @@ -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 diff --git a/apps/_example_clock/app-icon.js b/apps/_example_clock/app-icon.js new file mode 100644 index 000000000..49232b838 --- /dev/null +++ b/apps/_example_clock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) diff --git a/apps/_example_clock/clock.js b/apps/_example_clock/clock.js new file mode 100644 index 000000000..7e97cf758 --- /dev/null +++ b/apps/_example_clock/clock.js @@ -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(); \ No newline at end of file diff --git a/apps/_example_clock/icon.png b/apps/_example_clock/icon.png new file mode 100644 index 000000000..582cb2e08 Binary files /dev/null and b/apps/_example_clock/icon.png differ diff --git a/apps/_example_clock/metadata.json b/apps/_example_clock/metadata.json new file mode 100644 index 000000000..c6e1256d3 --- /dev/null +++ b/apps/_example_clock/metadata.json @@ -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} + ] +} diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index d531e43a9..108242825 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -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 diff --git a/apps/android/boot.js b/apps/android/boot.js index 846fc40a8..63f9b2883 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -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); } diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 68bd946c5..5babc520b 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -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", diff --git a/apps/angles/ChangeLog b/apps/angles/ChangeLog new file mode 100644 index 000000000..2286a7f70 --- /dev/null +++ b/apps/angles/ChangeLog @@ -0,0 +1 @@ +0.01: New App! \ No newline at end of file diff --git a/apps/angles/app.js b/apps/angles/app.js new file mode 100644 index 000000000..a07c29199 --- /dev/null +++ b/apps/angles/app.js @@ -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(); + } +}); \ No newline at end of file diff --git a/apps/angles/icon.js b/apps/angles/icon.js new file mode 100644 index 000000000..3f051f95f --- /dev/null +++ b/apps/angles/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cA///ov+5lChWMyGuxdzpdj4/lKf4AUkgQPgm0wAiPy2QCBsBkmS6QRNhIRBrVACJlPu2+pdICBcCrVJlvJtIRLifStMl3MtkARKydUyMkzMl0CMKyWWyUk1MkSJXkyR7BogRLgVcydSrVGzLHKgdLyfSpdE3JYKklqTwNJknJYJVkxcSp+pnygKhMs1OSEQOSYhVJl1bCIbBK5Mq7gRCyARJiVbqyPBCIKMKuVM24yBCIIiJnVOqu5CISMKp9JlvJCIRXKpP3nxoCRhUSBwSMNBwaMMgn6yp6DRhUl0mypiMMgM9ksipaMMhMtCINKRhlJmoRBpJuBCBIRGRhUE5I1CpKMLgmZn5ZDGhUAycnRoNMRhTDCsn3tfkRhLnDTwYQLNgSMMUQkyRhbGEkyMKAApFOAH4AGA")) \ No newline at end of file diff --git a/apps/angles/icon.png b/apps/angles/icon.png new file mode 100644 index 000000000..1a4559d44 Binary files /dev/null and b/apps/angles/icon.png differ diff --git a/apps/angles/metadata.json b/apps/angles/metadata.json new file mode 100644 index 000000000..f8a90a305 --- /dev/null +++ b/apps/angles/metadata.json @@ -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} + ] +} diff --git a/apps/angles/screenshot.png b/apps/angles/screenshot.png new file mode 100644 index 000000000..9d631cf74 Binary files /dev/null and b/apps/angles/screenshot.png differ diff --git a/apps/aviatorclk/.gitignore b/apps/aviatorclk/.gitignore new file mode 100644 index 000000000..bdbc0d22e --- /dev/null +++ b/apps/aviatorclk/.gitignore @@ -0,0 +1 @@ +aviatorclk.json diff --git a/apps/aviatorclk/ChangeLog b/apps/aviatorclk/ChangeLog new file mode 100644 index 000000000..929ee8387 --- /dev/null +++ b/apps/aviatorclk/ChangeLog @@ -0,0 +1,2 @@ +1.00: initial release +1.01: added tap event to scroll METAR and toggle seconds display diff --git a/apps/aviatorclk/README.md b/apps/aviatorclk/README.md new file mode 100644 index 000000000..ac27b80d3 --- /dev/null +++ b/apps/aviatorclk/README.md @@ -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) + diff --git a/apps/aviatorclk/aviatorclk-icon.js b/apps/aviatorclk/aviatorclk-icon.js new file mode 100644 index 000000000..508769a66 --- /dev/null +++ b/apps/aviatorclk/aviatorclk-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg96iIACCqMBCwYABiAWQiUiAAUhDBwWGDCAWHDAYuMCw4ABGBYWKGBYuLGBcBLpAXNFxhIKFxgwCIyhIJC58hC44WNC5B2NPBIXbBYIAHNgIXKCpAYEC5AhBII8SDAQXJMI5EEC6ZREC6EhFwkRO4zuCC46AFAgLYEC4YCBIoaADF4gXEKgYXDVBAcCXxBZDkcyDRAXHmILCif//4GEC5f/PQQWB//zbAX/C5gAKC78BC6K/In4WJ+YXW+QXHMAURl4XJeQYWEGALhBC4q+BYYLbDFwowCkLTCRIyNHGArNBC48SFxIXCMApHDOwQXIJAIQCAAaWCDYJGIDAipGFwQWKDAUSDAnzUoIWMDAcjn/zUgQWOPYYADOZJjKFqIAp")) diff --git a/apps/aviatorclk/aviatorclk.app.js b/apps/aviatorclk/aviatorclk.app.js new file mode 100644 index 000000000..33d671bc7 --- /dev/null +++ b/apps/aviatorclk/aviatorclk.app.js @@ -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 + diff --git a/apps/aviatorclk/aviatorclk.png b/apps/aviatorclk/aviatorclk.png new file mode 100644 index 000000000..af88cfbc4 Binary files /dev/null and b/apps/aviatorclk/aviatorclk.png differ diff --git a/apps/aviatorclk/aviatorclk.settings.js b/apps/aviatorclk/aviatorclk.settings.js new file mode 100644 index 000000000..d3ffbaad2 --- /dev/null +++ b/apps/aviatorclk/aviatorclk.settings.js @@ -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(); + } + }, + }); +}) diff --git a/apps/aviatorclk/metadata.json b/apps/aviatorclk/metadata.json new file mode 100644 index 000000000..9d2b0beef --- /dev/null +++ b/apps/aviatorclk/metadata.json @@ -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" }] +} diff --git a/apps/aviatorclk/screenshot.png b/apps/aviatorclk/screenshot.png new file mode 100644 index 000000000..127946f42 Binary files /dev/null and b/apps/aviatorclk/screenshot.png differ diff --git a/apps/aviatorclk/screenshot2.png b/apps/aviatorclk/screenshot2.png new file mode 100644 index 000000000..e00e2238b Binary files /dev/null and b/apps/aviatorclk/screenshot2.png differ diff --git a/apps/avwx/ChangeLog b/apps/avwx/ChangeLog new file mode 100644 index 000000000..971e5b97e --- /dev/null +++ b/apps/avwx/ChangeLog @@ -0,0 +1 @@ +1.00: initial release diff --git a/apps/avwx/README.md b/apps/avwx/README.md new file mode 100644 index 000000000..a954d118f --- /dev/null +++ b/apps/avwx/README.md @@ -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) + diff --git a/apps/avwx/avwx.js b/apps/avwx/avwx.js new file mode 100644 index 000000000..1a9193b26 --- /dev/null +++ b/apps/avwx/avwx.js @@ -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); +}; + diff --git a/apps/avwx/avwx.png b/apps/avwx/avwx.png new file mode 100644 index 000000000..129c9f9f4 Binary files /dev/null and b/apps/avwx/avwx.png differ diff --git a/apps/avwx/interface.html b/apps/avwx/interface.html new file mode 100644 index 000000000..cdd77cb74 --- /dev/null +++ b/apps/avwx/interface.html @@ -0,0 +1,47 @@ + + + + + + +To use the AVWX API, you need an account and generate an API token. The free "Hobby" plan is sufficient, but please consider supporting the AVWX project.
++ + +
++ +
+ + + + + + + + diff --git a/apps/avwx/metadata.json b/apps/avwx/metadata.json new file mode 100644 index 000000000..0b07f32d4 --- /dev/null +++ b/apps/avwx/metadata.json @@ -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" }] +} diff --git a/apps/banglexercise/settings.js b/apps/banglexercise/settings.js index 3208c6eca..0b52acd72 100644 --- a/apps/banglexercise/settings.js +++ b/apps/banglexercise/settings.js @@ -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); diff --git a/apps/binaryclk/ChangeLog b/apps/binaryclk/ChangeLog index a78cbe479..7b6810faa 100644 --- a/apps/binaryclk/ChangeLog +++ b/apps/binaryclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: Added app 0.02: Removed unneeded squares +0.03: Added settings with fullscreen option diff --git a/apps/binaryclk/app.js b/apps/binaryclk/app.js index 8b030ccff..94c906104 100644 --- a/apps/binaryclk/app.js +++ b/apps/binaryclk/app.js @@ -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(); +} diff --git a/apps/binaryclk/metadata.json b/apps/binaryclk/metadata.json index a81a39b7a..b4ddc6544 100644 --- a/apps/binaryclk/metadata.json +++ b/apps/binaryclk/metadata.json @@ -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"}] } diff --git a/apps/binaryclk/settings.js b/apps/binaryclk/settings.js new file mode 100644 index 000000000..0ef30e19d --- /dev/null +++ b/apps/binaryclk/settings.js @@ -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(); + } + }, + }); +}) diff --git a/apps/bthome/ChangeLog b/apps/bthome/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/bthome/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/bthome/README.md b/apps/bthome/README.md new file mode 100644 index 000000000..d232e8d64 --- /dev/null +++ b/apps/bthome/README.md @@ -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. + diff --git a/apps/bthome/app-icon.js b/apps/bthome/app-icon.js new file mode 100644 index 000000000..ecdc205bc --- /dev/null +++ b/apps/bthome/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA")) \ No newline at end of file diff --git a/apps/bthome/app.js b/apps/bthome/app.js new file mode 100644 index 000000000..7e9a39531 --- /dev/null +++ b/apps/bthome/app.js @@ -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(); + + diff --git a/apps/bthome/boot.js b/apps/bthome/boot.js new file mode 100644 index 000000000..9c02581fe --- /dev/null +++ b/apps/bthome/boot.js @@ -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); +}; diff --git a/apps/bthome/clkinfo.js b/apps/bthome/clkinfo.js new file mode 100644 index 000000000..8698c9828 --- /dev/null +++ b/apps/bthome/clkinfo.js @@ -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! \ No newline at end of file diff --git a/apps/bthome/icon.png b/apps/bthome/icon.png new file mode 100644 index 000000000..091784477 Binary files /dev/null and b/apps/bthome/icon.png differ diff --git a/apps/bthome/metadata.json b/apps/bthome/metadata.json new file mode 100644 index 000000000..c2767b9cf --- /dev/null +++ b/apps/bthome/metadata.json @@ -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"}] +} diff --git a/apps/bthome/settings.js b/apps/bthome/settings.js new file mode 100644 index 000000000..70f50f2ac --- /dev/null +++ b/apps/bthome/settings.js @@ -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(); +}) \ No newline at end of file diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json index 8ffb22c83..fc6804f17 100644 --- a/apps/bthometemp/metadata.json +++ b/apps/bthometemp/metadata.json @@ -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"], diff --git a/apps/bwclk/settings.js b/apps/bwclk/settings.js index 116253fda..8bcf0ae0f 100644 --- a/apps/bwclk/settings.js +++ b/apps/bwclk/settings.js @@ -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(); diff --git a/apps/bwclklite/settings.js b/apps/bwclklite/settings.js index 2d3916a3d..4c59198c6 100644 --- a/apps/bwclklite/settings.js +++ b/apps/bwclklite/settings.js @@ -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(); diff --git a/apps/calculator/app.js b/apps/calculator/app.js index d9a89a989..465291d13 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -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; + } } } diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index 6edb54f65..bd8e6117b 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -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 diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index 7477775ca..5d7cae240 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -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"] = () => { diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index e263efe35..895f8f7aa 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -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"}], diff --git a/apps/chess/ChangeLog b/apps/chess/ChangeLog index 064c7285e..87e526de0 100644 --- a/apps/chess/ChangeLog +++ b/apps/chess/ChangeLog @@ -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 diff --git a/apps/chess/app.js b/apps/chess/app.js index d2141c128..19802083d 100644 --- a/apps/chess/app.js +++ b/apps/chess/app.js @@ -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" }); diff --git a/apps/chess/metadata.json b/apps/chess/metadata.json index d6d6fd70a..94ecbedbd 100644 --- a/apps/chess/metadata.json +++ b/apps/chess/metadata.json @@ -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", diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 63a2b0f93..ae090c1d7 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -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); diff --git a/apps/clicompleteclk/settings.js b/apps/clicompleteclk/settings.js index 2df20ed3e..0213ead6e 100644 --- a/apps/clicompleteclk/settings.js +++ b/apps/clicompleteclk/settings.js @@ -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); diff --git a/apps/colorwheel/app.js b/apps/colorwheel/app.js index 7874c3f54..e8367d329 100644 --- a/apps/colorwheel/app.js +++ b/apps/colorwheel/app.js @@ -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); diff --git a/apps/configurable_clock/app.js b/apps/configurable_clock/app.js index 45c86c7e9..5c3bf3333 100644 --- a/apps/configurable_clock/app.js +++ b/apps/configurable_clock/app.js @@ -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(); diff --git a/apps/dclock/clock-dev.js b/apps/dclock/clock-dev.js index d2c3893d5..914234060 100644 --- a/apps/dclock/clock-dev.js +++ b/apps/dclock/clock-dev.js @@ -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; diff --git a/apps/dragboard/ChangeLog b/apps/dragboard/ChangeLog index 68cd82cfa..77cc63c98 100644 --- a/apps/dragboard/ChangeLog +++ b/apps/dragboard/ChangeLog @@ -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 diff --git a/apps/dragboard/lib.js b/apps/dragboard/lib.js index 78ef11bd4..2e40f3a77 100644 --- a/apps/dragboard/lib.js +++ b/apps/dragboard/lib.js @@ -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; } diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index 090c37a01..c4596d7bd 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -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", diff --git a/apps/drained/ChangeLog b/apps/drained/ChangeLog index c7fd27981..8d196b10d 100644 --- a/apps/drained/ChangeLog +++ b/apps/drained/ChangeLog @@ -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 diff --git a/apps/drained/app.js b/apps/drained/app.js index e27fcb1d1..37cc8c71d 100644 --- a/apps/drained/app.js +++ b/apps/drained/app.js @@ -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; diff --git a/apps/drained/app.ts b/apps/drained/app.ts index f4d33bc44..ed40262fa 100644 --- a/apps/drained/app.ts +++ b/apps/drained/app.ts @@ -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; diff --git a/apps/drained/metadata.json b/apps/drained/metadata.json index 6dfdac78d..ea18f6fcf 100644 --- a/apps/drained/metadata.json +++ b/apps/drained/metadata.json @@ -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": [ diff --git a/apps/espruinoctrl/ChangeLog b/apps/espruinoctrl/ChangeLog index 819ae56cb..522cba63e 100644 --- a/apps/espruinoctrl/ChangeLog +++ b/apps/espruinoctrl/ChangeLog @@ -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 \ No newline at end of file diff --git a/apps/espruinoctrl/README.md b/apps/espruinoctrl/README.md index 7b2e434e7..59c96b0de 100644 --- a/apps/espruinoctrl/README.md +++ b/apps/espruinoctrl/README.md @@ -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. + diff --git a/apps/espruinoctrl/custom.html b/apps/espruinoctrl/custom.html index 2329ad214..27ef1eb53 100644 --- a/apps/espruinoctrl/custom.html +++ b/apps/espruinoctrl/custom.html @@ -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; diff --git a/apps/espruinoctrl/metadata.json b/apps/espruinoctrl/metadata.json index 9308b4a46..4f5fa01c8 100644 --- a/apps/espruinoctrl/metadata.json +++ b/apps/espruinoctrl/metadata.json @@ -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", diff --git a/apps/flightdash/ChangeLog b/apps/flightdash/ChangeLog new file mode 100644 index 000000000..971e5b97e --- /dev/null +++ b/apps/flightdash/ChangeLog @@ -0,0 +1 @@ +1.00: initial release diff --git a/apps/flightdash/README.md b/apps/flightdash/README.md new file mode 100644 index 000000000..07b753178 --- /dev/null +++ b/apps/flightdash/README.md @@ -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) + diff --git a/apps/flightdash/flightdash-icon.js b/apps/flightdash/flightdash-icon.js new file mode 100644 index 000000000..3a2e2757c --- /dev/null +++ b/apps/flightdash/flightdash-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/AHcCkAX/C/4X9kUiC/4XcgczmcwRSArBkYWBAAMyA4KUMC4QWDAAIXOAAUziMTmMRmZdRmQXDkZhIHQJ1IAAYXGBgoNDgQJFLoQhFDQ84wQFDlGDBwxBInGIDAUoxAXFJosDOIIXDAAgXCPoJkGBAKfBmc6C4ujBIINBiYXIEIMK1AWDxWgHoQXMgGqC4eqKoYXHL4QFChQYC1QuBEwbcHZo7hHBpYA/AH4A/AH4")) diff --git a/apps/flightdash/flightdash.app.js b/apps/flightdash/flightdash.app.js new file mode 100644 index 000000000..f612836c6 --- /dev/null +++ b/apps/flightdash/flightdash.app.js @@ -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}); +*/ diff --git a/apps/flightdash/flightdash.png b/apps/flightdash/flightdash.png new file mode 100644 index 000000000..8230bc0c1 Binary files /dev/null and b/apps/flightdash/flightdash.png differ diff --git a/apps/flightdash/flightdash.settings.js b/apps/flightdash/flightdash.settings.js new file mode 100644 index 000000000..6d7de1287 --- /dev/null +++ b/apps/flightdash/flightdash.settings.js @@ -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); +}) diff --git a/apps/flightdash/interface.html b/apps/flightdash/interface.html new file mode 100644 index 000000000..d0f57f316 --- /dev/null +++ b/apps/flightdash/interface.html @@ -0,0 +1,186 @@ + + + + + + + + +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.
+ +The database of airports is based on OurAirports. + +
- or -
++ + +
+- or -
++ + +
+Only 1 of the above filters is applied, with higher up in the list taking precedence.
++ + +
+ ++ +
Recipe to be imported to BangleJs: -
+ + ++ +
+ +This tool allows you to update the firmware on Bangle.js 2 devices +
This tool allows you to update the firmware on Bangle.js 2 devices from within the App Loader.