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... + +![](Hochrad120px.png)...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..3f406e6a1 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -47,3 +47,4 @@ 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) diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index c9bd89f51..8eaeaf095 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+1; +}); +// 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+=1; }); -// 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..a4fa875fa 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.43", + "version": "0.44", "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/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/seiko-5actus/ChangeLog b/apps/seiko-5actus/ChangeLog new file mode 100644 index 000000000..28f11c1c7 --- /dev/null +++ b/apps/seiko-5actus/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Release 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 + +![](screenshot.png) + +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..a2e2d4569 --- /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 : 16, height : 176, bpp : 2, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AH4A/AH4A/AEk//gDp///gEDAYPAh4DB+E/AYP8AaYbDEYYrDLdgD/Af4DXh/wAYIA/AGwA=")) +}; + +/* Set minute hand image */ + +var imgMin = { + width : 8, height : 176, bpp : 2, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AH4A/AB8P+AB/AP4B/AIcA4DPHA=")) +}; + +/* 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..da48552e8 --- /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.01", + "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/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 ![X](preview.png) button to close the current app and go back to the clock. +(Widget is not visible on the clock screen) + +![Light theme screenshot](screenshot_light.png) +![Dark theme screenshot](screenshot_dark.png) \ 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;