diff --git a/apps/alpinenav/app-icon.js b/apps/alpinenav/app-icon.js
index dba084202..6708ee67f 100644
--- a/apps/alpinenav/app-icon.js
+++ b/apps/alpinenav/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A"))
\ No newline at end of file
+require("heatshrink").decompress(atob("mEkgIRO4AFJgPgAocDAoswAocHAokGjAFDhgFFhgFDjEOAoc4gxSE44FDuPjAod//+AAoXfn4FCgPMjJUCmIJBAoU7AoJUCv4CBsACBtwCBuACB4w3CEQIaCKgMBFgQFBgYFCLQMDMIfAg55D4BcDg/gNAcD+B0DSIMcOgiGEjCYEjgFEhhVCUgQ"))
diff --git a/apps/arrow/icon.js b/apps/arrow/icon.js
index 380728484..917a5c979 100644
--- a/apps/arrow/icon.js
+++ b/apps/arrow/icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))
+require("heatshrink").decompress(atob("kkkwIEBgf8AYMB//4AgN///ggEf4E/wED+EACQN8C4Pgh4TBh8BCYMAvEcEoWD4AEBnk4gFggPHwAXBj1wgIwB88An/Ah3gg/+gF+gH/+EH8Ef/+ABAPvuAIBgnyCIQjBBAMAJAIIEuAICFgIIBh14BAMB8eAg0Ajk8KAXBKAU4jwDBg+ADoIXBg4NBnxPBEgPAgP8gZaBg//KoKLBKAIEBMQMAA"))
diff --git a/apps/astral/app-icon.js b/apps/astral/app-icon.js
index 19d0998ff..d10e7a498 100644
--- a/apps/astral/app-icon.js
+++ b/apps/astral/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY="))
\ No newline at end of file
+require("heatshrink").decompress(atob("kUw4MA///xP5gEH/AMBh//4AHBwF4gEDwEHgEB4fw8EAsf/jEAjPh80AhngjnAgcwAIMB5kA50A+cAmfAtnAhnYmc//8zhln/+c4YjBg0w440Bxk38EB/cP/0B//Dwf/+FxwEf8EGIAJGB2BkCnhiB4EPgF//EDFQIpB+HGgOMnkxwFjh8MsEY4YQHn/x//j//8n/wHYItBCAKFBhgKBKAIQBBgIQC4AQCmAQChkD/v8gcA/wCBBoMA7+39kAPwP/WIMP4aYBCAYhCCAkHAYOAA="))
diff --git a/apps/bikespeedo/ChangeLog b/apps/bikespeedo/ChangeLog
new file mode 100644
index 000000000..5fb78710b
--- /dev/null
+++ b/apps/bikespeedo/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/bikespeedo/Hochrad120px.gif b/apps/bikespeedo/Hochrad120px.gif
new file mode 100644
index 000000000..1952cf44f
Binary files /dev/null and b/apps/bikespeedo/Hochrad120px.gif differ
diff --git a/apps/bikespeedo/Hochrad120px.png b/apps/bikespeedo/Hochrad120px.png
new file mode 100644
index 000000000..2c2d4e1ef
Binary files /dev/null and b/apps/bikespeedo/Hochrad120px.png differ
diff --git a/apps/bikespeedo/README.md b/apps/bikespeedo/README.md
new file mode 100644
index 000000000..7d271a022
--- /dev/null
+++ b/apps/bikespeedo/README.md
@@ -0,0 +1,12 @@
+## GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude...
+
+...all taken from internal sources.
+
+#### To speed-up GPS reception it is strongly recommended to upload AGPS data with ["Assisted GPS Update"](https://banglejs.com/apps/?id=assistedgps)
+
+#### If "CALIB!" is shown on the display or the compass heading differs too much from GPS heading, compass calibration should be done with the ["Navigation Compass" App](https://banglejs.com/apps/?id=magnav)
+
+**Credits:**
+Bike Speedometer App by github.com/HilmarSt
+Big parts of the software are based on github.com/espruino/BangleApps/tree/master/apps/speedalt
+Compass and Compass Calibration based on github.com/espruino/BangleApps/tree/master/apps/magnav
diff --git a/apps/bikespeedo/Screenshot.png b/apps/bikespeedo/Screenshot.png
new file mode 100644
index 000000000..fd27728e4
Binary files /dev/null and b/apps/bikespeedo/Screenshot.png differ
diff --git a/apps/bikespeedo/app-icon.js b/apps/bikespeedo/app-icon.js
new file mode 100644
index 000000000..c34f52cfb
--- /dev/null
+++ b/apps/bikespeedo/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+64A/AC+sF1uBgAwsq1W1krGEmswIFDlcAFoMrqyGjlcrGAQDB1guBBQJghKYZZCMYhqBlYugFAesgAuFYgQIHAE2sYMZDfwIABbgIuowMAqwABb4wAjFVQAEqyMrF4cAlYABqwypR4RgBwIyplYnF1hnBGIo8BAAQvhGIj6C1hpBgChBGCqGBqwdCRQQnCB4gJBGAgtWc4WBPoi9JH4ILBGYQATPoRHJRYoACwLFBLi4tGLIyLEA5QuPCoYpEMhBBBGDIuFgArIYQIUHA4b+GABLUBAwoQIXorDGI5RNGCB9WRQ0AJwwHGDxChOH4oDCRI4/GXpAaB1gyLEwlWKgTrBT46ALCogQKZoryFCwzgGBgz/NZpaQHHBCdEF5hKBBxWBUwoGBgEAEoIyHHYesBg7aBJQ7SBBAIvEIIJCBD4IFBgBIGEAcAUA8rGAIWHS4QvDCAJAHG4JfRCgKCFeAovCdRIiBDYq/NABi0Cfo5IEBgjUGACZ6BqwcGwLxBFYRsEHIKBIJwLkBNoIHDF468GYgIBBXY4EDE4IHDYwSwCN4IGBCIp5CJYtWgBZBHAgFEMoRjEE4QDCLYJUEUoaCBPYoQCgA4FGozxFLYwfEQgqrGexIYFBoxbDS4YHCIAYVEEAZcCYwwvGfoQHEcwQHHIg9WIAS9BIoYYESoowIABQuBUgg1DVwwACEpIwBChDLFDQ5JLlZnHJAajBQwgLEO4LDBHKAhBFxQxFCIIACAwadLHgJJBAAUrQJxYFAAbKPCwRGCCqAAm"))
diff --git a/apps/bikespeedo/app.js b/apps/bikespeedo/app.js
new file mode 100644
index 000000000..0c5680c9d
--- /dev/null
+++ b/apps/bikespeedo/app.js
@@ -0,0 +1,546 @@
+// Bike Speedometer by https://github.com/HilmarSt
+// Big parts of this software are based on https://github.com/espruino/BangleApps/tree/master/apps/speedalt
+// Compass and Compass Calibration based on https://github.com/espruino/BangleApps/tree/master/apps/magnav
+
+const BANGLEJS2 = 1;
+const screenH = g.getHeight();
+const screenYstart = 24; // 0..23 for widgets
+const screenY_Half = screenH / 2 + screenYstart;
+const screenW = g.getWidth();
+const screenW_Half = screenW / 2;
+const fontFactorB2 = 2/3;
+const colfg=g.theme.fg, colbg=g.theme.bg;
+const col1=colfg, colUncertain="#88f"; // if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
+
+var altiGPS=0, altiBaro=0;
+var hdngGPS=0, hdngCompass=0, calibrateCompass=false;
+
+/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
+var KalmanFilter = (function () {
+ 'use strict';
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ function _defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ function _createClass(Constructor, protoProps, staticProps) {
+ if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) _defineProperties(Constructor, staticProps);
+ return Constructor;
+ }
+
+ /**
+ * KalmanFilter
+ * @class
+ * @author Wouter Bulten
+ * @see {@link http://github.com/wouterbulten/kalmanjs}
+ * @version Version: 1.0.0-beta
+ * @copyright Copyright 2015-2018 Wouter Bulten
+ * @license MIT License
+ * @preserve
+ */
+ var KalmanFilter =
+ /*#__PURE__*/
+ function () {
+ /**
+ * Create 1-dimensional kalman filter
+ * @param {Number} options.R Process noise
+ * @param {Number} options.Q Measurement noise
+ * @param {Number} options.A State vector
+ * @param {Number} options.B Control vector
+ * @param {Number} options.C Measurement vector
+ * @return {KalmanFilter}
+ */
+ function KalmanFilter() {
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
+ _ref$R = _ref.R,
+ R = _ref$R === void 0 ? 1 : _ref$R,
+ _ref$Q = _ref.Q,
+ Q = _ref$Q === void 0 ? 1 : _ref$Q,
+ _ref$A = _ref.A,
+ A = _ref$A === void 0 ? 1 : _ref$A,
+ _ref$B = _ref.B,
+ B = _ref$B === void 0 ? 0 : _ref$B,
+ _ref$C = _ref.C,
+ C = _ref$C === void 0 ? 1 : _ref$C;
+
+ _classCallCheck(this, KalmanFilter);
+
+ this.R = R; // noise power desirable
+
+ this.Q = Q; // noise power estimated
+
+ this.A = A;
+ this.C = C;
+ this.B = B;
+ this.cov = NaN;
+ this.x = NaN; // estimated signal without noise
+ }
+ /**
+ * Filter a new value
+ * @param {Number} z Measurement
+ * @param {Number} u Control
+ * @return {Number}
+ */
+
+
+ _createClass(KalmanFilter, [{
+ key: "filter",
+ value: function filter(z) {
+ var u = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
+
+ if (isNaN(this.x)) {
+ this.x = 1 / this.C * z;
+ this.cov = 1 / this.C * this.Q * (1 / this.C);
+ } else {
+ // Compute prediction
+ var predX = this.predict(u);
+ var predCov = this.uncertainty(); // Kalman gain
+
+ var K = predCov * this.C * (1 / (this.C * predCov * this.C + this.Q)); // Correction
+
+ this.x = predX + K * (z - this.C * predX);
+ this.cov = predCov - K * this.C * predCov;
+ }
+
+ return this.x;
+ }
+ /**
+ * Predict next value
+ * @param {Number} [u] Control
+ * @return {Number}
+ */
+
+ }, {
+ key: "predict",
+ value: function predict() {
+ var u = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
+ return this.A * this.x + this.B * u;
+ }
+ /**
+ * Return uncertainty of filter
+ * @return {Number}
+ */
+
+ }, {
+ key: "uncertainty",
+ value: function uncertainty() {
+ return this.A * this.cov * this.A + this.R;
+ }
+ /**
+ * Return the last filtered measurement
+ * @return {Number}
+ */
+
+ }, {
+ key: "lastMeasurement",
+ value: function lastMeasurement() {
+ return this.x;
+ }
+ /**
+ * Set measurement noise Q
+ * @param {Number} noise
+ */
+
+ }, {
+ key: "setMeasurementNoise",
+ value: function setMeasurementNoise(noise) {
+ this.Q = noise;
+ }
+ /**
+ * Set the process noise R
+ * @param {Number} noise
+ */
+
+ }, {
+ key: "setProcessNoise",
+ value: function setProcessNoise(noise) {
+ this.R = noise;
+ }
+ }]);
+
+ return KalmanFilter;
+ }();
+
+ return KalmanFilter;
+
+}());
+
+
+//==================================== MAIN ====================================
+
+var lf = {fix:0,satellites:0};
+var showMax = 0; // 1 = display the max values. 0 = display the cur fix
+var canDraw = 1;
+var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
+var sec; // actual seconds for testing purposes
+
+var max = {};
+max.spd = 0;
+max.alt = 0;
+max.n = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
+
+var emulator = (process.env.BOARD=="EMSCRIPTEN" || process.env.BOARD=="EMSCRIPTEN2")?1:0; // 1 = running in emulator. Supplies test values;
+
+var wp = {}; // Waypoint to use for distance from cur position.
+var SATinView = 0;
+
+function radians(a) {
+ return a*Math.PI/180;
+}
+
+function distance(a,b){
+ var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
+ var y = radians(b.lat-a.lat);
+
+ // Distance in selected units
+ var d = Math.sqrt(x*x + y*y) * 6371000;
+ d = (d/parseFloat(cfg.dist)).toFixed(2);
+ if ( d >= 100 ) d = parseFloat(d).toFixed(1);
+ if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
+
+ return d;
+}
+
+function drawFix(dat) {
+
+ if (!canDraw) return;
+
+ g.clearRect(0,screenYstart,screenW,screenH);
+
+ var v = '';
+ var u='';
+
+ // Primary Display
+ v = (cfg.primSpd)?dat.speed.toString():dat.alt.toString();
+
+ // Primary Units
+ u = (cfg.primSpd)?cfg.spd_unit:dat.alt_units;
+
+ drawPrimary(v,u);
+
+ // Secondary Display
+ v = (cfg.primSpd)?dat.alt.toString():dat.speed.toString();
+
+ // Secondary Units
+ u = (cfg.primSpd)?dat.alt_units:cfg.spd_unit;
+
+ drawSecondary(v,u);
+
+ // Time
+ drawTime();
+
+ //Sats
+ if ( dat.age > 10 ) {
+ if ( dat.age > 90 ) dat.age = '>90';
+ drawSats('Age:'+dat.age);
+ }
+ else if (!BANGLEJS2) {
+ drawSats('Sats:'+dat.sats);
+ } else {
+ if (lf.fix) {
+ drawSats('Sats:'+dat.sats);
+ } else {
+ drawSats('View:' + SATinView);
+ }
+ }
+ g.reset();
+}
+
+
+function drawClock() {
+ if (!canDraw) return;
+ g.clearRect(0,screenYstart,screenW,screenH);
+ drawTime();
+ g.reset();
+}
+
+
+function drawPrimary(n,u) {
+ //if(emulator)console.log("\n1: " + n +" "+ u);
+ var s=40; // Font size
+ var l=n.length;
+
+ if ( l <= 7 ) s=48;
+ if ( l <= 6 ) s=55;
+ if ( l <= 5 ) s=66;
+ if ( l <= 4 ) s=85;
+ if ( l <= 3 ) s=110;
+
+ // X -1=left (default), 0=center, 1=right
+ // Y -1=top (default), 0=center, 1=bottom
+ g.setFontAlign(0,-1); // center, top
+ if (lf.fix) g.setColor(col1); else g.setColor(colUncertain);
+ if (BANGLEJS2) s *= fontFactorB2;
+ g.setFontVector(s);
+ g.drawString(n, screenW_Half - 10, screenYstart);
+
+ // Primary Units
+ s = 35; // Font size
+ g.setFontAlign(1,-1,3); // right, top, rotate
+ g.setColor(col1);
+ if (BANGLEJS2) s = 20;
+ g.setFontVector(s);
+ g.drawString(u, screenW - 20, screenYstart + 2);
+}
+
+
+function drawSecondary(n,u) {
+ //if(emulator)console.log("2: " + n +" "+ u);
+
+ if (calibrateCompass) hdngCompass = "CALIB!";
+ else hdngCompass +="°";
+
+ g.setFontAlign(0,1);
+ g.setColor(col1);
+
+ g.setFontVector(12).drawString("Altitude GPS / Barometer", screenW_Half - 5, screenY_Half - 10);
+ g.setFontVector(20);
+ g.drawString(n+" "+u+" / "+altiBaro+" "+u, screenW_Half, screenY_Half + 11);
+
+ g.setFontVector(12).drawString("Heading GPS / Compass", screenW_Half - 10, screenY_Half + 26);
+ g.setFontVector(20);
+ g.drawString(hdngGPS+"° / "+hdngCompass, screenW_Half, screenY_Half + 47);
+}
+
+
+function drawTime() {
+ var x = 0, y = screenH;
+ g.setFontAlign(-1,1); // left, bottom
+ g.setFont("6x8", 2);
+
+ g.setColor(colbg);
+ g.drawString(time,x+1,y); // clear old time
+
+ time = require("locale").time(new Date(),1);
+
+ g.setColor(colfg); // draw new time
+ g.drawString(time,x+2,y);
+}
+
+
+function drawSats(sats) {
+
+ g.setColor(col1);
+ g.setFont("6x8", 2);
+ g.setFontAlign(1,1); //right, bottom
+ g.drawString(sats,screenW,screenH);
+
+ g.setFontVector(18);
+ g.setColor(col1);
+
+ if ( cfg.modeA == 1 ) {
+ if ( showMax ) {
+ g.setFontAlign(0,1); //centre, bottom
+ g.drawString('MAX',120,164);
+ }
+ }
+}
+
+function onGPS(fix) {
+
+ if ( emulator ) {
+ fix.fix = 1;
+ fix.speed = Math.random()*30; // calmed by Kalman filter if cfg.spdFilt
+ fix.alt = Math.random()*200 -20; // calmed by Kalman filter if cfg.altFilt
+ fix.lat = 50.59; // google.de/maps/@50.59,8.53,17z
+ fix.lon = 8.53;
+ fix.course = 365;
+ fix.satellites = sec;
+ fix.time = new Date();
+ fix.smoothed = 0;
+ }
+
+ var m;
+
+ var sp = '---';
+ var al = '---';
+ var di = '---';
+ var age = '---';
+
+ if (fix.fix) lf = fix;
+
+ hdngGPS = lf.course;
+ if (isNaN(hdngGPS)) hdngGPS = "---";
+ else if (0 == hdngGPS) hdngGPS = "0?";
+ else hdngGPS = hdngGPS.toFixed(0);
+
+ if (emulator) hdngCompass = hdngGPS;
+ if (emulator) altiBaro = lf.alt.toFixed(0);
+
+ if (lf.fix) {
+
+ if (BANGLEJS2 && !emulator) Bangle.removeListener('GPS-raw', onGPSraw);
+
+ // Smooth data
+ if ( lf.smoothed !== 1 ) {
+ if ( cfg.spdFilt ) lf.speed = spdFilter.filter(lf.speed);
+ if ( cfg.altFilt ) lf.alt = altFilter.filter(lf.alt);
+ lf.smoothed = 1;
+ if ( max.n <= 15 ) max.n++;
+ }
+
+
+ // Speed
+ if ( cfg.spd == 0 ) {
+ m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
+ sp = parseFloat(m[1]);
+ cfg.spd_unit = m[2];
+ }
+ else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
+
+ if ( sp < 10 ) sp = sp.toFixed(1);
+ else sp = Math.round(sp);
+ if (parseFloat(sp) > parseFloat(max.spd) && max.n > 15 ) max.spd = parseFloat(sp);
+
+ // Altitude
+ al = lf.alt;
+ al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
+ if (parseFloat(al) > parseFloat(max.alt) && max.n > 15 ) max.alt = parseFloat(al);
+
+ // Distance to waypoint
+ di = distance(lf,wp);
+ if (isNaN(di)) di = 0;
+
+ // Age of last fix (secs)
+ age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
+ }
+
+ if ( cfg.modeA == 1 ) {
+ if ( showMax )
+ drawFix({
+ speed:max.spd,
+ sats:lf.satellites,
+ alt:max.alt,
+ alt_units:cfg.alt_unit,
+ age:age,
+ fix:lf.fix
+ }); // Speed and alt maximums
+ else
+ drawFix({
+ speed:sp,
+ sats:lf.satellites,
+ alt:al,
+ alt_units:cfg.alt_unit,
+ age:age,
+ fix:lf.fix
+ }); // Show speed/altitude
+ }
+}
+
+function setButtons(){
+ setWatch(_=>load(), BTN1);
+
+onGPS(lf);
+}
+
+
+function updateClock() {
+ if (!canDraw) return;
+ drawTime();
+ g.reset();
+
+ if ( emulator ) {
+ max.spd++; max.alt++;
+ d=new Date(); sec=d.getSeconds();
+ onGPS(lf);
+ }
+}
+
+
+
+//###
+let cfg = {};
+cfg.spd = 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
+cfg.spd_unit = 'km/h'; // Displayed speed unit
+cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
+cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
+cfg.dist = 1000; // Multiplier for distnce unit conversions.
+cfg.dist_unit = 'km'; // Displayed distnce units
+cfg.modeA = 1;
+cfg.primSpd = 1; // 1 = Spd in primary, 0 = Spd in secondary
+
+cfg.spdFilt = false;
+cfg.altFilt = false;
+
+if ( cfg.spdFilt ) var spdFilter = new KalmanFilter({R: 0.1 , Q: 1 });
+if ( cfg.altFilt ) var altFilter = new KalmanFilter({R: 0.01, Q: 2 });
+
+function onGPSraw(nmea) {
+ var nofGP = 0, nofBD = 0, nofGL = 0;
+ if (nmea.slice(3,6) == "GSV") {
+ // console.log(nmea.slice(1,3) + " " + nmea.slice(11,13));
+ if (nmea.slice(0,7) == "$GPGSV,") nofGP = Number(nmea.slice(11,13));
+ if (nmea.slice(0,7) == "$BDGSV,") nofBD = Number(nmea.slice(11,13));
+ if (nmea.slice(0,7) == "$GLGSV,") nofGL = Number(nmea.slice(11,13));
+ SATinView = nofGP + nofBD + nofGL;
+ } }
+if(BANGLEJS2) Bangle.on('GPS-raw', onGPSraw);
+
+function onPressure(dat) { altiBaro = dat.altitude.toFixed(0); }
+
+Bangle.setBarometerPower(1); // needs some time...
+g.clearRect(0,screenYstart,screenW,screenH);
+onGPS(lf);
+Bangle.setGPSPower(1);
+Bangle.on('GPS', onGPS);
+Bangle.on('pressure', onPressure);
+
+Bangle.setCompassPower(1);
+var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
+if (!CALIBDATA) calibrateCompass = true;
+function Compass_tiltfixread(O,S){
+ "ram";
+ //console.log(O.x+" "+O.y+" "+O.z);
+ var m = Bangle.getCompass();
+ var g = Bangle.getAccel();
+ m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
+ var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
+ if (d<0) d+=360;
+ var phi = Math.atan(-g.x/-g.z);
+ var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
+ var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
+ var costheta = Math.cos(theta), sintheta = Math.sin(theta);
+ var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
+ var yh = m.dz*sinphi - m.dx*cosphi;
+ var psi = Math.atan2(yh,xh)*180/Math.PI;
+ if (psi<0) psi+=360;
+ return psi;
+}
+var Compass_heading = 0;
+function Compass_newHeading(m,h){
+ var s = Math.abs(m - h);
+ var delta = (m>h)?1:-1;
+ if (s>=180){s=360-s; delta = -delta;}
+ if (s<2) return h;
+ var hd = h + delta*(1 + Math.round(s/5));
+ if (hd<0) hd+=360;
+ if (hd>360)hd-= 360;
+ return hd;
+}
+function Compass_reading() {
+ "ram";
+ var d = Compass_tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
+ Compass_heading = Compass_newHeading(d,Compass_heading);
+ hdngCompass = Compass_heading.toFixed(0);
+}
+if (!calibrateCompass) setInterval(Compass_reading,200);
+
+setButtons();
+if (emulator) setInterval(updateClock, 2000);
+else setInterval(updateClock, 10000);
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
diff --git a/apps/bikespeedo/app.png b/apps/bikespeedo/app.png
new file mode 100644
index 000000000..50f242b47
Binary files /dev/null and b/apps/bikespeedo/app.png differ
diff --git a/apps/bikespeedo/metadata.json b/apps/bikespeedo/metadata.json
new file mode 100644
index 000000000..7dea28649
--- /dev/null
+++ b/apps/bikespeedo/metadata.json
@@ -0,0 +1,18 @@
+{
+ "id": "bikespeedo",
+ "name": "Bike Speedometer (beta)",
+ "shortName": "Bike Speedomet.",
+ "version": "0.01",
+ "description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
+ "icon": "app.png",
+ "screenshots": [{"url":"Screenshot.png"}],
+ "type": "app",
+ "tags": "tool,cycling,bicycle,outdoors,sport",
+ "supports": ["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"bikespeedo.app.js","url":"app.js"},
+ {"name":"bikespeedo.img","url":"app-icon.js","evaluate":true}
+ ]
+}
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index e75c9cc7b..ef437fe3b 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -47,3 +47,5 @@
0.41: Add Keyboard and Mouse Bluetooth HID option
0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname..boot.js
0.43: Fix Gadgetbridge handling with Programmable:off
+0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM)
+0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js
index c9bd89f51..c2ed5458d 100644
--- a/apps/boot/bootupdate.js
+++ b/apps/boot/bootupdate.js
@@ -4,7 +4,7 @@ of the time. */
E.showMessage("Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
-var boot = "";
+var boot = "", bootPost = "";
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
@@ -15,6 +15,7 @@ if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't chang
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
boot += `E.setFlags({pretokenise:1});\n`;
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
+bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
if (s.ble!==false) {
if (s.HID) { // Human interface device
if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`;
@@ -195,8 +196,8 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
-var getPriority = /.*\.(\d+)\.boot\.js$/;
-require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
+var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
+ var getPriority = /.*\.(\d+)\.boot\.js$/;
var aPriority = a.match(getPriority);
var bPriority = b.match(getPriority);
if (aPriority && bPriority){
@@ -207,17 +208,39 @@ require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
return 1;
}
return a==b ? 0 : (a>b ? 1 : -1);
-}).forEach(bootFile=>{
+});
+// precalculate file size
+var fileSize = boot.length + bootPost.length;
+bootFiles.forEach(bootFile=>{
+ // match the size of data we're adding below in bootFiles.forEach
+ fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2;
+});
+// write file in chunks (so as not to use up all RAM)
+require('Storage').write('.boot0',boot,0,fileSize);
+var fileOffset = boot.length;
+bootFiles.forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error!
- boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
+ // we write:
+ // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
+ // but we need to do this without ever loading everything into RAM as some
+ // boot files seem to be getting pretty big now.
+ require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
+ fileOffset+=2+bootFile.length+1;
+ var bf = require('Storage').read(bootFile);
+ require('Storage').write('.boot0',bf,fileOffset);
+ fileOffset+=bf.length;
+ require('Storage').write('.boot0',";\n",fileOffset);
+ fileOffset+=2;
});
-// update ble
-boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`;
-// write file
-require('Storage').write('.boot0',boot);
+require('Storage').write('.boot0',bootPost,fileOffset);
+
delete boot;
+delete bootPost;
+delete bootFiles;
+delete fileSize;
+delete fileOffset;
E.showMessage("Reloading...");
eval(require('Storage').read('.boot0'));
// .bootcde should be run automatically after if required, since
diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json
index edddc6b41..c21ab6833 100644
--- a/apps/boot/metadata.json
+++ b/apps/boot/metadata.json
@@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
- "version": "0.43",
+ "version": "0.45",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
diff --git a/apps/bordle/README.md b/apps/bordle/README.md
new file mode 100644
index 000000000..f15f1e6fa
--- /dev/null
+++ b/apps/bordle/README.md
@@ -0,0 +1,17 @@
+# Bordle
+
+The Bangle version of a popular word guessing game. The goal is to guess a 5 letter word in 6 tries or less. After each guess, the letters in the guess are
+marked in colors: yellow for a letter that appears in the to-be-guessed word, but in a different location and green for a letter in the correct position.
+
+Only words contained in the internal dictionary are allowed as valid guesses. At app launch, a target word is picked from the dictionary at random.
+
+On startup, a grid of 6 lines with 5 (empty) letter boxes is displayed. Swiping left or right at any time switches between grid view and keyboard view.
+The keyboad was inspired by the 'Scribble' app (it is a simplified version using the layout library). The letter group "Z ..." contains the delete key and
+the enter key. Hitting enter after the 5th letter will add the guess to the grid view and color mark it.
+
+The (English language) dictionary was derived from the the Unix ispell word list by filtering out plurals and past particples (and some hand editing) from all 5 letter words.
+It is contained in the file 'wordlencr.txt' which contains one long string (no newline characters) of all the words concatenated. It would not be too difficult to swap it
+out for a different language version. The keyboard currently only supports the 26 characters of the latin alphabet (no accents or umlauts).
+
+
+
diff --git a/apps/bordle/app-icon.js b/apps/bordle/app-icon.js
new file mode 100644
index 000000000..64ccbc8a5
--- /dev/null
+++ b/apps/bordle/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AA/TADwoIFkYyOF0owIF04wGUSqvVBZQtZGJYJIFzomKF0onIF07EKF0owLF9wNEnwACE6oZILxovbMBov/F/4v/C54uWF/4vKBQQLLF/4YPFwYMLF7AZGF5Y5KF5xJIFwoMJD44vaBhwvcLQpgHF8gGRF6xYNBpQvTXBoNOF65QJBIgvjBywvUV5YOOF64OIB54v/cQwAKB5ov/F84wKADYuIF+AwkFIwwnE45hmExCSlEpTEiERr3KADw+PF0ownUSoseA=="))
diff --git a/apps/bordle/app.png b/apps/bordle/app.png
new file mode 100644
index 000000000..633a83e4e
Binary files /dev/null and b/apps/bordle/app.png differ
diff --git a/apps/bordle/bordle.app.js b/apps/bordle/bordle.app.js
new file mode 100644
index 000000000..b1d771877
--- /dev/null
+++ b/apps/bordle/bordle.app.js
@@ -0,0 +1,159 @@
+var Layout = require("Layout");
+
+var gameState = 0;
+var keyState = 0;
+var keyStateIdx = 0;
+
+function buttonPushed(b) {
+ if (keyState==0) {
+ keyState++;
+ keyStateIdx = b;
+ if (b<6) {
+ for (i=1; i<=5; ++i) {
+ var c = String.fromCharCode(i+64+(b-1)*5);
+ layout["bt"+i.toString()].label = c;
+ layout["bt"+i.toString()].bgCol = wordle.keyColors[c]||g.theme.bg;
+ }
+ layout.bt6.label = "<";
+ }
+ else {
+ layout.bt1.label = "Z";
+ layout.bt1.bgCol = wordle.keyColors.Z||g.theme.bg;
+ layout.bt2.label = "";
+ layout.bt4.label = "";
+ layout.bt3.label = layout.bt5.label = " ";
+ layout.bt6.label = "<";
+ }
+ }
+ else { // actual button pushed
+ inp = layout.input.label;
+ if (b!=6) {
+ if ((keyStateIdx<=5 || b<=1) && inp.length<5) inp += String.fromCharCode(b+(keyStateIdx-1)*5+64);
+ else if (layout.input.label.length>0 && b==2) inp = inp.slice(0,-1);
+ layout.input.label = inp;
+ }
+ layout = getKeyLayout(inp);
+ keyState = 0;
+ if (inp.length==5 && keyStateIdx==6 && b==4) {
+ rc = wordle.addGuess(inp);
+ layout.input.label = "";
+ layout.update();
+ gameState = 0;
+ if (rc>0) return;
+ g.clear();
+ wordle.render();
+ return;
+ }
+ }
+ layout.update();
+ g.clear();
+ layout.render();
+}
+
+function getKeyLayout(text) {
+ return new Layout( {
+ type: "v", c: [
+ {type:"txt", font:"6x8:2", id:"input", label:text, pad: 3},
+ {type: "h", c: [
+ {type:"btn", font:"6x8:2", id:"bt1", label:"ABCDE", cb: l=>buttonPushed(1), pad:4, filly:1, fillx:1 },
+ {type:"btn", font:"6x8:2", id:"bt2", label:"FGHIJ", cb: l=>buttonPushed(2), pad:4, filly:1, fillx:1 },
+ ]},
+ {type: "h", c: [
+ {type:"btn", font:"6x8:2", id:"bt3", label:"KLMNO", cb: l=>buttonPushed(3), pad:4, filly:1, fillx:1 },
+ {type:"btn", font:"6x8:2", id:"bt4", label:"PQRST", cb: l=>buttonPushed(4), pad:4, filly:1, fillx:1 },
+ ]},
+ {type: "h", c: [
+ {type:"btn", font:"6x8:2", id:"bt5", label:"UVWXY", cb: l=>buttonPushed(5), pad:4, filly:1, fillx:1 },
+ {type:"btn", font:"6x8:2", id:"bt6", label:"Z ...", cb: l=>buttonPushed(6), pad:4, filly:1, fillx:1 },
+ ]}
+ ]});
+}
+
+class Wordle {
+ constructor(word) {
+ this.word = word;
+ this.guesses = [];
+ this.guessColors = [];
+ this.keyColors = [];
+ this.nGuesses = -1;
+ if (word == "rnd") {
+ this.words = require("Storage").read("wordlencr.txt");
+ i = Math.floor(Math.floor(this.words.length/5)*Math.random())*5;
+ this.word = this.words.slice(i, i+5).toUpperCase();
+ }
+ console.log(this.word);
+ }
+ render(clear) {
+ h = g.getHeight();
+ bh = Math.floor(h/6);
+ bbh = Math.floor(0.85*bh);
+ w = g.getWidth();
+ bw = Math.floor(w/5);
+ bbw = Math.floor(0.85*bw);
+ if (clear) g.clear();
+ g.setFont("Vector", Math.floor(bbh*0.95)).setFontAlign(0,0);
+ g.setColor(g.theme.fg);
+ for (i=0; i<6; ++i) {
+ for (j=0; j<5; ++j) {
+ if (i<=this.nGuesses) {
+ g.setColor(this.guessColors[i][j]).fillRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
+ g.setColor(g.theme.fg).drawString(this.guesses[i][j], 2+j*bw+bw/2, 2+i*bh+bh/2);
+ }
+ g.setColor(g.theme.fg).drawRect(j*bw+(bw-bbw)/2, i*bh+(bh-bbh)/2, (j+1)*bw-(bw-bbw)/2, (i+1)*bh-(bh-bbh)/2);
+ }
+ }
+ }
+ addGuess(w) {
+ if ((this.words.indexOf(w.toLowerCase())%5)!=0) {
+ E.showAlert(w+"\nis not a word", "Invalid word").then(function() {
+ layout = getKeyLayout("");
+ wordle.render(true);
+ });
+ return 3;
+ }
+ this.guesses.push(w);
+ this.nGuesses++;
+ this.guessColors.push([]);
+ correct = 0;
+ var sol = this.word;
+ for (i=0; i n > dt)-1;
- let year = baseYear+parseInt(index/12);
- let month = index % 12;
- let day = parseInt((dt-sequence[index])/86400000);
- let colour = dateColours[day % 6];
- if(day==30){ colour=dateColours[6]; }
- return({"year":year,"month":month,"day":day,"colour":colour});
+ let index = sequence.findIndex(n => n > dt)-1;
+ let year = baseYear+parseInt(index/12);
+ let month = index % 12;
+ let day = parseInt((dt-sequence[index])/86400000);
+ let colour = dateColours[day % 6];
+ if(day==30){ colour=dateColours[6]; }
+ return({"year":year,"month":month,"day":day,"colour":colour});
}
function toggleTimeDigits(){
- addTimeDigit = !addTimeDigit;
- modeTime();
+ addTimeDigit = !addTimeDigit;
+ modeTime();
}
function toggleDateFormat(){
- dateFormat = !dateFormat;
- modeTime();
+ dateFormat = !dateFormat;
+ modeTime();
}
function formatDate(res,dateFormat){
- let yyyy = res.year.toString(12);
- calenDef = calen10;
- if(!dateFormat){ //ordinal format
- let mm = ("0"+(res.month+1).toString(12)).substr(-2);
- let dd = ("0"+(res.day+1).toString(12)).substr(-2);
- if(res.day==30){
- calenDef = calen7;
- let m = ((res.month+1).toString(12)).substr(-2);
- return(yyyy+"-"+"S"+m); // ordinal format
- }
- return(yyyy+"-"+mm+"-"+dd);
- }
- let m = res.month.toString(12); // cardinal format
- let w = parseInt(res.day/6);
- let d = res.day%6;
- //return(yyyy+"-"+res.month+"-"+w+"-"+d);
- return(yyyy+"-"+m+"-"+w+"-"+d);
+ let yyyy = res.year.toString(12);
+ calenDef = calen10;
+ if(!dateFormat){ //ordinal format
+ let mm = ("0"+(res.month+1).toString(12)).substr(-2);
+ let dd = ("0"+(res.day+1).toString(12)).substr(-2);
+ if(res.day==30){
+ calenDef = calen7;
+ let m = ((res.month+1).toString(12)).substr(-2);
+ return(yyyy+"-"+"S"+m); // ordinal format
+ }
+ return(yyyy+"-"+mm+"-"+dd);
+ }
+ let m = res.month.toString(12); // cardinal format
+ let w = parseInt(res.day/6);
+ let d = res.day%6;
+ //return(yyyy+"-"+res.month+"-"+w+"-"+d);
+ return(yyyy+"-"+m+"-"+w+"-"+d);
}
function writeDozTime(text,def){
- let pts = def.pts;
- let x=def.pt0[0];
- let y=def.pt0[1];
- g_t.clear();
+ let pts = def.pts;
+ let x=def.pt0[0];
+ let y=def.pt0[1];
+ g_t.clear();
g_t.setFont("Vector",def.size);
- for(let i in text){
- if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new
- else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new
- else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
- x = x+def.step[0];
- y = y+def.step[1];
- }
+ for(let i in text){
+ if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); }
+ else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); }
+ else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); }
+ x = x+def.step[0];
+ y = y+def.step[1];
+ }
}
function writeDozDate(text,def,colour){
-
- dateColour = colour;
- let pts = def.pts;
- let x=def.pt0[0];
- let y=def.pt0[1];
- g_d.clear();
- g_d.setFont("Vector",def.size);
- for(let i in text){
- if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new
- else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new
- else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
- x = x+def.step[0];
- y = y+def.step[1];
- }
+
+ dateColour = colour;
+ let pts = def.pts;
+ let x=def.pt0[0];
+ let y=def.pt0[1];
+ g_d.clear();
+ g_d.setFont("Vector",def.size);
+ for(let i in text){
+ if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); }
+ else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); }
+ else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); }
+ x = x+def.step[0];
+ y = y+def.step[1];
+ }
}
+Bangle.loadWidgets();
+//for malaire's Adjust Clock widget, if used
+function adjustedNow() {
+ return WIDGETS.adjust ? new Date(WIDGETS.adjust.now()) : new Date();
+}
+Bangle.drawWidgets();
+
// Functions for time mode
function drawTime()
{
- let dt = new Date();
- let date = "";
- let timeDef;
- let x = 0;
- dt.setDate(dt.getDate());
- if(addTimeDigit){
- x =
- 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds();
- let msg = "00000"+Math.floor(x).toString(12);
- let time = msg.substr(-5,3)+"."+msg.substr(-2);
- let wait = 347*(1-(x%1));
- timeDef = time6;
- } else {
- x =
- 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds();
- let msg = "0000"+Math.floor(x).toString(12);
- let time = msg.substr(-4,3)+"."+msg.substr(-1);
- let wait = 4167*(1-(x%1));
- timeDef = time5;
- }
- if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day
- date = formatDate(res,dateFormat);
- if(dt x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day
+ date = formatDate(res,dateFormat);
+ if(dt2200)) {
- } else {
- // We have a GPS time. Set time
- setTime(g.time.getTime()/1000);
- }
- });
- Bangle.setGPSPower(1,"time");
- setTimeout(fixTime, 10*60*1000); // every 10 minutes
-}
-// Start time fixing with GPS on next 10 minute interval
-setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000);
diff --git a/apps/doztime/metadata.json b/apps/doztime/metadata.json
index d206cb0c3..6933487ab 100644
--- a/apps/doztime/metadata.json
+++ b/apps/doztime/metadata.json
@@ -2,7 +2,7 @@
"id": "doztime",
"name": "Dozenal Time",
"shortName": "Dozenal Time",
- "version": "0.04",
+ "version": "0.05",
"description": "A dozenal Holocene calendar and dozenal diurnal clock",
"icon": "app.png",
"type": "clock",
diff --git a/apps/game1024/Changelog b/apps/game1024/Changelog
index 3d040bc08..dd4ee3149 100644
--- a/apps/game1024/Changelog
+++ b/apps/game1024/Changelog
@@ -1,2 +1,8 @@
+<<<<<<< HEAD
0.01: Initial version
-0.02: Temporary intermediate version, bug fix, added descriptions
\ No newline at end of file
+0.02: Temporary intermediate version, bug fix, added descriptions
+=======
+0.01: New App!
+0.02: Temporary intermediate version
+0.03: Basic colors
+>>>>>>> 49f2dae36dd66a567153baa1544cdc5d318b024e
diff --git a/apps/game1024/app-icon.js b/apps/game1024/app-icon.js
index 2a9df2fa3..3af366bc9 100644
--- a/apps/game1024/app-icon.js
+++ b/apps/game1024/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwwg96xAACCqMICwYABwAsSAAoWWDBhEGJSAWLGBQuMGBQWNGBAuH/84GBoNGx///AwMFxH/mQYHGAgXHn//GIIXLIo84nAxBwZIKFo4YBMAM/PQwXJCwIABn8y//zC5BeFwYXDmaSCGApgCC4oRBAAQ0CwYwFC5BeB/GCkY0B+YHBC452H+YqBSYRgJUw7tCAYIZCC6ApBU4TZHC44rCJAJgCR4wXLIgMzC4MzF55ICSYUzC6bZDPgIXOJAYACLwwXCa4oXHBgzvIJAzVGC7ZgHwXdAAXSLxAXIpvUkUilvdC6Pd7ADBwndlAXJMAwXLLwYXI6Wq1RHBC5RIGCYIAC6hGJGA6nKFwp5IABAWGbRDUKGCYWIGBouJGBoWKDBYWMJRBELGRQVRAE4A=="))
\ No newline at end of file
+require("heatshrink").decompress(atob("mEwwkBkQAWkAyVgQXYhnMACXAC/4X/C/4XH5s9CAvTmfcC5nf/4GGAAM8C5YOBGogWC//zC5XDC4ouB/vdBIJJCC4wnDC4c/CYXP//8C5APBC4pDDHQIXICwPzAQI2EC4QEB/oXHQYQXGCQQ0EF4wOBC6nNJQYXSPIgXaRQIvtC7jEEC5oqEa5QXIAoZ8B/gXQ774BAYgXPFYP/BAIJDC5wGCAAJGCC5PfOISREPQYXKABHdAogXRAAoX/C/4X/C+UAACgXYkQAW"))
diff --git a/apps/game1024/app.js b/apps/game1024/app.js
index 352426d57..829b29ba9 100644
--- a/apps/game1024/app.js
+++ b/apps/game1024/app.js
@@ -3,12 +3,10 @@ const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)}
const rows = 4, cols = 4;
const borderWidth = 6;
const sqWidth = (Math.floor(Bangle.appRect.w - 48) / rows) - borderWidth;
-const cellColors = [{bg:'#CCFFFF', fg: '#000000'},
- {bg:'#FF9966', fg: '#000000'}, {bg:'#330033', fg: '#FFFFFF'}, {bg:'#6600FF', fg: '#FFFFFF'}, {bg:'#33CC66', fg: '#000000'},
- {bg:'#990000', fg: '#FFFFFF'}, {bg:'#99FF66', fg: '#000000'}, {bg:'#0033CC', fg: '#FFFFFF'}, {bg:'#CCFF00', fg: '#000000'},
- {bg:'#00CC00', fg: '#FFFFFF'}, {bg:'#FF0000', fg: '#FFFFFF'}];
-const cellFgColor = '#000000';
-const cellShadowColor = '#f2f2f2';
+const cellColors = [{bg:'#00FFFF', fg: '#000000'},
+ {bg:'#FF00FF', fg: '#000000'}, {bg:'#808000', fg: '#FFFFFF'}, {bg:'#0000FF', fg: '#FFFFFF'}, {bg:'#008000', fg: '#FFFFFF'},
+ {bg:'#800000', fg: '#FFFFFF'}, {bg:'#00FF00', fg: '#000000'}, {bg:'#000080', fg: '#FFFFFF'}, {bg:'#FFFF00', fg: '#000000'},
+ {bg:'#800080', fg: '#FFFFFF'}, {bg:'#FF0000', fg: '#FFFFFF'}];
const cellFonts = ["12x20", "12x20", "Vector:14"];
const cellChars = [
[0,1,2,3,4,5,6,7,8,9,10],
@@ -135,7 +133,7 @@ const snapshot = {
debug(console.log("reset D U M P E D!", this.dump));
}
};
-const btnAtribs = {x: 134, w: 42, h: 42, fg:'#B5E61D', bg:'#870014'};
+const btnAtribs = {x: 134, w: 42, h: 42, fg:'#C0C0C0', bg:'#800000'};
const buttons = {
all: [],
draw: function () {
@@ -679,4 +677,4 @@ buttons.add(new Button('restart', btnAtribs.x, 106, btnAtribs.w, btnAtribs.h, 'R
initGame();
-dragger.setEnabled(true);
\ No newline at end of file
+dragger.setEnabled(true);
diff --git a/apps/game1024/game1024.png b/apps/game1024/game1024.png
index 4118dd3ca..28b0079d8 100644
Binary files a/apps/game1024/game1024.png and b/apps/game1024/game1024.png differ
diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json
index 5b75212bc..92d34bed3 100644
--- a/apps/game1024/metadata.json
+++ b/apps/game1024/metadata.json
@@ -1,7 +1,7 @@
{ "id": "game1024",
"name": "1024 Game",
"shortName" : "1024 Game",
- "version": "0.02",
+ "version": "0.03",
"icon": "game1024.png",
"screenshots": [ {"url":"screenshot.png" } ],
"readme":"README.md",
@@ -13,6 +13,5 @@
"storage": [
{"name":"game1024.app.js","url":"app.js"},
{"name":"game1024.img","url":"app-icon.js","evaluate":true}
- ],
- "data":[{"name": "game1024.json", "url": "game1024.json"}]
- }
\ No newline at end of file
+ ]
+ }
diff --git a/apps/game1024/screenshot.png b/apps/game1024/screenshot.png
index 256ecb53b..5cc244bda 100644
Binary files a/apps/game1024/screenshot.png and b/apps/game1024/screenshot.png differ
diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog
index 7d8fecb1e..9d43f6575 100644
--- a/apps/lcars/ChangeLog
+++ b/apps/lcars/ChangeLog
@@ -15,4 +15,5 @@
0.15: Using wpedom to count steps.
0.16: Improved stability. Wind can now be shown.
0.17: Settings for mph/kph and other minor improvements.
-0.18: Fullscreen mode can now be enabled or disabled in the settings.
\ No newline at end of file
+0.18: Fullscreen mode can now be enabled or disabled in the settings.
+0.19: Alarms can not go bigger than 100.
diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js
index 7d5da2d8e..433d33427 100644
--- a/apps/lcars/lcars.app.js
+++ b/apps/lcars/lcars.app.js
@@ -626,7 +626,7 @@ Bangle.on('charging',function(charging) {
function increaseAlarm(){
- if(isAlarmEnabled()){
+ if(isAlarmEnabled() && getAlarmMinutes() < 95){
settings.alarm += 5;
} else {
settings.alarm = getCurrentTimeInMinutes() + 5;
diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json
index e6ca10f79..1335b8e1d 100644
--- a/apps/lcars/metadata.json
+++ b/apps/lcars/metadata.json
@@ -3,7 +3,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
- "version":"0.18",
+ "version":"0.19",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",
diff --git a/apps/recorder/app.js b/apps/recorder/app.js
index e5e732fe3..99252e0e2 100644
--- a/apps/recorder/app.js
+++ b/apps/recorder/app.js
@@ -219,7 +219,7 @@ function viewTrack(filename, info) {
f.erase();
viewTracks();
} else
- viewTrack(n, info);
+ viewTrack(filename, info);
});
};
menu['< Back'] = () => { viewTracks(); };
diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog
index 032ebdc1a..401a68de9 100644
--- a/apps/run/ChangeLog
+++ b/apps/run/ChangeLog
@@ -8,3 +8,4 @@
0.07: Fix crash if an odd number of active boxes are configured (fix #1473)
0.08: Added support for notifications from exstats. Support all stats from exstats
0.09: Fix broken start/stop if recording not enabled (fix #1561)
+0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578)
diff --git a/apps/run/metadata.json b/apps/run/metadata.json
index 01a85ed05..51239d297 100644
--- a/apps/run/metadata.json
+++ b/apps/run/metadata.json
@@ -1,6 +1,6 @@
{ "id": "run",
"name": "Run",
- "version":"0.09",
+ "version":"0.10",
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
"icon": "app.png",
"tags": "run,running,fitness,outdoors,gps",
diff --git a/apps/run/settings.js b/apps/run/settings.js
index 29a2f43cc..949f7a235 100644
--- a/apps/run/settings.js
+++ b/apps/run/settings.js
@@ -42,6 +42,11 @@
value: Math.max(statsIDs.indexOf(settings[boxID]),0),
format: v => statsList[v].name,
onchange: v => {
+ for (var i=1;i<=6;i++)
+ if (settings["B"+i]==statsIDs[v]) {
+ settings["B"+i]="";
+ boxMenu["Box "+i].value=0;
+ }
settings[boxID] = statsIDs[v];
saveSettings();
},
@@ -60,7 +65,7 @@
'': { 'title': 'Run' },
'< Back': back,
};
- if (WIDGETS["recorder"])
+ if (global.WIDGETS&&WIDGETS["recorder"])
menu[/*LANG*/"Record Run"] = {
value : !!settings.record,
format : v => v?/*LANG*/"Yes":/*LANG*/"No",
@@ -87,7 +92,7 @@
notificationsMenu[/*LANG*/"Dist Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))),
min: 0, max: vibPatterns.length,
- format: v => vibPatterns[v]||"Off",
+ format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.dist.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
@@ -97,7 +102,7 @@
notificationsMenu[/*LANG*/"Step Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))),
min: 0, max: vibPatterns.length,
- format: v => vibPatterns[v]||"Off",
+ format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.step.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
@@ -107,7 +112,7 @@
notificationsMenu[/*LANG*/"Time Pattern"] = {
value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))),
min: 0, max: vibPatterns.length,
- format: v => vibPatterns[v]||"Off",
+ format: v => vibPatterns[v]||/*LANG*/"Off",
onchange: v => {
settings.notify.time.notifications = vibTimes[v];
sampleBuzz(vibTimes[v]);
diff --git a/apps/s7clk/icon.js b/apps/s7clk/icon.js
index d5d9aaf68..dbb4fc6d3 100644
--- a/apps/s7clk/icon.js
+++ b/apps/s7clk/icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mUygP/AC5BlH4MAn/gAwN/4EP/AFBsEMhkBwEAjEDgYJBgEGgHA4EYDwOAmEwBIIYyj/wgf+AoMH/kA/4eBJXwYLVxgAjh//AC3w"))
+require("heatshrink").decompress(atob("mEqgInkn/gg/8Ao/AjEYgYF/AoZT/Kb4AiA="))
diff --git a/apps/s7clk/icon.png b/apps/s7clk/icon.png
index cb08aec5e..cfb1c0349 100644
Binary files a/apps/s7clk/icon.png and b/apps/s7clk/icon.png differ
diff --git a/apps/seiko-5actus/ChangeLog b/apps/seiko-5actus/ChangeLog
new file mode 100644
index 000000000..978e5d6ea
--- /dev/null
+++ b/apps/seiko-5actus/ChangeLog
@@ -0,0 +1,2 @@
+0.01: Initial Release
+0.02: Shrink hand images to save memory
diff --git a/apps/seiko-5actus/README.md b/apps/seiko-5actus/README.md
new file mode 100644
index 000000000..4f09bf3c6
--- /dev/null
+++ b/apps/seiko-5actus/README.md
@@ -0,0 +1,16 @@
+# Seiko 5actus
+
+
+
+This is built on the knowledge of what I gained through designing the rolex watch face and improves on it by getting more done right at the start.
+
+This watch is modeled after one I personally own and love, I have spent quite a bit of time designing this in a pixel art editor to try and make it as clean as possible and am quite happy with how it came out.
+
+This watch face works in both the light and dark themes but I personally think it looks a lot cleaner in the dark them.
+
+This watch whilst technically designed in a way that would work with the BangleJs has been only listed to work with the BangleJs2, if someones wants to test it on a first gen and let me know if it works then i'll allow it to be installed on both devices but I assume with how the images have been designed it would look strange on a first gen watch.
+
+Special thanks to:
+* rozek (for his updated widget draw code for utilization with background images)
+* Gordon Williams (Bangle.js, watchapps for reference code and documentation)
+* The community (for helping drive such a wonderful project)
diff --git a/apps/seiko-5actus/app-icon.js b/apps/seiko-5actus/app-icon.js
new file mode 100644
index 000000000..796f24122
--- /dev/null
+++ b/apps/seiko-5actus/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEBpMB+fziAjRCQQXBHyoXEgIRLgMwC5EAj8gC5MC+QXJn4XKBgJHJMhkfJAYXEh/xC5cDBofzJocvIxKiCHpBGNExMCIxi2KeRIJFgMiiYBCkQ1Jh67EAAMSCgICBiQjFn8xDYX/AgQANn4qEgf/JIcDkcxiUSiMRY4cv+ZaFj6bDgZGBkMRDIIXD/7CHn5TDFYIADFIcRSxgAvXQwAQgRyDACcje4wAESQ4RDmMQgSGBj8zAAnyTgauH/65Cj7EBkMicAPyBYIABCgcRkYWCmYvCewMhmUiiMyF4gUBDoXzn7/Dj4RBF4IXB+QrDj40DmJgBiEyBYMDL4qcEgUikYqDCgSGKAAUSn8hbh8BgICBl/yFggwEbhMC/4sHAAcTIhIsBGYLcJGAQOHgLAEGBM/Jg0vRgsQZAMQBAQUBif/AwLJDfoQ1DkDOBkIOCgQDBFAKCDaIRPGSwqGBgM/dwUDeQiZEEAowCgXxb5jGGgQ+GaAjaHeIbmISYToHRYTPKYQIODE4cfEA4AEPghtEgQJCI5Mv+AXHJAiIIBggXFj/xDBMCcAYXGga7LcYIXIUpY1GC4SiJVRAXCiAWJA"))
diff --git a/apps/seiko-5actus/app.js b/apps/seiko-5actus/app.js
new file mode 100644
index 000000000..078b6e5c2
--- /dev/null
+++ b/apps/seiko-5actus/app.js
@@ -0,0 +1,181 @@
+var imgBg = {
+ width : 176, height : 176, bpp : 2,
+ transparent : 1,
+ buffer : require("heatshrink").decompress(atob("qoASv/8//1C6YASrorD6opjuorHBAIriQYwriV9FXFZldFjrSEFY6FjQcorPSYqtZFZaxaVoiDkZRArLv4rV/orT/5rGABtf/4rSq//+79UFaptIK5puHFZQUBFda5IABZuBCw4rKTAPVq4rUNw4rK/4rBroqRQAIrVQSd1N4QrQNZIAOFdbzBQ4IrOCQIIDWKQYFFZhqBHwaeBACBwBFazZQThQrJ/7CHABaSEFaTaWOIYrONJArTToorIdpDdRDQLJOCBDhRIxBoPABdXAwwrRVKQ+GZR7aZDYRHNM4IraOZyTQZbQrKaIwAKr7LOHRNdFaJzOr56RuppZACBgRAEtW1Wl1WqzWm1Nqy2qC5hyTrWq1IrFzWqyoXLv7mFQRlqqtp0tVy2V0uptNVA4JWK/72Fr4yFFY9VLINWyqJBFZtfFYwGGQZIrS////peF/6xM1KDDQQIrMKwJQFA4SEKQYLbGAoIrKv5PGGY4rGEYIAHyrZKQQoIDQhoARKwR6GBJIAXJpKECMIoAXUpaELBYQAICZR4CPYo3Kq4IBr7RITIzZFq7mOGwXXBYIjB//3/9drt/+5AGZ4RKHuoNEFZVdupQBuorBupjCJIyNIrqELr4mBr99vorEK476PBxAYC79//orCr4QBu5XIXAwiIcwxXCKYJlBFYSvIQRI7HTiLjBFYzZHAAzdBDA4AKDY6CMQgYQOABT8CEQjdJFTAAKuo8MADokkAH4A/AH4A/ABn1FldXFlfVTX4A/AH4A/AH4APr//ABVVrorcv4rL6tXFbgqL//1Qbv/1QAE1AED14re/wrK1Yr/Ff4rEcY3VFf4r/Ff4r/EaoAH/4rK14r/FZYALFb1/FZbTWAA9fFZYNBFjoA/AH4A/AH4A/vots+pu/AH4A/AH4A/ADdX6ousr4GFrohgABP/FbN/+o7O/6NZv5HOB4IrZ///LBlXB5wrO/qCNB5pzOOhgNBK7RICDhQNCX5P96td/91u4FBvpJLAoQPDrplEQRNdFYPVu93qvVO5JYJurZDSJV1FYP9FYP16oXBfJRKIbJpQB7vVv/3AwJvCbpTZVVIP9/9d6/9AALdTbJgAVEJDZMACoiCLAjZNAAqSKbpjZNSoo7PQg4zCIx9/FaBQBJ4rZRHqJQBFYzZRLCN/HooYRCQIRQYQ1dDCFVQR4A/ADFXCSK5RDJ40Iq6nPW6LcIq//fh39SrNf/6EN/47OFZp0NFbd/K5wPPI5obNM5F1FaYPNdYLcGfpA3IDQIrXABZ6FDSLcZTxAIBW4zcBFa4ZBFaLsNFZYZGFZBpIACCdBFZ7BFq7dSZJArOfQwAHHQYYBFaA+JABwhBIggrObirIJFZLuBbioXJFZI/JWJorBEJIJJS4KFRrqbKFZLvDupYSeZIrJbiwrBNwIrnTQYrReBIrNCpArKBRQAKIJQrLNhCvNJidXQSZYCPCiCTIQS6KEI4pVAAddFaF1FbAZHfioAVFYyUJWbRXHLrqxFFYhVeLI4rq6orCMAoAhFa4"))
+};
+
+/* Set hour hand image */
+
+var imgHour = {
+ width : 14, height : 114, bpp : 2,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AH4A/AB8P/4DB//wAz8D//8BIIKBn4DB54CBACPzAQP8EoImBD4PAJkQG/A34GIgbUBA"))
+};
+
+/* Set minute hand image */
+
+var imgMin = {
+ width : 4, height : 168, bpp : 2,
+ transparent : 0,
+ buffer : require("heatshrink").decompress(atob("AH4AE/4A/AEI"))
+};
+
+/* Set second hand image */
+
+var imgSec = {
+ width : 6, height : 176, bpp : 2,
+ transparent : 1,
+ buffer : require("heatshrink").decompress(atob("qoA/ADFf6v9AU1c6vNFlICWvtXAXlVA="))
+};
+
+/* Sets the font for the date at an appropriate size of 14 */
+
+Graphics.prototype.setFontWorkSans = function(scale) {
+ // Actual height 12 (12 - 1)
+ this.setFontCustom(atob("AAAAAAADwAAAPAAAAAAAAAvAAC/0AH/gAL/QAD9AAAIAAAAACQAAL/9AC/r9APAA8A8ADwDwAfAH//wAH/8AAAAAADwAAAtAAAH//8Af//wAAAAAAAAAABwAUAfgHwDwA/APAP8A8DzwD/9PAD/Q8AAABQAAA0AHwDwA9AHwDw4PAPDw8A///gA/f8AAAAAAAAQAAAvAAAP8AALzwAD8PAAv//wB///AAAPAAAAUAAAAAAC/88AP/y8A8NDwDywPAPD18A8L/AAAGgAAAAAAC/8AA//9ALTx8A8ODwDw4PALz/8ALD/AAAAAAPAAAA8AAADwB/APC/8A9/gAD/AAAPgAAAAAAAAAAAAB8fwAf//wDw8PAPDw8A8PDwB///AC8vwAAAAAAAAAAD/DgAv/PQDwsPAPC08A8PDwB//8AB//QAAAAAAAAAAA8DwADwPAABAE"), 46, atob("BAYJBggICQgJCAkJBA=="), 14+(scale<<8)+(2<<16));
+ return this;
+};
+
+/* Sets the font for the day at an appropriate size of 12 */
+
+Graphics.prototype.setFontWorkSansSmall = function(scale) {
+ // Actual height 12 (11 - 0)
+ this.setFontCustom(atob("AAAAAAAAAAAAAAAAAABAP/zwGqjQAAAAP0AAEAAAP4AAGAAAAEoAAs/gL//QL88AAt/gL/+QK88AAYAAAUFAD+PAHPDgv//8fr70Hj7QCx+AAAAAC8AAL/AAPHBgL/PQB68AAPgAC9/APT7wEDjwAC/QAAAAAAuAC7/gL/DwPPywL9/gCwvAAD7wAABgAAAAP0AAEAAAACkAC//wP0C8sAAPYAAGPQA+D//0Af9ABQAADzAAB/AAv8AAB/AADyAABAAAACAAADgAADgAC//AAr5AADgAADQAAABAAAD9AAD0AAAAACwAACwAACwAACwAAAgAAABAAADwAADQAAAkAAv0Af9AL+AAvAAAAKgAD//ALgLgPADwPADwD//QA/9AAAAAAwAADwAAL//gL//gAAAAAgBQD4DwPAPwPA/wLnzwD/TwAUBQAAFADwPQLADwPDDwPvjwD+/AAQYAAAoAAD8AAv8AD08AP//gL//gAA8AAAAAL/PAPrDwPPDwPPDwPH/AAAoAAKgAC/+AHzrgPPDwPPDwH3/gBS+AKAAAPAAAPAbgPL/gP/QAPwAALAAAAAYAD+/ALvjwPLDwPLDwH//gBk+AAYAAD/PALTzwPDzwPDjwD//AAv8AAQBAA8DwA4DgAAAAAEBAAPD9AOD4AAAAABQAADwAAP4AANsAA8OAA4PABwHAAIUAAM8AAM8AAM8AAM8AAM8AAMoABgCAA4LAA8OAAdsAAP8AAHwAADgABgAADwAAPCzgPDzwLvBAD9AAAQAAABoAAv/wC0A8DD/OLPX3OMDjONHDHP/7Dfi5B4HQAP9AAACgAC/gB/8AP48AP48AB/8AAC/gAACgAAAAL//gP//wPDDwPDDwLv3gD+/AAAYAAGQAB/+AH0fQPADwPADwPADwDwPQBgNAAAAAL//gP//wPADwPADwHQDgD//AA/8AAAAAAAAAL//gP//wPDDwPDDwPDDwPADwAAAAAAAAL//gP//gPDAAPDAAPDAAPAAAAGQAB/+AD0fQPADwPCjwPDzwHz/QBy/gAAAAAAAAL//gL//gADAAADAAADAAL//gL//gAAAAAAAAL//gL//gAAAAAAeAAAvgAADwAADwL//gP//AAAAAAAAAL//gL//gAPAAA/0ADx/APAPwIABgAAAAL//gL//wAADwAADwAADwAACgAAAAL//gP6qQC/gAAH/QAAvwAv9AL9AAP//wGqqQAAAAL//gL6qQC+AAAP0AAB/AL//wL//gAAAAAGQAB/+AH0fQPADwPADwPADwD6vQB/9AAGQAAAAAL//gP//gPDwAPDwAH7gAD/AAAAAAAGQAB/+AH0fQPADwPAD9PAD/D6vbB/9PAGQAAAAAL//gP//gPDgAPD0ALr/AD/LwAUAgAUFAD+PALvDwPHDwPDjwLT7gDx/AAAAALAAAPAAAPAAAP//wPqqQPAAAPAAAAAAAP/+AGqvgAADwAADwAADwL//AL/4AAAAAKAAAL+AAAv9AAAvwAB/gAv8AL9AAKAAAKQAAL/QAAf/gAAPwAv/QL+AAL/gAAL/gAAvwC/+AP9AAEAAAEABgPgLwD9+AAfwAA/8AL0fgPADwOAAAL0AAB/AAAH/wA/qQL4AAPAAAFACgPAPwPA/wPHzwPvDwP4DwLQDwAAAAAAAAv///uqqvsAAPdAAAf4AAB/0AAC/wAAC4cAAKsAAPv///Kqqp"), 32, atob("BAMFCAgLCAMEBAcHAwYDBQgFBwcHBwcHBwcEBAcHBwcLCAgICQgHCQkEBwgHCgkJCAkICAcJCAwHBwgEBQQ="), 12+(scale<<8)+(2<<16));
+ return this;
+};
+
+/* Set variables to get screen width, height and center points */
+
+let W = g.getWidth();
+let H = g.getHeight();
+let cx = W/2;
+let cy = H/2;
+let Timeout;
+
+Bangle.loadWidgets();
+
+/* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */
+
+Bangle.drawWidgets = function () {
+ var w = g.getWidth(), h = g.getHeight();
+
+ var pos = {
+ tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left
+ tr:{x:w-1, y:0, r:1, c:0},
+ bl:{x:0, y:h-24, r:0, c:0},
+ br:{x:w-1, y:h-24, r:1, c:0}
+ };
+
+ if (global.WIDGETS) {
+ for (var wd of WIDGETS) {
+ var p = pos[wd.area];
+ if (!p) continue;
+
+ wd.x = p.x - p.r*wd.width;
+ wd.y = p.y;
+
+ p.x += wd.width*(1-2*p.r);
+ p.c++;
+ }
+
+ g.reset(); // also loads the current theme
+
+ try {
+ for (var wd of WIDGETS) {
+ g.setClipRect(wd.x,wd.y, wd.x+wd.width-1,23);
+ wd.draw(wd);
+ }
+ } catch (e) { print(e); }
+
+ g.reset(); // clears the clipping rectangle!
+ }
+ };
+
+/* Draws the clock hands and date */
+
+function drawHands() {
+ let d = new Date();
+
+ let hour = d.getHours() % 12;
+ let min = d.getMinutes();
+ let sec = d.getSeconds();
+
+ let twoPi = 2*Math.PI;
+ let Pi = Math.PI;
+
+ let hourAngle = (hour+(min/60))/12 * twoPi - Pi;
+ let minAngle = (min/60) * twoPi - Pi;
+ let secAngle = (sec/60) * twoPi - Pi;
+
+ g.setFontWorkSans();
+ g.setColor(g.theme.bg);
+ g.setFontAlign(0,0,0);
+ g.drawString(d.getDate(),162,90);
+ g.setFontWorkSansSmall();
+ let weekDay = d.toString().split(" ");
+ if (weekDay[0] == "Sat"){g.setColor(0,0,1);}
+ else if (weekDay[0] == "Sun"){g.setColor(1,0,0);}
+ else {g.setColor(g.theme.bg);}
+ g.drawString(weekDay[0].toUpperCase(), 137, 90);
+
+ handLayers = [
+ {x:cx,
+ y:cy,
+ image:imgHour,
+ rotate:hourAngle,
+ center:true
+ },
+ {x:cx,
+ y:cy,
+ image:imgMin,
+ rotate:minAngle,
+ center:true
+ },
+ {x:cx,
+ y:cy,
+ image:imgSec,
+ rotate:secAngle,
+ center:true
+ }];
+
+ g.setColor(g.theme.fg);
+ g.drawImages(handLayers);
+}
+
+function drawBackground() {
+ g.clear(1);
+ g.setBgColor(g.theme.bg);
+ g.setColor(g.theme.fg);
+ bgLayer = [
+ {x:cx,
+ y:cy,
+ image:imgBg,
+ center:true
+ }];
+ g.drawImages(bgLayer);
+ g.reset();
+}
+
+/* Refresh the display every second */
+
+function displayRefresh() {
+ g.clear(true);
+ drawBackground();
+ drawHands();
+ Bangle.drawWidgets();
+
+ let Pause = 1000 - (Date.now() % 1000);
+ Timeout = setTimeout(displayRefresh,Pause);
+}
+
+Bangle.on('lcdPower', (on) => {
+ if (on) {
+ if (Timeout != null) { clearTimeout(Timeout); Timeout = undefined;}
+ displayRefresh();
+ }
+});
+
+Bangle.setUI("clock");
+// load widgets after 'setUI' so they're aware there is a clock active
+Bangle.loadWidgets();
+displayRefresh();
diff --git a/apps/seiko-5actus/metadata.json b/apps/seiko-5actus/metadata.json
new file mode 100644
index 000000000..33de8213b
--- /dev/null
+++ b/apps/seiko-5actus/metadata.json
@@ -0,0 +1,17 @@
+{ "id": "seiko-5actus",
+ "name": "Seiko 5actus",
+ "shortName":"5actus",
+ "icon": "seiko-5actus.png",
+ "screenshots": [{"url":"screenshot.png"}],
+ "version":"0.02",
+ "description": "A watch designed after then Seiko 5actus from the 1970's",
+ "tags": "clock",
+ "type": "clock",
+ "supports":["BANGLEJS2"],
+ "readme": "README.md",
+ "allow_emulator": true,
+ "storage": [
+ {"name":"seiko-5actus.app.js","url":"app.js"},
+ {"name":"seiko-5actus.img","url":"app-icon.js","evaluate":true}
+ ]
+ }
diff --git a/apps/seiko-5actus/screenshot.png b/apps/seiko-5actus/screenshot.png
new file mode 100644
index 000000000..fb8638999
Binary files /dev/null and b/apps/seiko-5actus/screenshot.png differ
diff --git a/apps/seiko-5actus/seiko-5actus.png b/apps/seiko-5actus/seiko-5actus.png
new file mode 100644
index 000000000..73f1b8164
Binary files /dev/null and b/apps/seiko-5actus/seiko-5actus.png differ
diff --git a/apps/slomoclock/app-icon.js b/apps/slomoclock/app-icon.js
index 22e264124..46f668745 100644
--- a/apps/slomoclock/app-icon.js
+++ b/apps/slomoclock/app-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("oFAwhC/ABOIABgfymYAKD+Z/9hGDL5c4wAf/XzjASTxqgQhAfPMB2IPxiACIBo+BDxqACIBg+CLxpANHwQPBABgvCIBT8CJ5owDD5iPOOAQfLBojiDCYQGFGIQfICIQfdBYJNMOI6SHD8jeNOIYzID8hfRD9LfEAoTdFBIifLAAIffBoQRBAJpxMD84JCD+S/GL56fID8ALBb6ZhID8qtJCZ4fgT4YDBABq/PD7RNEL6IRKD8WID5pfCD5kzNhKSFmYfMBwSeOGBoPDABgvCJ5wAON5pADABivPIAIAOd5xABABweOD4J+OD58IQBj8LD/6gUDyAfhXzgfiP/wA2"))
+require("heatshrink").decompress(atob("mEw4UA///7k8//GnldDZ9RosUqNABQsFqoACqALFg2qAAWQBaMVEYdUBYseC4e0BYsaBYekBYt6BYetBYouDAAIKEgPqC4erNgkFBQYABNgke2oiDrxIEvXUBYcXHgl7FIkB9oEDBYKYBTwILEi4LCoEBBYUQHQX7EYyRCBYJrF95ICBYNFBQdRBYcWEYwLDit7otUHQMVqIvDL4L5BgL8CI4YLDqILDO4gXGBQUEEZQ7CEYprEI4prFoLGBqkFoILFNZaPFF4ZHCR4hrFa5ILMfYJeDfYse2ovDrxGCAAMF1QAEMgIpD9QKD1Y1EgBfFBQg8BC4Y6EAAMaBYekBYseBYZGESIQuDfIYACgwXDyALRgojDNQhsCBYZqFABI="))
diff --git a/apps/sunclock/README.md b/apps/sunclock/README.md
new file mode 100644
index 000000000..4076767d9
--- /dev/null
+++ b/apps/sunclock/README.md
@@ -0,0 +1,6 @@
+# Sun Clock
+Clock showing date/time, sunset/sunrise, H = current sun height/noon sun height, Az = sun azimuth
+
+
+
+Location set with mylocation app, time zone set with settings app.
diff --git a/apps/sunclock/app-icon.js b/apps/sunclock/app-icon.js
new file mode 100644
index 000000000..977aec98d
--- /dev/null
+++ b/apps/sunclock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("kEgwhC/AC8N6APo7oPJBQndBQYPEhoaFAogZIEokO93u8AuGAAYOCCAgOLCBQOFAAIeNEBAPPBw4wHB5wuIGAwPthGIxwIC8UowUuB4eIwAPBxEk91CAgIGGwAhBBYeCAwMoA4ZwEBIIOCAxAA/ABwA="))
diff --git a/apps/sunclock/app.js b/apps/sunclock/app.js
new file mode 100644
index 000000000..4609565a2
--- /dev/null
+++ b/apps/sunclock/app.js
@@ -0,0 +1,79 @@
+/* sclock.app.js for Bangle2
+Peter Bernschneider 30.12.2021
+Update current latitude and longitude in My Location app
+Update current Timezone in Settings app, menu item "System"
+Update for summer time by incrementing Timezone += 1 */
+setting = require("Storage").readJSON("setting.json",1);
+E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ
+SunCalc = require("suncalc.js");
+loc = require('locale');
+const LOCATION_FILE = "mylocation.json";
+const xyCenter = g.getWidth() / 2 + 3;
+const yposTime = 60;
+const yposDate = 100;
+const yposRS = 135;
+const yposPos = 160;
+var rise = "07:00";
+var set = "20:00";
+var pos = {altitude: 20, azimuth: 135};
+var noonpos = {altitude: 37, azimuth: 180};
+let idTimeout = null;
+
+function updatePos() {
+ coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":53.3,"lon":10.1,"location":"Pattensen"};
+ pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon);
+ times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon);
+ rise = times.sunrise.toString().split(" ")[4].substr(0,5);
+ set = times.sunset.toString().split(" ")[4].substr(0,5);
+ noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon);
+}
+
+function drawSimpleClock() {
+ var d = new Date(); // get date
+ var da = d.toString().split(" ");
+ g.clear();
+ Bangle.drawWidgets();
+ g.reset(); // default draw styles
+ g.setFontAlign(0, 0); // drawSting centered
+
+ var time = da[4].substr(0, 5); // draw time
+
+ g.setFont("Vector",60);
+ g.drawString(time, xyCenter, yposTime, true);
+
+ var date = [loc.dow(new Date(),1), loc.date(d,1)].join(" "); // draw day of week, date
+ g.setFont("Vector",24);
+ g.drawString(date, xyCenter, yposDate, true);
+
+ g.setFont("Vector",25);
+ g.drawString(`${rise} ${set}`, xyCenter, yposRS, true); // draw riseset
+ g.drawImage(require("Storage").read("sunrise.img"), xyCenter-16, yposRS-16);
+
+ g.setFont("Vector",21);
+ g.drawString(`H${pos.altitude}/${noonpos.altitude} Az${pos.azimuth}`, xyCenter, yposPos, true); // draw sun pos
+
+ let t = d.getSeconds()*1000 + d.getMilliseconds();
+ idTimeout = setTimeout(drawSimpleClock, 60000 - t); // time till next minute
+}
+
+// special function to handle display switch on
+Bangle.on('lcdPower', function(on){
+ if (on) {
+ drawSimpleClock();
+ } else {
+ if(idTimeout) {
+ clearTimeout(idTimeout);
+ }
+ }
+});
+
+g.clear(); // clean app screen
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+setInterval(updatePos, 60*5E3); // refesh every 5 mins
+
+updatePos();
+drawSimpleClock(); // draw now
+
+setWatch(Bangle.showLauncher, BTN1, { repeat: false, edge: "falling" }); // Show launcher when button pressed
diff --git a/apps/sunclock/app.png b/apps/sunclock/app.png
new file mode 100644
index 000000000..72c5b10d5
Binary files /dev/null and b/apps/sunclock/app.png differ
diff --git a/apps/sunclock/metadata.json b/apps/sunclock/metadata.json
new file mode 100644
index 000000000..a39343992
--- /dev/null
+++ b/apps/sunclock/metadata.json
@@ -0,0 +1,16 @@
+{
+ "id": "sunclock",
+ "name": "Sun Clock",
+ "version": "0.01",
+ "description": "A clock with sunset/sunrise, sun height/azimuth",
+ "icon": "app.png",
+ "type": "clock",
+ "tags": "clock",
+ "supports": ["BANGLEJS2"],
+ "allow_emulator": true,
+ "storage": [
+ {"name":"sunclock.app.js","url":"app.js"},
+ {"name":"sunclock.img","url":"app-icon.js","evaluate":true},
+ {"name":"suncalc.js","url":"suncalc.js"}
+ ]
+}
diff --git a/apps/sunclock/screenshot_sunclock.png b/apps/sunclock/screenshot_sunclock.png
new file mode 100644
index 000000000..a24af2116
Binary files /dev/null and b/apps/sunclock/screenshot_sunclock.png differ
diff --git a/apps/sunclock/suncalc.js b/apps/sunclock/suncalc.js
new file mode 100644
index 000000000..b1af0a0d9
--- /dev/null
+++ b/apps/sunclock/suncalc.js
@@ -0,0 +1,298 @@
+/* Module suncalc.js
+ (c) 2011-2015, Vladimir Agafonkin
+ SunCalc is a JavaScript library for calculating sun/moon position and light phases.
+ https://github.com/mourner/suncalc
+
+PB: Usage:
+E.setTimeZone(2); // 1 = MEZ, 2 = MESZ
+SunCalc = require("suncalc.js");
+pos = SunCalc.getPosition(Date.now(), 53.3, 10.1);
+times = SunCalc.getTimes(Date.now(), 53.3, 10.1);
+rise = times.sunrise; // Date object
+rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm
+*/
+var exports={};
+
+// shortcuts for easier to read formulas
+
+var PI = Math.PI,
+ sin = Math.sin,
+ cos = Math.cos,
+ tan = Math.tan,
+ asin = Math.asin,
+ atan = Math.atan2,
+ acos = Math.acos,
+ rad = PI / 180;
+
+// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
+
+// date/time constants and conversions
+
+var dayMs = 1000 * 60 * 60 * 24,
+ J1970 = 2440588,
+ J2000 = 2451545;
+
+function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
+function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021
+function toDays(date) { return toJulian(date) - J2000; }
+
+
+// general calculations for position
+
+var e = rad * 23.4397; // obliquity of the Earth
+
+function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
+function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
+
+function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
+function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
+
+function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
+
+function astroRefraction(h) {
+ if (h < 0) // the following formula works for positive altitudes only.
+ h = 0; // if h = -0.08901179 a div/0 would occur.
+
+ // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
+ return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
+}
+
+// general sun calculations
+
+function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
+
+function eclipticLongitude(M) {
+
+ var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
+ P = rad * 102.9372; // perihelion of the Earth
+
+ return M + C + P + PI;
+}
+
+function sunCoords(d) {
+
+ var M = solarMeanAnomaly(d),
+ L = eclipticLongitude(M);
+
+ return {
+ dec: declination(L, 0),
+ ra: rightAscension(L, 0)
+ };
+}
+
+// calculates sun position for a given date and latitude/longitude
+
+exports.getPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+
+ c = sunCoords(d),
+ H = siderealTime(d, lw) - c.ra;
+
+ return {
+ azimuth: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg
+ altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg
+ };
+};
+
+
+// sun times configuration (angle, morning name, evening name)
+
+var times = [
+ [-0.833, 'sunrise', 'sunset' ]
+];
+
+// calculations for sun times
+var J0 = 0.0009;
+
+function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
+
+function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
+function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
+
+function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
+function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
+
+// returns set time for the given sun altitude
+function getSetJ(h, lw, phi, dec, n, M, L) {
+
+ var w = hourAngle(h, phi, dec),
+ a = approxTransit(w, lw, n);
+ return solarTransitJ(a, M, L);
+}
+
+
+// calculates sun times for a given date, latitude/longitude, and, optionally,
+// the observer height (in meters) relative to the horizon
+
+exports.getTimes = function (date, lat, lng, height) {
+
+ height = height || 0;
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+
+ dh = observerAngle(height),
+
+ d = toDays(date),
+ n = julianCycle(d, lw),
+ ds = approxTransit(0, lw, n),
+
+ M = solarMeanAnomaly(ds),
+ L = eclipticLongitude(M),
+ dec = declination(L, 0),
+
+ Jnoon = solarTransitJ(ds, M, L),
+
+ i, len, time, h0, Jset, Jrise;
+
+
+ var result = {
+ solarNoon: fromJulian(Jnoon),
+ nadir: fromJulian(Jnoon - 0.5)
+ };
+
+ for (i = 0, len = times.length; i < len; i += 1) {
+ time = times[i];
+ h0 = (time[0] + dh) * rad;
+
+ Jset = getSetJ(h0, lw, phi, dec, n, M, L);
+ Jrise = Jnoon - (Jset - Jnoon);
+
+ result[time[1]] = fromJulian(Jrise);
+ result[time[2]] = fromJulian(Jset);
+ }
+
+ return result;
+};
+
+
+// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
+
+function moonCoords(d) { // geocentric ecliptic coordinates of the moon
+
+ var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
+ M = rad * (134.963 + 13.064993 * d), // mean anomaly
+ F = rad * (93.272 + 13.229350 * d), // mean distance
+
+ l = L + rad * 6.289 * sin(M), // longitude
+ b = rad * 5.128 * sin(F), // latitude
+ dt = 385001 - 20905 * cos(M); // distance to the moon in km
+
+ return {
+ ra: rightAscension(l, b),
+ dec: declination(l, b),
+ dist: dt
+ };
+}
+
+getMoonPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+
+ c = moonCoords(d),
+ H = siderealTime(d, lw) - c.ra,
+ h = altitude(H, phi, c.dec),
+ // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
+
+ h = h + astroRefraction(h); // altitude correction for refraction
+
+ return {
+ azimuth: azimuth(H, phi, c.dec),
+ altitude: h,
+ distance: c.dist,
+ parallacticAngle: pa
+ };
+};
+
+
+// calculations for illumination parameters of the moon,
+// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
+// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+
+getMoonIllumination = function (date) {
+
+ var d = toDays(date || new Date()),
+ s = sunCoords(d),
+ m = moonCoords(d),
+
+ sdist = 149598000, // distance from Earth to Sun in km
+
+ phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
+ inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
+ angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
+ cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
+
+ return {
+ fraction: (1 + cos(inc)) / 2,
+ phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
+ angle: angle
+ };
+};
+
+
+function hoursLater(date, h) {
+ return new Date(date.valueOf() + h * dayMs / 24);
+}
+
+// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
+
+getMoonTimes = function (date, lat, lng, inUTC) {
+ var t = new Date(date);
+ if (inUTC) t.setUTCHours(0, 0, 0, 0);
+ else t.setHours(0, 0, 0, 0);
+
+ var hc = 0.133 * rad,
+ h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
+ h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
+
+ // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
+ for (var i = 1; i <= 24; i += 2) {
+ h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
+ h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
+
+ a = (h0 + h2) / 2 - h1;
+ b = (h2 - h0) / 2;
+ xe = -b / (2 * a);
+ ye = (a * xe + b) * xe + h1;
+ d = b * b - 4 * a * h1;
+ roots = 0;
+
+ if (d >= 0) {
+ dx = Math.sqrt(d) / (Math.abs(a) * 2);
+ x1 = xe - dx;
+ x2 = xe + dx;
+ if (Math.abs(x1) <= 1) roots++;
+ if (Math.abs(x2) <= 1) roots++;
+ if (x1 < -1) x1 = x2;
+ }
+
+ if (roots === 1) {
+ if (h0 < 0) rise = i + x1;
+ else set = i + x1;
+
+ } else if (roots === 2) {
+ rise = i + (ye < 0 ? x2 : x1);
+ set = i + (ye < 0 ? x1 : x2);
+ }
+
+ if (rise && set) break;
+
+ h0 = h2;
+ }
+
+ var result = {};
+
+ if (rise) result.rise = hoursLater(t, rise);
+ if (set) result.set = hoursLater(t, set);
+
+ if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
+
+ return result;
+};
\ No newline at end of file
diff --git a/apps/widclose/ChangeLog b/apps/widclose/ChangeLog
new file mode 100644
index 000000000..4be6afb16
--- /dev/null
+++ b/apps/widclose/ChangeLog
@@ -0,0 +1 @@
+0.01: New widget!
\ No newline at end of file
diff --git a/apps/widclose/README.md b/apps/widclose/README.md
new file mode 100644
index 000000000..55c8de483
--- /dev/null
+++ b/apps/widclose/README.md
@@ -0,0 +1,7 @@
+# Close Button
+
+Adds a  button to close the current app and go back to the clock.
+(Widget is not visible on the clock screen)
+
+
+
\ No newline at end of file
diff --git a/apps/widclose/icon.png b/apps/widclose/icon.png
new file mode 100644
index 000000000..1d95ba0ce
Binary files /dev/null and b/apps/widclose/icon.png differ
diff --git a/apps/widclose/metadata.json b/apps/widclose/metadata.json
new file mode 100644
index 000000000..e044a2d39
--- /dev/null
+++ b/apps/widclose/metadata.json
@@ -0,0 +1,15 @@
+{
+ "id": "widclose",
+ "name": "Close Button",
+ "version": "0.01",
+ "description": "A button to close the current app",
+ "readme": "README.md",
+ "icon": "icon.png",
+ "type": "widget",
+ "tags": "widget,tools",
+ "supports": ["BANGLEJS2"],
+ "screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}],
+ "storage": [
+ {"name":"widclose.wid.js","url":"widget.js"}
+ ]
+}
diff --git a/apps/widclose/preview.png b/apps/widclose/preview.png
new file mode 100644
index 000000000..d90a3b4c5
Binary files /dev/null and b/apps/widclose/preview.png differ
diff --git a/apps/widclose/screenshot_dark.png b/apps/widclose/screenshot_dark.png
new file mode 100644
index 000000000..58067a3b9
Binary files /dev/null and b/apps/widclose/screenshot_dark.png differ
diff --git a/apps/widclose/screenshot_light.png b/apps/widclose/screenshot_light.png
new file mode 100644
index 000000000..32817ea8d
Binary files /dev/null and b/apps/widclose/screenshot_light.png differ
diff --git a/apps/widclose/widget.js b/apps/widclose/widget.js
new file mode 100644
index 000000000..3a354018b
--- /dev/null
+++ b/apps/widclose/widget.js
@@ -0,0 +1,14 @@
+if (!Bangle.CLOCK) WIDGETS.close = {
+ area: "tr", width: 24, sortorder: 10, // we want the right-most spot please
+ draw: function() {
+ Bangle.removeListener("touch", this.touch);
+ Bangle.on("touch", this.touch);
+ g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button
+ // b/w version of preview.png, 24x24
+ "GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA"
+ ), this.x, this.y);
+ }, touch: function(_, c) {
+ const w = WIDGETS.close;
+ if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) load();
+ }
+};
\ No newline at end of file
diff --git a/backup.js b/backup.js
index b0159036d..75e236049 100644
--- a/backup.js
+++ b/backup.js
@@ -22,10 +22,14 @@ function bangleDownload() {
var promise = Promise.resolve();
// Normal files
normalFiles.forEach((filename,n) => {
+ if (filename==".firmware") {
+ console.log("Ignoring .firmware file");
+ return;
+ }
promise = promise.then(() => {
Progress.hide({sticky: true});
var percent = n/fileCount;
- Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent,percent:0});
+ Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0});
return Comms.readFile(filename).then(data => zip.file(filename,data));
});
});
@@ -36,7 +40,7 @@ function bangleDownload() {
promise = promise.then(() => {
Progress.hide({sticky: true});
var percent = (normalFiles.length+n)/fileCount;
- Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent,percent:0});
+ Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0});
return Comms.readStorageFile(filename).then(data => zipStorageFiles.file(filename,data));
});
});
diff --git a/core b/core
index 6e94cf77a..27c7db603 160000
--- a/core
+++ b/core
@@ -1 +1 @@
-Subproject commit 6e94cf77a2b355389dcd5efa55e2249a9b31983c
+Subproject commit 27c7db6035832837ca3909ea52939f60803df72f
diff --git a/modules/Layout.js b/modules/Layout.js
index 20fa2be8b..0bfbc28ad 100644
--- a/modules/Layout.js
+++ b/modules/Layout.js
@@ -59,6 +59,7 @@ options is an object containing:
* `label` - the text on the button
* `cb` - a callback function
* `cbl` - a callback function for long presses
+* `back` - a callback function, passed as `back` into Bangle.setUI
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically
determine what objects have changed or moved, clear their previous locations, and re-render just those objects.
@@ -89,7 +90,7 @@ function Layout(layout, options) {
options = options || {};
this.lazy = options.lazy || false;
- var btnList;
+ var btnList, uiSet;
Bangle.setUI(); // remove all existing input handlers
if (process.env.HWVERSION!=2) {
// no touchscreen, find any buttons in 'layout'
@@ -104,7 +105,7 @@ function Layout(layout, options) {
this.physBtns = 0;
this.buttons = btnList;
this.selectedButton = -1;
- Bangle.setUI("updown", dir=>{
+ Bangle.setUI({mode:"updown", back:options.back}, dir=>{
var s = this.selectedButton, l=this.buttons.length;
if (dir===undefined && this.buttons[s])
return this.buttons[s].cb();
@@ -119,8 +120,10 @@ function Layout(layout, options) {
}
this.selectedButton = s;
});
+ uiSet = true;
}
}
+ if (options.back && !uiSet) Bangle.setUI({mode: "custom", back: options.back});
if (options.btns) {
var buttons = options.btns;