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 4c3d3b930..3f406e6a1 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -46,3 +46,5 @@ 0.40: Bootloader now rebuilds for new firmware versions 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 63424bfbf..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="));`; @@ -38,7 +39,7 @@ LoopbackA.setConsole(true);\n`; boot += ` Bluetooth.line=""; Bluetooth.on('data',function(d) { - var l = (Bluetooth.line + d).split("\n"); + var l = (Bluetooth.line + d).split(/[\\n\\r]/); Bluetooth.line = l.pop(); l.forEach(n=>Bluetooth.emit("line",n)); }); @@ -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){ @@ -206,18 +207,40 @@ require('Storage').list(/\.boot\.js/).sort((a,b)=>{ } else if (!aPriority && bPriority){ return 1; } - return a > b; -}).forEach(bootFile=>{ + return a==b ? 0 : (a>b ? 1 : -1); +}); +// 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 4cbfd9c59..a4fa875fa 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.42", + "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 11) { ay++; _am -= 12; } + while ((m + _am) < 0) { ay--; _am += 12; } + n = new Date(_d.getTime()); + n.setMonth(m + _am); + n.setFullYear(y + ay); + return n; + }; + monthOffset = (typeof monthOffset == "undefined") ? 0 : monthOffset; + state = "calendar"; + var start = Date().getTime(); + const months = ['Jan.', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec.']; + const monthclr = ['#0f0', '#f0f', '#00f', '#ff0', '#0ff', '#fff']; + if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); + d = addMonths(Date(), monthOffset); + tdy = Date().getDate() + "." + Date().getMonth(); + newmonth=false; + c_y = 0; + g.reset(); + g.setBgColor(0); + g.clear(); + var prevmonth = addMonths(d, -1) + const today = prevmonth.getDate(); + var rD = new Date(prevmonth.getTime()); + rD.setDate(rD.getDate() - (today - 1)); + const dow = (s.FIRSTDAYOFFSET + rD.getDay()) % 7; + rD.setDate(rD.getDate() - dow); + var rDate = rD.getDate(); + bottomrightY = c_y - 3; + clrsun=s.REDSUN?'#f00':'#fff'; + clrsat=s.REDSUN?'#f00':'#fff'; + var fg=[clrsun,'#fff','#fff','#fff','#fff','#fff',clrsat]; + for (var y = 1; y <= 11; y++) { + bottomrightY += CELL_H; + bottomrightX = -2; + for (var x = 1; x <= 7; x++) { + bottomrightX += CELL2_W; + rMonth = rD.getMonth(); + rDate = rD.getDate(); + if (tdy == rDate + "." + rMonth) { + caldrawToday(rDate); + } else if (rDate == 1) { + caldrawFirst(rDate); + } else { + caldrawNormal(rDate,fg[rD.getDay()]); + } + if (newmonth && x == 7) { + caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD); + } + rD.setDate(rDate + 1); + } + } + delete addMonths; + if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start)); +} +function caldrawMonth(rDate,c,m,rD) { + g.setColor(c); + g.setFont("Vector", 18); + g.setFontAlign(-1, 1, 1); + drawyear = ((rMonth % 11) == 0) ? String(rD.getFullYear()).substr(-2) : ""; + g.drawString(m + drawyear, bottomrightX, bottomrightY - CELL_H, 1); + newmonth = false; +} +function caldrawToday(rDate) { + g.setFont("Vector", 16); + g.setFontAlign(1, 1); + g.setColor('#0f0'); + g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); + g.setColor('#000'); + g.drawString(rDate, bottomrightX, bottomrightY); +} +function caldrawFirst(rDate) { + g.flip(); + g.setFont("Vector", 16); + g.setFontAlign(1, 1); + bottomrightY += 3; + newmonth = true; + g.setColor('#0ff'); + g.fillRect(bottomrightX - CELL2_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); + g.setColor('#000'); + g.drawString(rDate, bottomrightX, bottomrightY); +} +function caldrawNormal(rDate,c) { + g.setFont("Vector", 16); + g.setFontAlign(1, 1); + g.setColor(c); + g.drawString(rDate, bottomrightX, bottomrightY);//100 +} function drawMinutes() { if (DEBUG) console.log("|-->minutes"); var d = new Date(); @@ -52,8 +153,10 @@ function drawSeconds() { if (!dimSeconds) secondInterval = setTimeout(drawSeconds, 1000); } -function drawCalendar() { +function drawWatch() { if (DEBUG) console.log("CALENDAR"); + monthOffset = 0; + state = "watch"; var d = new Date(); g.reset(); g.setBgColor(0); @@ -91,7 +194,7 @@ function drawCalendar() { var nextday = (3600 * 24) - (d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds() + 1); if (DEBUG) console.log("Next Day:" + (nextday / 3600)); if (typeof dayInterval !== "undefined") clearTimeout(dayInterval); - dayInterval = setTimeout(drawCalendar, nextday * 1000); + dayInterval = setTimeout(drawWatch, nextday * 1000); } function BTevent() { @@ -103,17 +206,87 @@ function BTevent() { } } +function input(dir) { + if (s.DRAGENABLED) { + Bangle.buzz(100,1); + console.log("swipe:"+dir); + switch (dir) { + case "r": + if (state == "calendar") { + drawWatch(); + } else { + if (s.DRAGMUSIC) { + l=require("Storage").list(RegExp("music.*app")); + if (l.length > 0) { + load(l[0]); + } else Bangle.buzz(3000,1);//not found + } + } + break; + case "l": + if (state == "calendar") { + drawWatch(); + } + break; + case "d": + if (state == "calendar") { + monthOffset--; + drawFullCalendar(monthOffset); + } else { + if (s.DRAGMESSAGES) { + l=require("Storage").list(RegExp("message.*app")); + if (l.length > 0) { + load(l[0]); + } else Bangle.buzz(3000,1);//not found + } + } + break; + case "u": + if (state == "watch") { + state = "calendar"; + drawFullCalendar(0); + } else if (state == "calendar") { + monthOffset++; + drawFullCalendar(monthOffset); + } + break; + default: + if (state == "calendar") { + drawWatch(); + } + break; + } + } +} + +let drag; +Bangle.on("drag", e => { + if (s.DRAGENABLED) { + if (!drag) { + drag = { x: e.x, y: e.y }; + } else if (!e.b) { + const dx = e.x - drag.x, dy = e.y - drag.y; + var dir = "t"; + if (Math.abs(dx) > Math.abs(dy) + 10) { + dir = (dx > 0) ? "r" : "l"; + } else if (Math.abs(dy) > Math.abs(dx) + 10) { + dir = (dy > 0) ? "d" : "u"; + } + drag = null; + input(dir); + } + } +}); + //register events Bangle.on('lock', locked => { if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); dimSeconds = locked; //dim seconds if lock=on - drawCalendar(); + drawWatch(); }); NRF.on('connect', BTevent); NRF.on('disconnect', BTevent); - dimSeconds = Bangle.isLocked(); -drawCalendar(); - +drawWatch(); Bangle.setUI("clock"); diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index ccc84a980..a42e1ad2e 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.01", + "version": "0.2", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/screenshot3.png b/apps/clockcal/screenshot3.png new file mode 100644 index 000000000..ab34f4306 Binary files /dev/null and b/apps/clockcal/screenshot3.png differ diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index cc2a78181..c4ec764c9 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -8,6 +8,9 @@ FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su REDSUN: true, // Use red color for sunday? REDSAT: true, // Use red color for saturday? + DRAGENABLED: true, //Enable drag gestures (bigger calendar etc) + DRAGMUSIC: true, //Enable drag down for music (looks for "music*app") + DRAGMESSAGES: true //Enable drag right for messages (looks for "message*app") }, require('Storage').readJSON(FILE, true) || {}); @@ -67,6 +70,30 @@ writeSettings(); } }, + 'Swipes (big cal.)?': { + value: settings.DRAGENABLED, + format: v => v ? "On" : "Off", + onchange: v => { + settings.DRAGENABLED = v; + writeSettings(); + } + }, + 'Swipes (music)?': { + value: settings.DRAGMUSIC, + format: v => v ? "On" : "Off", + onchange: v => { + settings.DRAGMUSIC = v; + writeSettings(); + } + }, + 'Swipes (messg)?': { + value: settings.DRAGMESSAGES, + format: v => v ? "On" : "Off", + onchange: v => { + settings.DRAGMESSAGES = v; + writeSettings(); + } + }, 'Load deafauls?': { value: 0, min: 0, max: 1, @@ -80,13 +107,16 @@ FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su REDSUN: true, // Use red color for sunday? REDSAT: true, // Use red color for saturday? + DRAGENABLED: true, + DRAGMUSIC: true, + DRAGMESSAGES: true }; writeSettings(); - load() + load(); } } }, - } + }; // Show the menu E.showMenu(menu); -}) +}); diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index 8f23fa9f3..a98be5c0f 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -5,3 +5,4 @@ 0.05: Add cadence sensor support 0.06: Now read wheel rev as well as cadence sensor Improve connection code +0.07: Make Bangle.js 2 compatible diff --git a/apps/cscsensor/README.md b/apps/cscsensor/README.md index 9740fd9cf..3828e8e3e 100644 --- a/apps/cscsensor/README.md +++ b/apps/cscsensor/README.md @@ -11,9 +11,9 @@ Currently the app displays the following data: - total distance traveled - an icon with the battery status of the remote sensor -Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. -If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor. -Button 2 switches between the display for cycling speed and cadence. +Button 1 (swipe up on Bangle.js 2) resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. +If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 (swipe down on Bangle.js 2) will attempt to reconnect to the sensor. +Button 2 (tap on Bangle.js 2) switches between the display for cycling speed and cadence. Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app. diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js index e2af0db16..4ebe7d57e 100644 --- a/apps/cscsensor/cscsensor.app.js +++ b/apps/cscsensor/cscsensor.app.js @@ -7,6 +7,11 @@ const SETTINGS_FILE = 'cscsensor.json'; const storage = require('Storage'); const W = g.getWidth(); const H = g.getHeight(); +const yStart = 48; +const rowHeight = (H-yStart)/6; +const yCol1 = W/2.7586; +const fontSizeLabel = W/12.632; +const fontSizeValue = W/9.2308; class CSCSensor { constructor() { @@ -22,7 +27,6 @@ class CSCSensor { this.speed = 0; this.maxSpeed = 0; this.lastSpeed = 0; - this.qUpdateScreen = true; this.lastRevsStart = -1; this.qMetric = !require("locale").speed(1).toString().endsWith("mph"); this.speedUnit = this.qMetric ? "km/h" : "mph"; @@ -49,6 +53,7 @@ class CSCSensor { toggleDisplayCadence() { this.showCadence = !this.showCadence; this.screenInit = true; + g.setBgColor(0, 0, 0); } setBatteryLevel(level) { @@ -63,14 +68,16 @@ class CSCSensor { } drawBatteryIcon() { - g.setColor(1, 1, 1).drawRect(10, 55, 20, 75).fillRect(14, 53, 16, 55).setColor(0).fillRect(11, 56, 19, 74); + g.setColor(1, 1, 1).drawRect(10*W/240, yStart+0.029167*H, 20*W/240, yStart+0.1125*H) + .fillRect(14*W/240, yStart+0.020833*H, 16*W/240, yStart+0.029167*H) + .setColor(0).fillRect(11*W/240, yStart+0.033333*H, 19*W/240, yStart+0.10833*H); if (this.batteryLevel!=-1) { if (this.batteryLevel<25) g.setColor(1, 0, 0); else if (this.batteryLevel<50) g.setColor(1, 0.5, 0); else g.setColor(0, 1, 0); - g.fillRect(11, 74-18*this.batteryLevel/100, 19, 74); + g.fillRect(11*W/240, (yStart+0.10833*H)-18*this.batteryLevel/100, 19*W/240, yStart+0.10833*H); } - else g.setFontVector(14).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16, 66); + else g.setFontVector(W/17.143).setFontAlign(0, 0, 0).setColor(0xffff).drawString("?", 16*W/240, yStart+0.075*H); } updateScreenRevs() { @@ -88,36 +95,36 @@ class CSCSensor { for (var i=0; i<6; ++i) { if ((i&1)==0) g.setColor(0, 0, 0); else g.setColor(0x30cd); - g.fillRect(0, 48+i*32, 86, 48+(i+1)*32); + g.fillRect(0, yStart+i*rowHeight, yCol1-1, yStart+(i+1)*rowHeight); if ((i&1)==1) g.setColor(0); else g.setColor(0x30cd); - g.fillRect(87, 48+i*32, 239, 48+(i+1)*32); - g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239); - g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80); + g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight); + g.setColor(0.5, 0.5, 0.5).drawRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight).drawLine(0, H-1, W-1, H-1); + g.moveTo(0, yStart+0.13333*H).lineTo(30*W/240, yStart+0.13333*H).lineTo(30*W/240, yStart).lineTo(yCol1, yStart).lineTo(yCol1, H-1).lineTo(0, H-1).lineTo(0, yStart+0.13333*H); } - g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0); - g.drawString("Time:", 87, 66); - g.drawString("Speed:", 87, 98); - g.drawString("Ave spd:", 87, 130); - g.drawString("Max spd:", 87, 162); - g.drawString("Trip:", 87, 194); - g.drawString("Total:", 87, 226); + g.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0); + g.drawString("Time:", yCol1, yStart+rowHeight/2+0*rowHeight); + g.drawString("Speed:", yCol1, yStart+rowHeight/2+1*rowHeight); + g.drawString("Avg spd:", yCol1, yStart+rowHeight/2+2*rowHeight); + g.drawString("Max spd:", yCol1, yStart+rowHeight/2+3*rowHeight); + g.drawString("Trip:", yCol1, yStart+rowHeight/2+4*rowHeight); + g.drawString("Total:", yCol1, yStart+rowHeight/2+5*rowHeight); this.drawBatteryIcon(); this.screenInit = false; } - g.setFontAlign(-1, 0, 0).setFontVector(26); - g.setColor(0x30cd).fillRect(88, 49, 238, 79); - g.setColor(0xffff).drawString(dmins+":"+dsecs, 92, 66); - g.setColor(0).fillRect(88, 81, 238, 111); - g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, 92, 98); - g.setColor(0x30cd).fillRect(88, 113, 238, 143); - g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, 92, 130); - g.setColor(0).fillRect(88, 145, 238, 175); - g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, 92, 162); - g.setColor(0x30cd).fillRect(88, 177, 238, 207); - g.setColor(0xffff).drawString(ddist + " " + this.distUnit, 92, 194); - g.setColor(0).fillRect(88, 209, 238, 238); - g.setColor(0xffff).drawString(tdist + " " + this.distUnit, 92, 226); + g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue); + g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*0, 238, 47+1*rowHeight); + g.setColor(0xffff).drawString(dmins+":"+dsecs, yCol1+5, 50+rowHeight/2+0*rowHeight); + g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight); + g.setColor(0xffff).drawString(dspeed+" "+this.speedUnit, yCol1+5, 50+rowHeight/2+1*rowHeight); + g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*2, 238, 47+3*rowHeight); + g.setColor(0xffff).drawString(avespeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+2*rowHeight); + g.setColor(0).fillRect(yCol1+1, 49+rowHeight*3, 238, 47+4*rowHeight); + g.setColor(0xffff).drawString(maxspeed + " " + this.speedUnit, yCol1+5, 50+rowHeight/2+3*rowHeight); + g.setColor(0x30cd).fillRect(yCol1+1, 49+rowHeight*4, 238, 47+5*rowHeight); + g.setColor(0xffff).drawString(ddist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+4*rowHeight); + g.setColor(0).fillRect(yCol1+1, 49+rowHeight*5, 238, 47+6*rowHeight); + g.setColor(0xffff).drawString(tdist + " " + this.distUnit, yCol1+5, 50+rowHeight/2+5*rowHeight); } updateScreenCadence() { @@ -125,21 +132,21 @@ class CSCSensor { for (var i=0; i<2; ++i) { if ((i&1)==0) g.setColor(0, 0, 0); else g.setColor(0x30cd); - g.fillRect(0, 48+i*32, 86, 48+(i+1)*32); + g.fillRect(0, yStart+i*rowHeight, yCol1-1, yStart+(i+1)*rowHeight); if ((i&1)==1) g.setColor(0); else g.setColor(0x30cd); - g.fillRect(87, 48+i*32, 239, 48+(i+1)*32); - g.setColor(0.5, 0.5, 0.5).drawRect(87, 48+i*32, 239, 48+(i+1)*32).drawLine(0, 239, 239, 239);//.drawRect(0, 48, 87, 239); - g.moveTo(0, 80).lineTo(30, 80).lineTo(30, 48).lineTo(87, 48).lineTo(87, 239).lineTo(0, 239).lineTo(0, 80); + g.fillRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight); + g.setColor(0.5, 0.5, 0.5).drawRect(yCol1, yStart+i*rowHeight, H-1, yStart+(i+1)*rowHeight).drawLine(0, H-1, W-1, H-1); + g.moveTo(0, yStart+0.13333*H).lineTo(30*W/240, yStart+0.13333*H).lineTo(30*W/240, yStart).lineTo(yCol1, yStart).lineTo(yCol1, H-1).lineTo(0, H-1).lineTo(0, yStart+0.13333*H); } - g.setFontAlign(1, 0, 0).setFontVector(19).setColor(1, 1, 0); - g.drawString("Cadence:", 87, 98); + g.setFontAlign(1, 0, 0).setFontVector(fontSizeLabel).setColor(1, 1, 0); + g.drawString("Cadence:", yCol1, yStart+rowHeight/2+1*rowHeight); this.drawBatteryIcon(); this.screenInit = false; } - g.setFontAlign(-1, 0, 0).setFontVector(26); - g.setColor(0).fillRect(88, 81, 238, 111); - g.setColor(0xffff).drawString(Math.round(this.cadence), 92, 98); + g.setFontAlign(-1, 0, 0).setFontVector(fontSizeValue); + g.setColor(0).fillRect(yCol1+1, 49+rowHeight*1, 238, 47+2*rowHeight); + g.setColor(0xffff).drawString(Math.round(this.cadence), yCol1+5, 50+rowHeight/2+1*rowHeight); } updateScreen() { @@ -163,45 +170,45 @@ class CSCSensor { } this.lastCrankRevs = crankRevs; this.lastCrankTime = crankTime; - } - // wheel revolution - var wheelRevs = event.target.value.getUint32(1, true); - var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); - if (dRevs>0) { - qChanged = true; - this.totaldist += dRevs*this.wheelCirc/63360.0; - if ((this.totaldist-this.settings.totaldist)>0.1) { - this.settings.totaldist = this.totaldist; - storage.writeJSON(SETTINGS_FILE, this.settings); + } else { + // wheel revolution + var wheelRevs = event.target.value.getUint32(1, true); + var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); + if (dRevs>0) { + qChanged = true; + this.totaldist += dRevs*this.wheelCirc/63360.0; + if ((this.totaldist-this.settings.totaldist)>0.1) { + this.settings.totaldist = this.totaldist; + storage.writeJSON(SETTINGS_FILE, this.settings); + } } - } - this.lastRevs = wheelRevs; - if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; - var wheelTime = event.target.value.getUint16(5, true); - var dT = (wheelTime-this.lastTime)/1024; - var dBT = (Date.now()-this.lastBangleTime)/1000; - this.lastBangleTime = Date.now(); - if (dT<0) dT+=64; - if (Math.abs(dT-dBT)>3) dT = dBT; - this.lastTime = wheelTime; - this.speed = this.lastSpeed; - if (dRevs>0 && dT>0) { - this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; - this.speedFailed = 0; - this.movingTime += dT; - } - else { - this.speedFailed++; - qChanged = false; - if (this.speedFailed>3) { - this.speed = 0; - qChanged = (this.lastSpeed>0); + this.lastRevs = wheelRevs; + if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; + var wheelTime = event.target.value.getUint16(5, true); + var dT = (wheelTime-this.lastTime)/1024; + var dBT = (Date.now()-this.lastBangleTime)/1000; + this.lastBangleTime = Date.now(); + if (dT<0) dT+=64; + if (Math.abs(dT-dBT)>3) dT = dBT; + this.lastTime = wheelTime; + this.speed = this.lastSpeed; + if (dRevs>0 && dT>0) { + this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; + this.speedFailed = 0; + this.movingTime += dT; + } else if (!this.showCadence) { + this.speedFailed++; + qChanged = false; + if (this.speedFailed>3) { + this.speed = 0; + qChanged = (this.lastSpeed>0); + } } + this.lastSpeed = this.speed; + if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; } - this.lastSpeed = this.speed; - if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; } - if (qChanged && this.qUpdateScreen) this.updateScreen(); + if (qChanged) this.updateScreen(); } } @@ -253,9 +260,9 @@ E.on('kill',()=>{ }); NRF.on('disconnect', connection_setup); // restart if disconnected Bangle.setUI("updown", d=>{ - if (d<0) { mySensor.reset(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); } - if (d==0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); } - if (d>0) { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); } + if (d<0) { mySensor.reset(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } + else if (d>0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); } + else { mySensor.toggleDisplayCadence(); g.clearRect(0, yStart, W, H); mySensor.updateScreen(); } }); Bangle.loadWidgets(); diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json index af338c59e..4006789ef 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -2,11 +2,11 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.06", + "version": "0.07", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"cscsensor.app.js","url":"cscsensor.app.js"}, diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index 115c8f2ff..26396b75c 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -2,3 +2,4 @@ 0.02: added settings menu to change color 0.03: fix metadata.json to allow setting as clock 0.04: added heart rate which is switched on when cycled to it through up/down touch on rhs +0.05: changed text to uppercase, just looks better, removed colons on text diff --git a/apps/daisy/app.js b/apps/daisy/app.js index 01d177a32..cf0287616 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -109,10 +109,10 @@ function updateSunRiseSunSet(now, lat, lon, line){ const infoData = { ID_DATE: { calc: () => {var d = (new Date()).toString().split(" "); return d[2] + ' ' + d[1] + ' ' + d[3];} }, ID_DAY: { calc: () => {var d = require("locale").dow(new Date()).toLowerCase(); return d[0].toUpperCase() + d.substring(1);} }, - ID_SR: { calc: () => 'Sunrise: ' + sunRise }, - ID_SS: { calc: () => 'Sunset: ' + sunSet }, - ID_STEP: { calc: () => 'Steps: ' + getSteps() }, - ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' }, + ID_SR: { calc: () => 'Sunrise ' + sunRise }, + ID_SS: { calc: () => 'Sunset ' + sunSet }, + ID_STEP: { calc: () => 'Steps ' + getSteps() }, + ID_BATT: { calc: () => 'Battery ' + E.getBattery() + '%' }, ID_HRM: { calc: () => hrmCurrent } }; @@ -158,7 +158,7 @@ function drawInfo() { g.setColor('#f00'); // red drawHeartIcon(); } else { - g.drawString((infoData[infoMode].calc()), w/2, infoLine); + g.drawString((infoData[infoMode].calc().toUpperCase()), w/2, infoLine); } } diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index 5e53f2d5e..15a24592c 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.04", + "version":"0.05", "dependencies": {"mylocation":"app"}, "description": "A clock based on the Pastel clock with large ring guage for steps", "icon": "app.png", diff --git a/apps/doztime/ChangeLog b/apps/doztime/ChangeLog index 6c4a25b26..77d82eff9 100644 --- a/apps/doztime/ChangeLog +++ b/apps/doztime/ChangeLog @@ -2,3 +2,6 @@ 0.02: added emulator capability and display of widgets 0.03: bug of advancing time fixed; doztime now correct within ca. 1 second 0.04: changed time colour from slightly off white to pure white +0.05: extraneous comments and code removed + display improved + now supports Adjust Clock widget, if installed diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js index b77e5201a..8a315118f 100644 --- a/apps/doztime/app-bangle2.js +++ b/apps/doztime/app-bangle2.js @@ -1,23 +1,23 @@ // Positioning values for graphics buffers const g_height = 80; // total graphics height -const g_x_off = 0; // position from left was 16, then 8 here -const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240 +const g_x_off = 0; // position from left +const g_y_off = (180 - g_height)/2; // vertical center for graphics region const g_width = 240 - 2 * g_x_off; // total graphics width -const g_height_d = 28; // height of date region was 32 +const g_height_d = 28; // height of date region const g_y_off_d = 0; // y position of date region within graphics region -const spacing = 0; // space between date and time in graphics region +const spacing = 6; // space between date and time in graphics region const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region -const g_height_t = 44; // height of time region was 48 +const g_height_t = 44; // height of time region // Other vars const A1 = [30,30,30,30,31,31,31,31,31,31,30,30]; const B1 = [30,30,30,30,30,31,31,31,31,31,30,30]; const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; const timeColour = "#ffffff"; -const dateColours = ["#ff0000","#ffa500","#ffff00","#00b800","#8383ff","#ff00ff","#ff0080"]; //blue was 0000ff -const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20 -const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20 -const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30 +const dateColours = ["#ff0000","#ff8000","#ffff00","#00ff00","#0080ff","#ff00ff","#ffffff"]; +const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line +const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line +const time5 = {"size":42,"pt0":[39-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line const time6 = {"size":42,"pt0":[26-g_x_off,24],"step":[26,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30 const baseYear = 11584; const baseDate = Date(2020,11,21); // month values run from 0 to 11 @@ -42,28 +42,25 @@ var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true}); g.clear(); // start with blank screen g.flip = function() { - g.setBgColor(0,0,0); + g.setBgColor(0,0,0); g.setColor(dateColour); - g.drawImage( - { - width:g_width, - height:g_height_d, - buffer:g_d.buffer - }, g_x_off, g_y_off + g_y_off_d); - g.setColor(timeColour2); - g.drawImage( - { - width:g_width, - height:g_height_t, - buffer:g_t.buffer - }, g_x_off, g_y_off + g_y_off_t); + g.drawImage( + { + width:g_width, + height:g_height_d, + buffer:g_d.buffer + }, g_x_off, g_y_off + g_y_off_d); + g.setColor(timeColour2); + g.drawImage( + { + width:g_width, + height:g_height_t, + buffer:g_t.buffer + }, g_x_off, g_y_off + g_y_off_t); }; -setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1 -setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2 -//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); -//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true}); -//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true}); +setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); +setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); Bangle.on('touch', function(button, xy) { //from Gordon Williams if (button==1) toggleTimeDigits(); @@ -71,10 +68,10 @@ Bangle.on('touch', function(button, xy) { //from Gordon Williams }); function buildSequence(targ){ - for(let 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/info/README.md b/apps/info/README.md index 007a9794e..32920cb75 100644 --- a/apps/info/README.md +++ b/apps/info/README.md @@ -7,7 +7,7 @@ screen. Very useful if combined with pattern launcher ;) ![](screenshot_1.png) ![](screenshot_2.png) -![](screenshot_2.png) +![](screenshot_3.png) ## Contributors diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 02492d5ea..c39e7d6fe 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -38,3 +38,4 @@ Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured 0.24: Remove left-over debug statement 0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550) +0.26: Setting to auto-open music diff --git a/apps/messages/README.md b/apps/messages/README.md index cce4391e7..655c549b9 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -13,12 +13,13 @@ and `Messages`: * `Vibrate` - This is the pattern of buzzes that should be made when a new message is received * `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds -* `Unread Timer` - when a new message is received we go into the Messages app. +* `Unread Timer` - When a new message is received we go into the Messages app. If there is no user input for this amount of time then the app will exit and return to the clock where a ringing bell will be shown in the Widget bar. -* `Min Font` - the minimum font size used when displaying messages on the screen. A bigger font +* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font is chosen if there isn't much message text, but this specifies the smallest the font should get before it starts getting clipped. +* `Auto-Open Music` - Should the app automatically open when the phone starts playing music? ## New Messages diff --git a/apps/messages/app.js b/apps/messages/app.js index 3f3b80080..403f9b5d8 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -26,6 +26,8 @@ var fontSmall = "6x8"; var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; +var active; // active screen +var openMusic = false; // go back to music screen after we handle something else? // hack for 2v10 firmware's lack of ':size' font handling try { g.setFont("6x8:2"); @@ -50,10 +52,14 @@ var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; if (!Array.isArray(MESSAGES)) MESSAGES=[]; var onMessagesModified = function(msg) { // TODO: if new, show this new one - if (msg && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { + if (msg && msg.id!=="music" && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { if (WIDGETS["messages"]) WIDGETS["messages"].buzz(); else Bangle.buzz(); } + if (msg && msg.id=="music") { + if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to + if (active!="music") return; // don't open music over other screens + } showMessage(msg&&msg.id); }; function saveMessages() { @@ -135,6 +141,7 @@ function getMessageImageCol(msg,def) { } function showMapMessage(msg) { + active = "map"; var m; var distance, street, target, eta; m=msg.title.match(/(.*) - (.*)/); @@ -168,12 +175,14 @@ function showMapMessage(msg) { msg.new = false; saveMessages(); layout = undefined; - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0}); }); } +var updateLabelsInterval; function showMusicMessage(msg) { - var updateLabelsInterval; + active = "music"; + openMusic = msg.state=="play"; var trackScrollOffset = 0; var artistScrollOffset = 0; var albumScrollOffset = 0; @@ -194,10 +203,14 @@ function showMusicMessage(msg) { function back() { clearInterval(updateLabelsInterval); + updateLabelsInterval = undefined; + openMusic = false; + var wasNew = msg.new; msg.new = false; saveMessages(); layout = undefined; - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0}); + else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); } function updateLabels() { trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13); @@ -243,6 +256,7 @@ function showMusicMessage(msg) { } function showMessageScroller(msg) { + active = "scroller"; var bodyFont = fontBig; g.setFont(bodyFont); var lines = []; @@ -272,6 +286,7 @@ function showMessageScroller(msg) { } function showMessageSettings(msg) { + active = "settings"; E.showMenu({"":{"title":/*LANG*/"Message"}, "< Back" : () => showMessage(msg.id), /*LANG*/"View Message" : () => { @@ -280,12 +295,12 @@ function showMessageSettings(msg) { /*LANG*/"Delete" : () => { MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); saveMessages(); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Mark Unread" : () => { msg.new = true; saveMessages(); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Delete all messages" : () => { E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { @@ -293,7 +308,7 @@ function showMessageSettings(msg) { MESSAGES = []; saveMessages(); } - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }); }, }); @@ -301,15 +316,20 @@ function showMessageSettings(msg) { function showMessage(msgid) { var msg = MESSAGES.find(m=>m.id==msgid); - if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); // go home if no message found - if (msg.src=="Maps") { - cancelReloadTimeout(); // don't auto-reload to clock now - return showMapMessage(msg); + if (updateLabelsInterval) { + clearInterval(updateLabelsInterval); + updateLabelsInterval=undefined; } + if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found if (msg.id=="music") { cancelReloadTimeout(); // don't auto-reload to clock now return showMusicMessage(msg); } + if (msg.src=="Maps") { + cancelReloadTimeout(); // don't auto-reload to clock now + return showMapMessage(msg); + } + active = "message"; // Normal text message display var title=msg.title, titleFont = fontLarge, lines; if (title) { @@ -342,7 +362,7 @@ function showMessage(msgid) { function goBack() { msg.new = false; saveMessages(); // read mail cancelReloadTimeout(); // don't auto-reload to clock now - checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); } var buttons = [ {type:"btn", src:getBackImage(), cb:goBack} // back @@ -353,7 +373,7 @@ function showMessage(msgid) { msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); }}); } if (msg.negative) { @@ -362,7 +382,7 @@ function showMessage(msgid) { msg.new = false; saveMessages(); cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,false); - checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); }}); } @@ -411,15 +431,19 @@ function checkMessages(options) { return load(); } // we have >0 messages - var newMessages = MESSAGES.filter(m=>m.new); + var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music"); // If we have a new message, show it if (options.showMsgIfUnread && newMessages.length) return showMessage(newMessages[0].id); + // no new messages: show playing music? (only if we have playing music to show) + if (options.openMusic && MESSAGES.some(m=>m.id=="music" && m.track && m.state=="play")) + return showMessage('music'); // no new messages - go to clock? if (options.clockIfAllRead && newMessages.length==0) return load(); // we don't have to time out of this screen... cancelReloadTimeout(); + active = "main"; // Otherwise show a menu E.showScroller({ h : 48, @@ -482,5 +506,7 @@ setTimeout(() => { print("Message not seen - reloading"); load(); }, unreadTimeoutSecs*1000); - checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1}); + // only openMusic on launch if music is new + var newMusic = MESSAGES.some(m=>m.id==="music"&&m.new); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1,openMusic:newMusic&&settings.openMusic}); },10); // if checkMessages wants to 'load', do that diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 32dff78ba..0f514a73d 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -1,3 +1,10 @@ +function openMusic() { + // only read settings file for first music message + if ("undefined"==typeof exports._openMusic) { + exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic); + } + return exports._openMusic; +} /* Push a new message onto messages queue, event is: {t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool} {t:"add",id:int, id:"music", state, artist, track, etc} // add new @@ -26,6 +33,9 @@ exports.pushMessage = function(event) { messages.unshift(event); // add new messages to the beginning } else Object.assign(messages[mIdx], event); + if (event.id=="music" && messages[mIdx].state=="play") { + messages[mIdx].new = true; // new track, or playback (re)started + } } require("Storage").writeJSON("messages.json",messages); // if in app, process immediately @@ -34,8 +44,12 @@ exports.pushMessage = function(event) { if (event.t=="remove" && !messages.some(m=>m.new)) { if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.hide(); } - // ok, saved now - we only care if it's new - if (event.t!="add") { + // ok, saved now + if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) { + // just load the app to display music: no buzzing + load("messages.app.js"); + } else if (event.t!="add") { + // we only care if it's new return; } else if(event.new == false) { return; diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index ff6a078a5..a3d3f4179 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,7 +1,7 @@ { "id": "messages", "name": "Messages", - "version": "0.25", + "version": "0.26", "description": "App to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", diff --git a/apps/messages/settings.js b/apps/messages/settings.js index 622177440..99843602b 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -4,6 +4,7 @@ if (settings.vibrate===undefined) settings.vibrate="."; if (settings.repeat===undefined) settings.repeat=4; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; + settings.openMusic=!!settings.openMusic; settings.maxUnreadTimeout=240; return settings; } @@ -43,6 +44,11 @@ format: v => [/*LANG*/"Small",/*LANG*/"Medium"][v], onchange: v => updateSetting("fontSize", v) }, + /*LANG*/'Auto-Open Music': { + value: !!settings().openMusic, + format: v => v?/*LANG*/'Yes':/*LANG*/'No', + onchange: v => updateSetting("openMusic", v) + }, }; E.showMenu(mainmenu); }) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index ad8d58c40..7abb415c3 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -50,5 +50,5 @@ message but then the watch was never viewed. In that case we don't want to buzz but should still show that there are unread messages. */ if (global.MESSAGES===undefined) (function() { var messages = require("Storage").readJSON("messages.json",1)||[]; - if (messages.some(m=>m.new)) WIDGETS["messages"].show(true); + if (messages.some(m=>m.new&&m.id!="music")) WIDGETS["messages"].show(true); })(); diff --git a/apps/powermanager/ChangeLog b/apps/powermanager/ChangeLog index 5560f00bc..8ccf678de 100644 --- a/apps/powermanager/ChangeLog +++ b/apps/powermanager/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Allow forcing monotonic battery voltage/percentage diff --git a/apps/powermanager/README.md b/apps/powermanager/README.md index f2cfcdf3e..434ec814e 100644 --- a/apps/powermanager/README.md +++ b/apps/powermanager/README.md @@ -1,6 +1,10 @@ # Power manager -Manages settings for charging. You can set a warning threshold to be able to disconnect the charger at a given percentage. Also allows to set the battery calibration offset. +Manages settings for charging. +Features: +* Warning threshold to be able to disconnect the charger at a given percentage +* Set the battery calibration offset. +* Force monotonic battery percentage or voltage ## Internals diff --git a/apps/powermanager/boot.js b/apps/powermanager/boot.js index ff4ba8932..077e24413 100644 --- a/apps/powermanager/boot.js +++ b/apps/powermanager/boot.js @@ -26,4 +26,26 @@ Bangle.on("charging",handleCharging); handleCharging(Bangle.isCharging()); } + + if (settings.forceMonoPercentage){ + var p = (E.getBattery()+E.getBattery()+E.getBattery()+E.getBattery())/4; + var op = E.getBattery; + E.getBattery = function() { + var current = Math.round((op()+op()+op()+op())/4); + if (Bangle.isCharging() && current > p) p = current; + if (!Bangle.isCharging() && current < p) p = current; + return p; + }; + } + + if (settings.forceMonoVoltage){ + var v = (NRF.getBattery()+NRF.getBattery()+NRF.getBattery()+NRF.getBattery())/4; + var ov = NRF.getBattery; + NRF.getBattery = function() { + var current = (ov()+ov()+ov()+ov())/4; + if (Bangle.isCharging() && current > v) v = current; + if (!Bangle.isCharging() && current < v) v = current; + return v; + }; + } })(); diff --git a/apps/powermanager/default.json b/apps/powermanager/default.json index a6d8412b2..6c929dc38 100644 --- a/apps/powermanager/default.json +++ b/apps/powermanager/default.json @@ -1,4 +1,6 @@ { "warnEnabled": false, - "warn": 96 + "warn": 96, + "forceMonoVoltage": false, + "forceMonoPercentage": false } diff --git a/apps/powermanager/metadata.json b/apps/powermanager/metadata.json index 3ad31ba1e..2bb531099 100644 --- a/apps/powermanager/metadata.json +++ b/apps/powermanager/metadata.json @@ -2,7 +2,7 @@ "id": "powermanager", "name": "Power Manager", "shortName": "Power Manager", - "version": "0.01", + "version": "0.02", "description": "Allow configuration of warnings and thresholds for battery charging and display.", "icon": "app.png", "type": "bootloader", diff --git a/apps/powermanager/settings.js b/apps/powermanager/settings.js index c8aa057fa..8af873e5f 100644 --- a/apps/powermanager/settings.js +++ b/apps/powermanager/settings.js @@ -24,6 +24,20 @@ 'title': 'Power Manager' }, '< Back': back, + 'Monotonic percentage': { + value: !!settings.forceMonoPercentage, + format: v => settings.forceMonoPercentage ? "On" : "Off", + onchange: v => { + writeSettings("forceMonoPercentage", v); + } + }, + 'Monotonic voltage': { + value: !!settings.forceMonoVoltage, + format: v => settings.forceMonoVoltage ? "On" : "Off", + onchange: v => { + writeSettings("forceMonoVoltage", v); + } + }, 'Charge warning': function() { E.showMenu(submenu_chargewarn); }, diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog index b9c26b4e3..b80dfef94 100644 --- a/apps/rebble/ChangeLog +++ b/apps/rebble/ChangeLog @@ -2,3 +2,4 @@ 0.02: Fix typo to Purple 0.03: Added dependancy on Pedometer Widget 0.04: Fixed icon and png to 48x48 pixels +0.05: added charging icon \ No newline at end of file diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json index 212a7b5b3..b26fb6a27 100644 --- a/apps/rebble/metadata.json +++ b/apps/rebble/metadata.json @@ -2,7 +2,7 @@ "id": "rebble", "name": "Rebble Clock", "shortName": "Rebble", - "version": "0.04", + "version": "0.05", "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion", "readme": "README.md", "icon": "rebble.png", diff --git a/apps/rebble/rebble.app.js b/apps/rebble/rebble.app.js index d186ea8ec..7c7d57939 100644 --- a/apps/rebble/rebble.app.js +++ b/apps/rebble/rebble.app.js @@ -204,6 +204,14 @@ function drawBattery(x,y,wi,hi) { g.setColor(g.theme.fg); g.fillRect(x+wi-3,y+2+(((hi - 1)/2)-1),x+wi-2,y+2+(((hi - 1)/2)-1)+4); // contact g.fillRect(x+3, y+5, x +4 + E.getBattery()*(wi-12)/100, y+hi-1); // the level + + if( Bangle.isCharging() ) + { + g.setBgColor(settings.bg); + image = ()=> { return require("heatshrink").decompress(atob("j8OwMB/4AD94DC44DCwP//n/gH//EOgE/+AdBh/gAYMH4EAvkDAYP/+/AFAX+FgfzGAnAA=="));} + g.drawImage(image(),x+3,y+4); + } + } function getSteps() { @@ -270,3 +278,14 @@ for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} loadSettings(); loadLocation(); draw(); // queues the next draw for a minutes time +Bangle.on('charging', function(charging) { + //redraw the sidebar ( with the battery ) + switch(sideBar) { + case 0: + drawSideBar1(); + break; + case 1: + drawSideBar2(); + break; + } +}); \ No newline at end of file diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 963944144..90937e160 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -16,3 +16,4 @@ 0.10: Fix broken recorder settings (when launched from settings app) 0.11: Fix KML and GPX export when there is no GPS data 0.12: Fix 'Back' label positioning on track/graph display, make translateable +0.13: Fix for when widget is used before app diff --git a/apps/recorder/app-settings.json b/apps/recorder/app-settings.json index 4a3117a17..7410af213 100644 --- a/apps/recorder/app-settings.json +++ b/apps/recorder/app-settings.json @@ -1,6 +1,6 @@ { "recording":false, - "file":"record.log0.csv", + "file":"recorder.log0.csv", "period":10, "record" : ["gps"] } diff --git a/apps/recorder/app.js b/apps/recorder/app.js index d900c12c1..e5e732fe3 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -31,7 +31,12 @@ function updateSettings() { } function getTrackNumber(filename) { - return parseInt(filename.match(/^recorder\.log(.*)\.csv$/)[1]||0); + var trackNum = 0; + var matches = filename.match(/^recorder\.log(.*)\.csv$/); + if (matches) { + trackNum = parseInt(matches[1]||0); + } + return trackNum; } function showMainMenu() { diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index 09873dada..e2400603d 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.12", + "version": "0.13", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -15,5 +15,5 @@ {"name":"recorder.wid.js","url":"widget.js"}, {"name":"recorder.settings.js","url":"settings.js"} ], - "data": [{"name":"recorder.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] + "data": [{"name":"recorder.json","url":"app-settings.json"},{"wildcard":"recorder.log?.csv","storageFile":true}] } diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index e10c99c0c..221bc6c1a 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -11,7 +11,7 @@ settings.recording = false; return settings; } - + function updateSettings(settings) { require("Storage").writeJSON("recorder.json", settings); if (WIDGETS["recorder"]) WIDGETS["recorder"].reload(); @@ -233,7 +233,9 @@ Bangle.drawWidgets(); // relayout all widgets },setRecording:function(isOn) { var settings = loadSettings(); - if (isOn && !settings.recording && require("Storage").list(settings.file).length){ + if (isOn && !settings.recording && !settings.file) { + settings.file = "recorder.log0.csv"; + } else if (isOn && !settings.recording && require("Storage").list(settings.file).length){ var logfiles=require("Storage").list(/recorder.log.*/); var maxNumber=0; for (var c of logfiles){ @@ -247,9 +249,11 @@ var buttons={Yes:"yes",No:"no"}; if (newFileName) buttons["New"] = "new"; var prompt = E.showPrompt("Overwrite\nLog " + settings.file.match(/\d+/)[0] + "?",{title:"Recorder",buttons:buttons}).then(selection=>{ - if (selection=="no") return false; // just cancel - if (selection=="yes") require("Storage").open(settings.file,"r").erase(); - if (selection=="new"){ + if (selection==="no") return false; // just cancel + if (selection==="yes") { + require("Storage").open(settings.file,"r").erase(); + } + if (selection==="new"){ settings.file = newFileName; updateSettings(settings); } diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index 0a697ecb9..401a68de9 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -6,4 +6,6 @@ 0.05: exstats updated so update 'distance' label is updated, option for 'speed' 0.06: Add option to record a run using the recorder app automatically 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 \ No newline at end of file +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/app.js b/apps/run/app.js index 45daf878e..d066c8b1f 100644 --- a/apps/run/app.js +++ b/apps/run/app.js @@ -66,6 +66,9 @@ function onStartStop() { } } + if (!prepPromises.length) // fix for Promise.all bug in 2v12 + prepPromises.push(Promise.resolve()); + Promise.all(prepPromises) .then(() => { if (running) { diff --git a/apps/run/metadata.json b/apps/run/metadata.json index 8f139c2d5..51239d297 100644 --- a/apps/run/metadata.json +++ b/apps/run/metadata.json @@ -1,6 +1,6 @@ { "id": "run", "name": "Run", - "version":"0.08", + "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/speedalt/ChangeLog b/apps/speedalt/ChangeLog index 0550f9b86..78c14594b 100644 --- a/apps/speedalt/ChangeLog +++ b/apps/speedalt/ChangeLog @@ -9,3 +9,4 @@ 0.09: Add third screen mode with large clock and waypoint selection display to ease visibility in bright daylight. 0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings. 0.11: Now also runs on Bangle.js 2 with basic functionality +0.12: Full functionality on Bangle.js 2: Bangle.js 1 buttons mapped to touch areas. diff --git a/apps/speedalt/README.md b/apps/speedalt/README.md index c21828aff..6f0d4efe5 100644 --- a/apps/speedalt/README.md +++ b/apps/speedalt/README.md @@ -2,23 +2,21 @@ You can switch between three display modes. One showing speed and altitude (A), one showing speed and distance to waypoint (D) and a large dispay of time and selected waypoint. -*Note for **Bangle.js 2:** Currently only the BTN3 functionality is working with the Bangle.js 2 button.* - Within the [A]ltitude and [D]istance displays modes one figure is displayed on the watch face using the largest possible characters depending on the number of digits. The other is in a smaller characters below that. Both are always visible. You can display the current or maximum observed speed/altitude values. Current time is always displayed. The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information. ## Buttons and Controls -BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint +*(Mapping for **Bangle.js 2**: BTN2 = Touch upper right side; BTN3 = Touch lower right side; BTN4 = Touch left side)* -***Bangle.js 2:** Currently only this button function is working* +BTN3 : Cycles the modes between Speed+[A]ltitude, Speed+[D]istance and large Time/Waypoint ### [A]ltitude mode BTN1 : Short press < 2 secs toggles the displays between showing the current speed/alt values or the maximum speed/alt values recorded. -BTN1 : Long press > 2 secs resets the recorded maximum values. +BTN1 : Long press > 2 secs resets the recorded maximum values. *(Bangle.js 2: Long press > 0.4 secs)* ### [D]istance mode @@ -32,7 +30,7 @@ BTN1 : Select next waypoint. BTN2 : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts. -BTN3 : Long press exit and return to watch. +BTN3 : Long press exit and return to watch. *(Bangle.js 2: Long press BTN > 2 secs)* BTN4 : Left Display Tap : Swaps which figure is in the large display. You can have either speed or [A]ltitude/[D]istance on the large primary display. diff --git a/apps/speedalt/app.js b/apps/speedalt/app.js index f979762f1..79db932db 100644 --- a/apps/speedalt/app.js +++ b/apps/speedalt/app.js @@ -349,7 +349,7 @@ function drawSecondary(n,u) { s = 30; // Font size if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); - buf.drawString(u,xu - (BANGLEJS2*20),screenH_TwoThirds-25); + buf.drawString(u,xu - (BANGLEJS2*xu/5),screenH_TwoThirds-25); } function drawTime() { @@ -391,7 +391,7 @@ function drawWP() { // from waypoints.json - see README.md buf.setFontAlign(-1,1); //left, bottom if (BANGLEJS2) s *= fontFactorB2; buf.setFontVector(s); - buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 20)); + buf.drawString(nm.substring(0,6),72,screenH_TwoThirds-(BANGLEJS2 * 15)); } if ( cfg.modeA == 2 ) { // clock/large mode @@ -421,7 +421,7 @@ function drawSats(sats) { buf.drawString('A',screenW,140-(BANGLEJS2 * 40)); if ( showMax ) { buf.setFontAlign(0,1); //centre, bottom - buf.drawString('MAX',120,164); + buf.drawString('MAX',screenW_Half,screenH_TwoThirds + 4); } } if ( cfg.modeA == 0 ) buf.drawString('D',screenW,140-(BANGLEJS2 * 40)); @@ -536,22 +536,18 @@ function onGPS(fix) { } -function setButtons(){ -if (!BANGLEJS2) { // Buttons for Bangle.js - // Spd+Dist : Select next waypoint - setWatch(function(e) { - var dur = e.time - e.lastTime; - if ( cfg.modeA == 1 ) { - // Spd+Alt mode - Switch between fix and MAX - if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display - else { max.spd = 0; max.alt = 0; } // Long press resets max values. - } - else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint - onGPS(lf); - }, BTN1, { edge:"falling",repeat:true}); - // Power saving on/off - setWatch(function(e){ +function btn1press(longpress) { + if(emulator) console.log("Btn1, long="+longpress); + if ( cfg.modeA == 1 ) { // Spd+Alt mode - Switch between fix and MAX + if ( !longpress ) showMax = !showMax; // Short press toggle fix/max display + else { max.spd = 0; max.alt = 0; } // Long press resets max values. + } + else nxtWp(1); // Spd+Dist or Clock mode - Select next waypoint + onGPS(lf); + } +function btn2press(){ + if(emulator) console.log("Btn2"); pwrSav=!pwrSav; if ( pwrSav ) { LED1.reset(); @@ -564,52 +560,51 @@ if (!BANGLEJS2) { // Buttons for Bangle.js Bangle.setLCDPower(1); LED1.set(); } - }, BTN2, {repeat:true,edge:"falling"}); - - // Toggle between alt or dist - setWatch(function(e){ - cfg.modeA = cfg.modeA+1; - if ( cfg.modeA > 2 ) cfg.modeA = 0; - savSettings(); - onGPS(lf); - }, BTN3, {repeat:true,edge:"falling"}); - - // Touch left screen to toggle display - setWatch(function(e){ - cfg.primSpd = !cfg.primSpd; - savSettings(); - onGPS(lf); // Update display - }, BTN4, {repeat:true,edge:"falling"}); - -} else { // Buttons for Bangle.js 2 - setWatch(function(e){ // Bangle.js BTN3 + } +function btn3press(){ + if(emulator) console.log("Btn3"); cfg.modeA = cfg.modeA+1; if ( cfg.modeA > 2 ) cfg.modeA = 0; if(emulator)console.log("cfg.modeA="+cfg.modeA); savSettings(); onGPS(lf); - }, BTN1, {repeat:true,edge:"falling"}); - -/* Bangle.on('tap', function(data) { // data - {dir, double, x, y, z} + } +function btn4press(){ + if(emulator) console.log("Btn4"); cfg.primSpd = !cfg.primSpd; - if(emulator)console.log("!cfg.primSpd"); - }); */ + savSettings(); + onGPS(lf); // Update display + } -/* Bangle.on('swipe', function(dir) { - if (dir < 0) { // left: Bangle.js BTN3 - cfg.modeA = cfg.modeA+1; - if ( cfg.modeA > 2 ) cfg.modeA = 0; - if(emulator)console.log("cfg.modeA="+cfg.modeA); - } + +function setButtons(){ +if (!BANGLEJS2) { // Buttons for Bangle.js 1 + setWatch(function(e) { + btn1press(( e.time - e.lastTime) > 2); // > 2 sec. is long press + }, BTN1, { edge:"falling",repeat:true}); + + // Power saving on/off (red dot visible if off) + setWatch(btn2press, BTN2, {repeat:true,edge:"falling"}); + + // Toggle between alt or dist + setWatch(btn3press, BTN3, {repeat:true,edge:"falling"}); + + // Touch left screen to toggle display + setWatch(btn4press, BTN4, {repeat:true,edge:"falling"}); + +} else { // Buttons for Bangle.js 2 + setWatch(function(e) { + btn1press(( e.time - e.lastTime) > 0.4); // > 0.4 sec. is long press + }, BTN1, { edge:"falling",repeat:true}); + + Bangle.on('touch', function(btn_l_r, e) { + if(e.x < screenW_Half) btn4press(); else - { // right: Bangle.js BTN4 - cfg.primSpd = !cfg.primSpd; - if(emulator)console.log("!cfg.primSpd"); - } + if (e.y < screenH_Half) + btn2press(); + else + btn3press(); }); -*/ - savSettings(); - onGPS(lf); } } @@ -700,18 +695,6 @@ Bangle.on('lcdPower',function(on) { else stopDraw(); }); -/* -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); -*/ var gpssetup; try { diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json index 617ac4b8e..e03d23c8b 100644 --- a/apps/speedalt/metadata.json +++ b/apps/speedalt/metadata.json @@ -2,7 +2,7 @@ "id": "speedalt", "name": "GPS Adventure Sports", "shortName": "GPS Adv Sport", - "version": "0.11", + "version": "0.12", "description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.", "icon": "app.png", "type": "app", diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json index 6907da84d..de0244318 100644 --- a/apps/terminalclock/metadata.json +++ b/apps/terminalclock/metadata.json @@ -3,7 +3,7 @@ "name": "Terminal Clock", "shortName":"Terminal Clock", "description": "A terminal cli like clock displaying multiple sensor data", - "version":"0.01", + "version":"0.02", "icon": "app.png", "type": "clock", "tags": "clock", diff --git a/apps/todolist/ChangeLog b/apps/todolist/ChangeLog new file mode 100644 index 000000000..2e979ec12 --- /dev/null +++ b/apps/todolist/ChangeLog @@ -0,0 +1 @@ +0.01: Initial release \ No newline at end of file diff --git a/apps/todolist/README.md b/apps/todolist/README.md new file mode 100644 index 000000000..27c7cfb63 --- /dev/null +++ b/apps/todolist/README.md @@ -0,0 +1,40 @@ +Todo List +======== + +This is a simple Todo List application. + +![](screenshot2.png) + +The content is loaded from a JSON file. +You can mark a task as completed. + +JSON file content example: +```javascript +[ + { + name: "Pro", + children: [ + { + name: "Read doc", + done: true, + children: [], + } + ], + }, + { + name: "Pers", + children: [ + { + name: "Grocery", + children: [ + { name: "Milk", done: false, children: [] }, + { name: "Eggs", done: false, children: [] }, + { name: "Cheese", done: false, children: [] }, + ], + }, + { name: "Workout", done: false, children: [] }, + { name: "Learn Rust", done: false, children: [] }, + ], + }, +] +``` \ No newline at end of file diff --git a/apps/todolist/app-icon.js b/apps/todolist/app-icon.js new file mode 100644 index 000000000..229852134 --- /dev/null +++ b/apps/todolist/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwgmjiMRiAWTgIXUCoYZQB4IADC4YHECxkSkIECkQYLEwMSkQQBkcyCAMTmYKEiIuGif/AAIXBmciiUzC4MvBQPyC44LCC4YADBYpIFiM/BYZDBC5EhC4wKCBYKLFEYkxC5UxCwsSBYgXK/5GEmYuDC5oAKC/4XUmK5DC6PziMfC6cimTRB+bbDiSpCC5ItBaIXxbIg2CF5QqBB4IcCAAQvMCYMhdIi//X7P/X6sz+S/CkQADX8gXCif/GQIADMwS/LZ4a//BgkyJBK/ll/zmYADX54FBX9cyB4ZHEO5wPDa/7RJAAshC4xyCABacBC40SGBsxiIWEgEBW4gAKFwowCABwWGACgA==")) \ No newline at end of file diff --git a/apps/todolist/app.js b/apps/todolist/app.js new file mode 100644 index 000000000..58cd3783c --- /dev/null +++ b/apps/todolist/app.js @@ -0,0 +1,129 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Const +let TODOLIST_FILE = "todolist.json"; +let MAX_DESCRIPTION_LEN = 14; + +// Clear todolist file +// require("Storage").erase(TODOLIST_FILE); + +let DEFAULT_TODOLIST = [ + { + name: "Pro", + children: [ + { + name: "Read doc", + done: true, + children: [], + }, + ], + }, + { + name: "Pers", + children: [ + { + name: "Grocery", + children: [ + { name: "Milk", done: false, children: [] }, + { name: "Eggs", done: false, children: [] }, + { name: "Cheese", done: false, children: [] }, + ], + }, + { name: "Workout", done: false, children: [] }, + { name: "Learn Rust", done: false, children: [] }, + ], + }, +]; + +// Load todolist +let todolist = + require("Storage").readJSON(TODOLIST_FILE, true) || DEFAULT_TODOLIST; +let menus = {}; + +function writeData() { + require("Storage").writeJSON(TODOLIST_FILE, todolist); +} + +function getChild(todolist, indexes) { + let childData = todolist; + for (let i = 0; i < indexes.length; i++) { + childData = childData[indexes[i]]; + childData = childData.children; + } + + return childData; +} + +function getName(item) { + let title = item.name.substr(0, MAX_DESCRIPTION_LEN); + return title; +} +function getParentTitle(todolist, indexes) { + let parentIndexes = indexes.slice(0, indexes.length - 1); + let lastIndex = indexes[indexes.length - 1]; + let item = getItem(todolist, parentIndexes, lastIndex); + return getName(item); +} + +function getItem(todolist, parentIndexes, index) { + let childData = getChild(todolist, parentIndexes, index); + return childData[index]; +} + +function toggleableStatus(todolist, indexes, index) { + const reminder = getItem(todolist, indexes, index); + return { + value: !!reminder.done, // !! converts undefined to false + format: (val) => (val ? "[X]" : "[-]"), + onchange: (val) => { + reminder.done = val; + writeData(); + }, + }; +} + +function showSubMenu(key) { + const sub_menu = menus[key]; + return E.showMenu(sub_menu); +} + +function createListItem(todolist, indexes, index) { + let reminder = getItem(todolist, indexes, index); + if (reminder.children.length > 0) { + let childIndexes = []; + for (let i = 0; i < indexes.length; i++) { + childIndexes.push(indexes[i]); + } + childIndexes.push(index); + createMenus(todolist, childIndexes); + return () => showSubMenu(childIndexes); + } else { + return toggleableStatus(todolist, indexes, index); + } +} + +function showMainMenu() { + const mainmenu = menus[""]; + return E.showMenu(mainmenu); +} + +function createMenus(todolist, indexes) { + const menuItem = {}; + if (indexes.length == 0) { + menuItem[""] = { title: "todolist" }; + } else { + menuItem[""] = { title: getParentTitle(todolist, indexes) }; + menuItem["< Back"] = () => + showSubMenu(indexes.slice(0, indexes.length - 1)); + } + for (let i = 0; i < getChild(todolist, indexes).length; i++) { + const item = getItem(todolist, indexes, i); + const name = getName(item); + menuItem[name] = createListItem(todolist, indexes, i); + } + menus[indexes] = menuItem; +} + +createMenus(todolist, []); +showMainMenu(); diff --git a/apps/todolist/app.png b/apps/todolist/app.png new file mode 100644 index 000000000..a93fc14ad Binary files /dev/null and b/apps/todolist/app.png differ diff --git a/apps/todolist/metadata.json b/apps/todolist/metadata.json new file mode 100644 index 000000000..0833a86bd --- /dev/null +++ b/apps/todolist/metadata.json @@ -0,0 +1,23 @@ +{ + "id": "todolist", + "name": "TodoList", + "shortName": "TodoList", + "version": "0.01", + "type": "app", + "description": "Simple Todo List", + "icon": "app.png", + "allow_emulator": true, + "tags": "tool,todo", + "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + { "name": "todolist.app.js", "url": "app.js" }, + { "name": "todolist.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{ "name": "todolist.json" }], + "screenshots": [ + { "url": "screenshot1.png" }, + { "url": "screenshot2.png" }, + { "url": "screenshot3.png" } + ] +} diff --git a/apps/todolist/screenshot1.png b/apps/todolist/screenshot1.png new file mode 100644 index 000000000..523d60307 Binary files /dev/null and b/apps/todolist/screenshot1.png differ diff --git a/apps/todolist/screenshot2.png b/apps/todolist/screenshot2.png new file mode 100644 index 000000000..0337f9000 Binary files /dev/null and b/apps/todolist/screenshot2.png differ diff --git a/apps/todolist/screenshot3.png b/apps/todolist/screenshot3.png new file mode 100644 index 000000000..e5a4a85ac Binary files /dev/null and b/apps/todolist/screenshot3.png differ diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog index abbfcbb99..02831edde 100644 --- a/apps/vectorclock/ChangeLog +++ b/apps/vectorclock/ChangeLog @@ -5,3 +5,4 @@ 0.05: "Chime the time" (buzz or beep) with up/down swipe added 0.06: Redraw widgets when time is updated 0.07: Fix problem with "Bangle.CLOCK": github.com/espruino/BangleApps/issues/1437 +0.08: Redraw widgets only once per minute diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js index 8d2961c4a..663a4c84f 100644 --- a/apps/vectorclock/app.js +++ b/apps/vectorclock/app.js @@ -81,7 +81,7 @@ function draw() { executeCommands(); - Bangle.drawWidgets(); + if (process.env.HWVERSION==2) Bangle.drawWidgets(); } var timeout; diff --git a/apps/vectorclock/metadata.json b/apps/vectorclock/metadata.json index 0f558e3ee..541766fa2 100644 --- a/apps/vectorclock/metadata.json +++ b/apps/vectorclock/metadata.json @@ -1,7 +1,7 @@ { "id": "vectorclock", "name": "Vector Clock", - "version": "0.07", + "version": "0.08", "description": "A digital clock that uses the built-in vector font.", "icon": "app.png", "type": "clock", 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 new file mode 100644 index 000000000..75e236049 --- /dev/null +++ b/backup.js @@ -0,0 +1,122 @@ +/* Code to handle Backup/Restore functionality */ + +const BACKUP_STORAGEFILE_DIR = "storage-files"; + +function bangleDownload() { + var zip = new JSZip(); + Progress.show({title:"Scanning...",sticky:true}); + var normalFiles, storageFiles; + console.log("Listing normal files..."); + Comms.reset() + .then(() => Comms.showMessage("Backing up...")) + .then(() => Comms.listFiles({sf:false})) + .then(f => { + normalFiles = f; + console.log(" - "+f.join(",")); + console.log("Listing StorageFiles..."); + return Comms.listFiles({sf:true}); + }).then(f => { + storageFiles = f; + console.log(" - "+f.join(",")); + var fileCount = normalFiles.length + storageFiles.length; + 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+(1/fileCount),percent:0}); + return Comms.readFile(filename).then(data => zip.file(filename,data)); + }); + }); + // Storage files + if (storageFiles.length) { + var zipStorageFiles = zip.folder(BACKUP_STORAGEFILE_DIR); + storageFiles.forEach((filename,n) => { + promise = promise.then(() => { + Progress.hide({sticky: true}); + var percent = (normalFiles.length+n)/fileCount; + 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)); + }); + }); + } + return promise; + }).then(() => { + return Comms.showMessage(Const.MESSAGE_RELOAD); + }).then(() => { + return zip.generateAsync({type:"binarystring"}); + }).then(content => { + Progress.hide({ sticky: true }); + showToast('Backup complete!', 'success'); + Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip"); + }).catch(err => { + Progress.hide({ sticky: true }); + showToast('Backup failed, ' + err, 'error'); + }); +} + +function bangleUpload() { + Espruino.Core.Utils.fileOpenDialog({ + id:"backup", + type:"arraybuffer", + mimeType:".zip,application/zip"}, function(data) { + if (data===undefined) return; + var promise = Promise.resolve(); + var zip = new JSZip(); + var cmds = ""; + zip.loadAsync(data).then(function(zip) { + return showPrompt("Restore from ZIP","Are you sure? This will remove all existing apps"); + }).then(()=>{ + Progress.show({title:`Reading ZIP`}); + zip.forEach(function (path, file){ + console.log("path"); + promise = promise + .then(() => file.async("string")) + .then(data => { + console.log("decoded", path); + if (path.startsWith(BACKUP_STORAGEFILE_DIR)) { + path = path.substr(BACKUP_STORAGEFILE_DIR.length+1); + cmds += AppInfo.getStorageFileUploadCommands(path, data)+"\n"; + } else if (!path.includes("/")) { + cmds += AppInfo.getFileUploadCommands(path, data)+"\n"; + } else console.log("Ignoring "+path); + }); + }); + return promise; + }) + .then(() => { + Progress.hide({sticky:true}); + Progress.show({title:`Erasing...`}); + return Comms.removeAllApps(); }) + .then(() => { + Progress.hide({sticky:true}); + Progress.show({title:`Restoring...`, sticky:true}); + return Comms.showMessage(`Restoring...`); }) + .then(() => Comms.write("\x10"+Comms.getProgressCmd()+"\n")) + .then(() => Comms.uploadCommandList(cmds, 0, cmds.length)) + .then(() => Comms.showMessage(Const.MESSAGE_RELOAD)) + .then(() => { + Progress.hide({sticky:true}); + showToast('Restore complete!', 'success'); + }) + .catch(err => { + Progress.hide({sticky:true}); + showToast('Restore failed, ' + err, 'error'); + }); + return promise; + }); +} + +window.addEventListener('load', (event) => { + document.getElementById("downloadallapps").addEventListener("click",event=>{ + bangleDownload(); + }); + document.getElementById("uploadallapps").addEventListener("click",event=>{ + bangleUpload(); + }); +}); diff --git a/core b/core index affb0b15b..27c7db603 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit affb0b15b41eb35a1548373831af7001bad64435 +Subproject commit 27c7db6035832837ca3909ea52939f60803df72f diff --git a/css/main.css b/css/main.css index f4850babe..a986df22e 100644 --- a/css/main.css +++ b/css/main.css @@ -81,7 +81,7 @@ a.btn.btn-link.dropdown-toggle { min-height: 8em; } -.tile-content { position: relative; } +.tile-content { position: relative; word-break: break-all; } .link-github { position:absolute; top: 36px; diff --git a/index.html b/index.html index 6c9a21bf8..a418b48eb 100644 --- a/index.html +++ b/index.html @@ -131,6 +131,8 @@

+

+

Settings