diff --git a/apps/2047pp/2047pp.app.js b/apps/2047pp/2047pp.app.js index 9163aaf3a..ac7fe6391 100644 --- a/apps/2047pp/2047pp.app.js +++ b/apps/2047pp/2047pp.app.js @@ -1,140 +1,142 @@ -class TwoK { - constructor() { - this.b = Array(4).fill().map(() => Array(4).fill(0)); - this.score = 0; - this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"}; - } - drawBRect(x1, y1, x2, y2, th, c, cf, fill) { - g.setColor(c); - for (i=0; i 4) g.setColor(1, 1, 1); - else g.setColor(0, 0, 0); - g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7)); - if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh); - } - } - shift(d) { // +/-1: shift x, +/- 2: shift y - var crc = E.CRC32(this.b.toString()); - if (d==-1) { // shift x left - for (y=0; y<4; ++y) { - for (x=2; x>=0; x--) - if (this.b[y][x]==0) { - for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1]; - this.b[y][3] = 0; - } - for (x=0; x<3; ++x) - if (this.b[y][x]==this.b[y][x+1]) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y][x+1]; - for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1]; +{ // wrap app in scope to prevent minifier from removing the class definition completely + class TwoK { + constructor() { + this.b = Array(4).fill().map(() => Array(4).fill(0)); + this.score = 0; + this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"}; + } + drawBRect(x1, y1, x2, y2, th, c, cf, fill) { + g.setColor(c); + for (i=0; i 4) g.setColor(1, 1, 1); + else g.setColor(0, 0, 0); + g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7)); + if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh); + } + } + shift(d) { // +/-1: shift x, +/- 2: shift y + var crc = E.CRC32(this.b.toString()); + if (d==-1) { // shift x left + for (y=0; y<4; ++y) { + for (x=2; x>=0; x--) + if (this.b[y][x]==0) { + for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1]; this.b[y][3] = 0; - } + } + for (x=0; x<3; ++x) + if (this.b[y][x]==this.b[y][x+1]) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y][x+1]; + for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1]; + this.b[y][3] = 0; + } + } } - } - else if (d==1) { // shift x right - for (y=0; y<4; ++y) { - for (x=1; x<4; x++) - if (this.b[y][x]==0) { - for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1]; - this.b[y][0] = 0; - } - for (x=3; x>0; --x) - if (this.b[y][x]==this.b[y][x-1]) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y][x-1] ; - for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1]; - this.b[y][0] = 0; - } + else if (d==1) { // shift x right + for (y=0; y<4; ++y) { + for (x=1; x<4; x++) + if (this.b[y][x]==0) { + for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1]; + this.b[y][0] = 0; + } + for (x=3; x>0; --x) + if (this.b[y][x]==this.b[y][x-1]) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y][x-1] ; + for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1]; + this.b[y][0] = 0; + } + } } - } - else if (d==-2) { // shift y down - for (x=0; x<4; ++x) { - for (y=1; y<4; y++) - if (this.b[y][x]==0) { - for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x]; - this.b[0][x] = 0; - } - for (y=3; y>0; y--) - if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y-1][x]; - for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x]; - this.b[0][x] = 0; - } + else if (d==-2) { // shift y down + for (x=0; x<4; ++x) { + for (y=1; y<4; y++) + if (this.b[y][x]==0) { + for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x]; + this.b[0][x] = 0; + } + for (y=3; y>0; y--) + if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y-1][x]; + for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x]; + this.b[0][x] = 0; + } + } } - } - else if (d==2) { // shift y up - for (x=0; x<4; ++x) { - for (y=2; y>=0; y--) - if (this.b[y][x]==0) { - for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x]; - this.b[3][x] = 0; - } - for (y=0; y<3; ++y) - if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) { - this.score += 2*this.b[y][x]; - this.b[y][x] += this.b[y+1][x]; - for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x]; - this.b[3][x] = 0; - } + else if (d==2) { // shift y up + for (x=0; x<4; ++x) { + for (y=2; y>=0; y--) + if (this.b[y][x]==0) { + for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x]; + this.b[3][x] = 0; + } + for (y=0; y<3; ++y) + if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) { + this.score += 2*this.b[y][x]; + this.b[y][x] += this.b[y+1][x]; + for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x]; + this.b[3][x] = 0; + } + } } + return (E.CRC32(this.b.toString())!=crc); + } + addDigit() { + var d = Math.random()>0.9 ? 4 : 2; + var id = Math.floor(Math.random()*16); + while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16); + this.b[Math.floor(id/4)][id%4] = d; } - return (E.CRC32(this.b.toString())!=crc); } - addDigit() { - var d = Math.random()>0.9 ? 4 : 2; - var id = Math.floor(Math.random()*16); - while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16); - this.b[Math.floor(id/4)][id%4] = d; - } -} -function dragHandler(e) { - if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) { - var res = false; - if (Math.abs(e.dx)>Math.abs(e.dy)) { - if (e.dx>0) res = twok.shift(1); - if (e.dx<0) res = twok.shift(-1); + function dragHandler(e) { + if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) { + var res = false; + if (Math.abs(e.dx)>Math.abs(e.dy)) { + if (e.dx>0) res = twok.shift(1); + if (e.dx<0) res = twok.shift(-1); + } + else { + if (e.dy>0) res = twok.shift(-2); + if (e.dy<0) res = twok.shift(2); + } + if (res) twok.addDigit(); + twok.render(); } - else { - if (e.dy>0) res = twok.shift(-2); - if (e.dy<0) res = twok.shift(2); - } - if (res) twok.addDigit(); - twok.render(); } -} -function swipeHandler() { - -} + function swipeHandler() { + + } -function buttonHandler() { - -} + function buttonHandler() { + + } -var twok = new TwoK(); -twok.addDigit(); twok.addDigit(); -twok.render(); -if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); -if (process.env.HWVERSION==1) { - Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); }); - setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true}); - setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true}); -} + var twok = new TwoK(); + twok.addDigit(); twok.addDigit(); + twok.render(); + if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); + if (process.env.HWVERSION==1) { + Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); }); + setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true}); + setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true}); + } +} \ No newline at end of file diff --git a/apps/2047pp/ChangeLog b/apps/2047pp/ChangeLog index a1f88e5ec..9436289ae 100644 --- a/apps/2047pp/ChangeLog +++ b/apps/2047pp/ChangeLog @@ -1,2 +1,3 @@ 0.01: New app! 0.02: Better support for watch themes +0.03: Workaround minifier bug \ No newline at end of file diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json index 033354ac6..ac4dc70bd 100644 --- a/apps/2047pp/metadata.json +++ b/apps/2047pp/metadata.json @@ -2,7 +2,7 @@ "name": "2047pp", "shortName":"2047pp", "icon": "app.png", - "version":"0.02", + "version":"0.03", "description": "Bangle version of a tile shifting game", "supports" : ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, diff --git a/apps/90sclk/ChangeLog b/apps/90sclk/ChangeLog index 057d6ff73..9718a652d 100644 --- a/apps/90sclk/ChangeLog +++ b/apps/90sclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fullscreen settings. 0.03: Tell clock widgets to hide. +0.04: Use widget_utils. diff --git a/apps/90sclk/app.js b/apps/90sclk/app.js index 351c235e0..63a48b27a 100644 --- a/apps/90sclk/app.js +++ b/apps/90sclk/app.js @@ -1,6 +1,7 @@ const SETTINGS_FILE = "90sclk.setting.json"; const locale = require('locale'); const storage = require('Storage'); +const widget_utils = require('widget_utils'); /* @@ -109,7 +110,7 @@ function draw() { // Draw widgets if not fullscreen if(settings.fullscreen){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + widget_utils.hide(); } else { Bangle.drawWidgets(); } diff --git a/apps/90sclk/metadata.json b/apps/90sclk/metadata.json index 59b627427..bfbb6b080 100644 --- a/apps/90sclk/metadata.json +++ b/apps/90sclk/metadata.json @@ -1,7 +1,7 @@ { "id": "90sclk", "name": "90s Clock", - "version": "0.03", + "version": "0.04", "description": "A 90s style watch-face", "readme": "README.md", "icon": "app.png", diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index b53e657fd..99c44a2b1 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -10,4 +10,5 @@ 0.09: Ensure Agenda supplies an image for clkinfo items 0.10: Update clock_info to avoid a redraw 0.11: Setting to use "Today" and "Yesterday" instead of dates - Added dynamic, short and range fields to clkinfo \ No newline at end of file + Added dynamic, short and range fields to clkinfo +0.12: Added color field and updating clkinfo periodically (running events) diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js index d203119f4..f9ea6a35d 100644 --- a/apps/agenda/agenda.clkinfo.js +++ b/apps/agenda/agenda.clkinfo.js @@ -5,6 +5,51 @@ if(passed<0) return 0; return passed; } + + /* + * Returns the array [interval, switchTimeout] + * `interval` is the refresh rate (hourly or per minute) + * `switchTimeout` is the time before the refresh rate should change (or expiration) + */ + function getRefreshIntervals(ev) { + const threshold = 2 * 60 * 1000; //2 mins + const slices = 16; + var now = new Date(); + var passed = now - (ev.timestamp*1000); + var remaining = (ev.durationInSeconds*1000) - passed; + if(remaining<0) + return []; + if(passed<0) //check once it's started + return [ 2*-passed, -passed ]; + var slice = Math.round(remaining/slices); + if(slice < threshold) { //no need to refresh frequently + return [ threshold, remaining ]; + } + return [ slice, remaining ]; + } + + function _doInterval(interval) { + return setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, interval); + }, interval); + } + function _doSwitchTimeout(ev, switchTimeout) { + return setTimeout(()=>{ + this.emit("redraw"); + clearInterval(this.interval); + this.interval = undefined; + var tmp = getRefreshIntervals(ev); + var interval = tmp[0]; + var switchTimeout = tmp[1]; + if(!interval) return; + this.interval = _doInterval.call(this, interval); + this.switchTimeout = _doSwitchTimeout.call(this, ev, switchTimeout); + }, switchTimeout); + } + var agendaItems = { name: "Agenda", img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="), @@ -23,16 +68,33 @@ var date = new Date(entry.timestamp*1000); var dateStr = locale.date(date).replace(/\d\d\d\d/,""); var shortStr = ((date-now) > 86400000 || entry.allDay) ? dateStr : locale.time(date,1); + var color = "#"+(0x1000000+Number(entry.color)).toString(16).padStart(6,"0"); dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; + shortStr = shortStr.trim().replace(" ", "\n"); agendaItems.items.push({ name: "Agenda "+i, hasRange: true, get: () => ({ text: title + "\n" + dateStr, - img: agendaItems.img, short: shortStr.trim(), + img: agendaItems.img, short: shortStr, + color: color, v: getPassedSec(date), min: 0, max: entry.durationInSeconds}), - show: function() {}, - hide: function () {} + show: function() { + var tmp = getRefreshIntervals(entry); + var interval = tmp[0]; + var switchTimeout = tmp[1]; + if(!interval) return; + this.interval = _doInterval.call(this, interval); + this.switchTimeout = _doSwitchTimeout.call(this, entry, switchTimeout); + }, + hide: function() { + if(this.interval) + clearInterval(this.interval); + if(this.switchTimeout) + clearTimeout(this.switchTimeout); + this.interval = undefined; + this.switchTimeout = undefined; + } }); }); diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index b5b7c1582..88dd2c1bc 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.11", + "version": "0.12", "description": "Simple agenda", "icon": "agenda.png", "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], diff --git a/apps/agpsdata/ChangeLog b/apps/agpsdata/ChangeLog index d900841d7..303fc7583 100644 --- a/apps/agpsdata/ChangeLog +++ b/apps/agpsdata/ChangeLog @@ -5,3 +5,4 @@ 0.04: Write AGPS data chunks with delay to improve reliability 0.05: Show last success date Do not start A-GPS update automatically +0.06: Switch off gps after updating \ No newline at end of file diff --git a/apps/agpsdata/lib.js b/apps/agpsdata/lib.js index 34608a5c6..4610331f6 100644 --- a/apps/agpsdata/lib.js +++ b/apps/agpsdata/lib.js @@ -10,20 +10,24 @@ readSettings(); function setAGPS(b64) { return new Promise(function(resolve, reject) { - var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on const gnsstype = settings.gnsstype || 1; // default GPS - initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode // What about: // NAV-TIMEUTC (0x01 0x10) // NAV-PV (0x01 0x03) // or AGPS.zip uses AID-INI (0x0B 0x01) - - eval(initCommands); + Bangle.setGPSPower(1,"agpsdata"); // turn GPS on + Serial1.println(CASIC_CHECKSUM("$PCAS04," + gnsstype)); // set GNSS mode try { - writeChunks(atob(b64), resolve); + writeChunks(atob(b64), ()=>{ + setTimeout(()=>{ + Bangle.setGPSPower(0,"agpsdata"); + resolve(); + }, 1000); + }); } catch (e) { console.log("error:", e); + Bangle.setGPSPower(0,"agpsdata"); reject(); } }); @@ -36,9 +40,8 @@ function writeChunks(bin, resolve) { setTimeout(function() { if (chunkI < bin.length) { var chunk = bin.substr(chunkI, chunkSize); - js = `Serial1.write(atob("${btoa(chunk)}"))\n`; - eval(js); - + Serial1.write(atob(btoa(chunk))); + chunkI += chunkSize; writeChunks(bin, resolve); } else { diff --git a/apps/agpsdata/metadata.json b/apps/agpsdata/metadata.json index 7f0b53217..446661045 100644 --- a/apps/agpsdata/metadata.json +++ b/apps/agpsdata/metadata.json @@ -2,7 +2,7 @@ "name": "A-GPS Data Downloader App", "shortName":"A-GPS Data", "icon": "agpsdata.png", - "version":"0.05", + "version":"0.06", "description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.", "tags": "boot,tool,assisted,gps,agps,http", "allow_emulator":true, diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog index 6d6eeb55e..43236015e 100644 --- a/apps/aiclock/ChangeLog +++ b/apps/aiclock/ChangeLog @@ -4,4 +4,5 @@ 0.04: Use widget_utils module. 0.05: Support for clkinfo. 0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. -0.07: Use clock_info.addInteractive instead of a custom implementation \ No newline at end of file +0.07: Use clock_info.addInteractive instead of a custom implementation +0.08: Use clock_info module as an app diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json index 4c01ecaa9..d8d1e9d68 100644 --- a/apps/aiclock/metadata.json +++ b/apps/aiclock/metadata.json @@ -3,9 +3,10 @@ "name": "AI Clock", "shortName":"AI Clock", "icon": "aiclock.png", - "version":"0.07", + "version":"0.08", "readme": "README.md", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", "type": "clock", "tags": "clock", diff --git a/apps/alarmqm/ChangeLog b/apps/alarmqm/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/alarmqm/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/alarmqm/app.png b/apps/alarmqm/app.png new file mode 100644 index 000000000..ee0085206 Binary files /dev/null and b/apps/alarmqm/app.png differ diff --git a/apps/alarmqm/boot.js b/apps/alarmqm/boot.js new file mode 100644 index 000000000..cf35452c0 --- /dev/null +++ b/apps/alarmqm/boot.js @@ -0,0 +1,20 @@ +(function () { + function dismissAlarm(alarm) { + // Run only for alarms, not timers + if (!alarm.timer) { + if ("qmsched" in WIDGETS) { + require("qmsched").setMode(0); + } else { + // Code from qmsched.js, so we can work without it + require("Storage").writeJSON( + "setting.json", + Object.assign(require("Storage").readJSON("setting.json", 1) || {}, { + quiet: 0, + }) + ); + } + } + } + + Bangle.on("alarmDismiss", dismissAlarm); + })(); \ No newline at end of file diff --git a/apps/alarmqm/metadata.json b/apps/alarmqm/metadata.json new file mode 100644 index 000000000..bae4b0807 --- /dev/null +++ b/apps/alarmqm/metadata.json @@ -0,0 +1,13 @@ +{ "id": "alarmqm", + "name": "Alarm Quiet Mode", + "shortName":"AlarmQM", + "version":"0.01", + "description": "Service that turns off quiet mode after alarm dismiss", + "icon": "app.png", + "tags": "quiet,alarm", + "supports" : ["BANGLEJS2"], + "type": "bootloader", + "storage": [ + {"name":"alarmqm.boot.js","url":"boot.js"} + ] +} diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 1e0c14a5e..a651c1747 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -19,3 +19,6 @@ If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged) 0.19: Add automatic translation for a couple of strings. 0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly. +0.21: Fix broken 'Messages' button in menu +0.22: Handle connection events for GPS forwarding from phone +0.23: Handle 'act' Gadgetbridge messages for realtime activity monitoring diff --git a/apps/android/boot.js b/apps/android/boot.js index c5a9dd746..81975182d 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -3,8 +3,11 @@ Bluetooth.println(""); Bluetooth.println(JSON.stringify(message)); } - var lastMsg; + var lastMsg; // for music messages - may not be needed now... + var actInterval; // Realtime activity reporting interval when `act` is true + var actHRMHandler; // For Realtime activity reporting + // this settings var is deleted after this executes to save memory var settings = require("Storage").readJSON("android.settings.json",1)||{}; //default alarm settings if (settings.rp == undefined) settings.rp = true; @@ -60,6 +63,7 @@ title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number}); require("messages").pushMessage(event); }, + // {"t":"alarm", "d":[{h:int,m:int,rep:int},... } "alarm" : function() { //wipe existing GB alarms var sched; @@ -92,6 +96,7 @@ }, //TODO perhaps move those in a library (like messages), used also for viewing events? //add and remove events based on activity on phone (pebble-like) + // {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool "calendar" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); if (!cal || !Array.isArray(cal)) cal = []; @@ -102,6 +107,7 @@ cal[i] = event; require("Storage").writeJSON("android.calendar.json", cal); }, + // {t:"calendar-", id:int} "calendar-" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); //if any of those happen we are out of sync! @@ -110,11 +116,13 @@ require("Storage").writeJSON("android.calendar.json", cal); }, //triggered by GB, send all ids + // { t:"force_calendar_sync_start" } "force_calendar_sync_start" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); if (!cal || !Array.isArray(cal)) cal = []; gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)}); }, + // {t:"http",resp:"......",[id:"..."]} "http":function() { //get the promise and call the promise resolve if (Bangle.httpRequest === undefined) return; @@ -127,21 +135,44 @@ else request.r(event); //r = resolve function }, + // {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true } "gps": function() { const settings = require("Storage").readJSON("android.settings.json",1)||{}; if (!settings.overwriteGps) return; delete event.t; event.satellites = NaN; - event.course = NaN; + if (!isFinite(event.course)) event.course = NaN; event.fix = 1; - if (event.long!==undefined) { + if (event.long!==undefined) { // for earlier Gadgetbridge implementations event.lon = event.long; delete event.long; } Bangle.emit('GPS', event); }, + // {t:"is_gps_active"} "is_gps_active": function() { - gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 }); + gbSend({ t: "gps_power", status: Bangle.isGPSOn() }); + }, + // {t:"act", hrm:bool, stp:bool, int:int} + "act": function() { + if (actInterval) clearInterval(actInterval); + actInterval = undefined; + if (actHRMHandler) + actHRMHandler = undefined; + Bangle.setHRMPower(event.hrm,"androidact"); + if (!(event.hrm || event.stp)) return; + if (!isFinite(event.int)) event.int=1; + var lastSteps = Bangle.getStepCount(); + var lastBPM = 0; + actHRMHandler = function(e) { + lastBPM = e.bpm; + }; + Bangle.on('HRM',actHRMHandler); + actInterval = setInterval(function() { + var steps = Bangle.getStepCount(); + gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM }); + lastSteps = steps; + }, event.int*1000); } }; var h = HANDLERS[event.t]; @@ -178,21 +209,28 @@ },options.timeout||30000)}; }); return promise; - } + }; // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } + Bangle.on("charging", sendBattery); NRF.on("connect", () => setTimeout(function() { sendBattery(); GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process }, 2000)); - Bangle.on("charging", sendBattery); - if (!settings.keep) - NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect + NRF.on("disconnect", () => { + // disable HRM/activity monitoring ('act' message) + GB({t:"act",stp:0,hrm:0,int:0}); // just call the handler to save duplication + // remove all messages on disconnect (if enabled) + var settings = require("Storage").readJSON("android.settings.json",1)||{}; + if (!settings.keep) + require("messages").clearAll(); + }); setInterval(sendBattery, 10*60*1000); // Health tracking Bangle.on('health', health=>{ - gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); + if (actInterval===undefined) // if 'realtime' we do it differently + gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); }); // Music control Bangle.musicControl = cmd => { @@ -207,13 +245,39 @@ }; // GPS overwrite logic if (settings.overwriteGps) { // if the overwrite option is set../ - // Save current logic - const originalSetGpsPower = Bangle.setGPSPower; + const origSetGPSPower = Bangle.setGPSPower; + // migrate all GPS clients to the other variant on connection events + let handleConnection = (state) => { + if (Bangle.isGPSOn()){ + let orig = Bangle._PWR.GPS; + delete Bangle._PWR.GPS; + origSetGPSPower(state); + Bangle._PWR.GPS = orig; + } + }; + NRF.on('connect', ()=>{handleConnection(0);}); + NRF.on('disconnect', ()=>{handleConnection(1);}); + + // Work around Serial1 for GPS not working when connected to something + let serialTimeout; + let wrap = function(f){ + return (s)=>{ + if (serialTimeout) clearTimeout(serialTimeout); + handleConnection(1); + f(s); + serialTimeout = setTimeout(()=>{ + serialTimeout = undefined; + if (NRF.getSecurityStatus().connected) handleConnection(0); + }, 10000); + }; + }; + Serial1.println = wrap(Serial1.println); + Serial1.write = wrap(Serial1.write); + // Replace set GPS power logic to suppress activation of gps (and instead request it from the phone) Bangle.setGPSPower = (isOn, appID) => { - // if not connected, use old logic - if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID); - // Emulate old GPS power logic + // if not connected use internal GPS power function + if (!NRF.getSecurityStatus().connected) return origSetGPSPower(isOn, appID); if (!Bangle._PWR) Bangle._PWR={}; if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[]; if (!appID) appID="?"; @@ -222,11 +286,15 @@ let pwr = Bangle._PWR.GPS.length>0; gbSend({ t: "gps_power", status: pwr }); return pwr; - } - // Replace check if the GPS is on to check the _PWR variable + }; + // Allow checking for GPS via GadgetBridge Bangle.isGPSOn = () => { - return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0; - } + return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0); + }; + // stop GPS on boot if not activated + setTimeout(()=>{ + if (!Bangle.isGPSOn()) gbSend({ t: "gps_power", status: false }); + },3000); } // remove settings object so it's not taking up RAM diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 883a821a4..f8ea3521a 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.20", + "version": "0.23", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", diff --git a/apps/android/settings.js b/apps/android/settings.js index 3e04e0f9d..0abb32249 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -1,6 +1,6 @@ (function(back) { - + function gb(j) { Bluetooth.println(JSON.stringify(j)); @@ -36,7 +36,7 @@ updateSettings(); } }, - /*LANG*/"Messages" : ()=>require("message").openGUI(), + /*LANG*/"Messages" : ()=>require("messages").openGUI(), }; E.showMenu(mainmenu); }) diff --git a/apps/android/test.js b/apps/android/test.js new file mode 100644 index 000000000..88a7c0566 --- /dev/null +++ b/apps/android/test.js @@ -0,0 +1,126 @@ +let result = true; + +function assertTrue(condition, text) { + if (!condition) { + result = false; + print("FAILURE: " + text); + } else print("OK: " + text); +} + +function assertFalse(condition, text) { + assertTrue(!condition, text); +} + +function assertUndefinedOrEmpty(array, text) { + assertTrue(!array || array.length == 0, text); +} + +function assertNotEmpty(array, text) { + assertTrue(array && array.length > 0, text); +} + +let internalOn = () => { + return getPinMode((process.env.HWVERSION==2)?D30:D26) == "input"; +}; + +let sec = { + connected: false +}; + +NRF.getSecurityStatus = () => sec; + +setTimeout(() => { + // add an empty starting point to make the asserts work + Bangle._PWR={}; + + print("Not connected, should use internal GPS"); + assertTrue(!NRF.getSecurityStatus().connected, "Not connected"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + + assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on"); + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertTrue(internalOn(), "Internal GPS on"); + + assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + print("Connected, should use GB GPS"); + sec.connected = true; + + assertTrue(NRF.getSecurityStatus().connected, "Connected"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on"); + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + print("Connected, then reconnect cycle"); + sec.connected = true; + + assertTrue(NRF.getSecurityStatus().connected, "Connected"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertTrue(Bangle.setGPSPower(1, "test"), "Switch GPS on"); + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + NRF.emit("disconnect", {}); + print("disconnect"); + sec.connected = false; + + setTimeout(() => { + + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertTrue(internalOn(), "Internal GPS on"); + + print("connect"); + sec.connected = true; + NRF.emit("connect", {}); + + setTimeout(() => { + assertNotEmpty(Bangle._PWR.GPS, "GPS"); + assertTrue(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + assertFalse(Bangle.setGPSPower(0, "test"), "Switch GPS off"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + setTimeout(() => { + print("Test disconnect without gps on"); + + assertUndefinedOrEmpty(Bangle._PWR.GPS, "No GPS"); + assertFalse(Bangle.isGPSOn(), "isGPSOn"); + assertFalse(internalOn(), "Internal GPS off"); + + print("Result Overall is " + (result ? "OK" : "FAIL")); + }, 0); + }, 0); + }, 0); +}, 5000); \ No newline at end of file diff --git a/apps/assistedgps/ChangeLog b/apps/assistedgps/ChangeLog index 739ccf915..ff2de6f67 100644 --- a/apps/assistedgps/ChangeLog +++ b/apps/assistedgps/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Update to work with Bangle.js 2 0.03: Select GNSS systems to use for Bangle.js 2 +0.04: Now turns GPS off after upload diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index 80d68a71f..75a4ecf32 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -133,7 +133,7 @@ function jsFromBase64(b64) { var bin = atob(b64); var chunkSize = 128; - var js = "\x10Bangle.setGPSPower(1);\n"; // turn GPS on + var js = "\x10Bangle.setGPSPower(1,'agps');\n"; // turn GPS on if (isB1) { // UBLOX //js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`; //js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec @@ -158,6 +158,7 @@ var chunk = bin.substr(i,chunkSize); js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`; } + js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay return js; } diff --git a/apps/assistedgps/metadata.json b/apps/assistedgps/metadata.json index 4c91dcd35..ac9fe5725 100644 --- a/apps/assistedgps/metadata.json +++ b/apps/assistedgps/metadata.json @@ -1,7 +1,7 @@ { "id": "assistedgps", "name": "Assisted GPS Updater (AGPS)", - "version": "0.03", + "version": "0.04", "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", "sortorder": -1, "icon": "app.png", diff --git a/apps/backswipe/ChangeLog b/apps/backswipe/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/backswipe/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/backswipe/app.png b/apps/backswipe/app.png new file mode 100644 index 000000000..93019e1e2 Binary files /dev/null and b/apps/backswipe/app.png differ diff --git a/apps/backswipe/boot.js b/apps/backswipe/boot.js new file mode 100644 index 000000000..523149e8c --- /dev/null +++ b/apps/backswipe/boot.js @@ -0,0 +1,50 @@ +(function () { + var DEFAULTS = { + mode: 0, + apps: [], + }; + var settings = require("Storage").readJSON("backswipe.json", 1) || DEFAULTS; + + // Overrride the default setUI method, so we can save the back button callback + var setUI = Bangle.setUI; + Bangle.setUI = function (mode, cb) { + var options = {}; + if ("object"==typeof mode) { + options = mode; + } + + var currentFile = global.__FILE__ || ""; + + if(global.BACK) delete global.BACK; + if (options && options.back && enabledForApp(currentFile)) { + global.BACK = options.back; + } + setUI(mode, cb); + }; + + function goBack(lr, ud) { + // if it is a left to right swipe + if (lr === 1) { + // if we're in an app that has a back button, run the callback for it + if (global.BACK) { + global.BACK(); + } + } + } + + // Check if the back button should be enabled for the current app + // app is the src file of the app + function enabledForApp(app) { + if (!settings) return true; + if (settings.mode === 0) { + return !(settings.apps.filter((a) => a.src === app).length > 0); + } else if (settings.mode === 1) { + return settings.apps.filter((a) => a.src === app).length > 0; + } else { + return settings.mode === 2 ? true : false; + } + } + + // Listen to left to right swipe + Bangle.on("swipe", goBack); +})(); diff --git a/apps/backswipe/metadata.json b/apps/backswipe/metadata.json new file mode 100644 index 000000000..7aa9f6247 --- /dev/null +++ b/apps/backswipe/metadata.json @@ -0,0 +1,17 @@ +{ "id": "backswipe", + "name": "Back Swipe", + "shortName":"BackSwipe", + "version":"0.01", + "description": "Service that allows you to use an app's back button using left to right swipe gesture", + "icon": "app.png", + "tags": "back,gesture,swipe", + "supports" : ["BANGLEJS2"], + "type": "bootloader", + "storage": [ + {"name":"backswipe.boot.js","url":"boot.js"}, + {"name":"backswipe.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"backswipe.json"} + ] +} diff --git a/apps/backswipe/settings.js b/apps/backswipe/settings.js new file mode 100644 index 000000000..2c29e86f8 --- /dev/null +++ b/apps/backswipe/settings.js @@ -0,0 +1,104 @@ +(function(back) { + var FILE = 'backswipe.json'; + // Mode can be 'blacklist', 'whitelist', 'on' or 'disabled' + // Apps is an array of app info objects, where all the apps that are there are either blocked or allowed, depending on the mode + var DEFAULTS = { + 'mode': 0, + 'apps': [] + }; + + var settings = {}; + + var loadSettings = function() { + settings = require('Storage').readJSON(FILE, 1) || DEFAULTS; + } + + var saveSettings = function(settings) { + require('Storage').write(FILE, settings); + } + + // Get all app info files + var getApps = function() { + var apps = require('Storage').list(/\.info$/).map(appInfoFileName => { + var appInfo = require('Storage').readJSON(appInfoFileName, 1); + return appInfo && { + 'name': appInfo.name, + 'sortorder': appInfo.sortorder, + 'src': appInfo.src + }; + }).filter(app => app && !!app.src); + apps.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + return apps; + } + + var showMenu = function() { + var menu = { + '': { 'title': 'Backswipe' }, + '< Back': () => { + back(); + }, + 'Mode': { + value: settings.mode, + min: 0, + max: 3, + format: v => ["Blacklist", "Whitelist", "Always On", "Disabled"][v], + onchange: v => { + settings.mode = v; + saveSettings(settings); + }, + }, + 'App List': () => { + showAppSubMenu(); + } + }; + + E.showMenu(menu); + } + + var showAppSubMenu = function() { + var menu = { + '': { 'title': 'Backswipe' }, + '< Back': () => { + showMenu(); + }, + 'Add App': () => { + showAppList(); + } + }; + settings.apps.forEach(app => { + menu[app.name] = () => { + settings.apps.splice(settings.apps.indexOf(app), 1); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + var showAppList = function() { + var apps = getApps(); + var menu = { + '': { 'title': 'Backswipe' }, + '< Back': () => { + showMenu(); + } + }; + apps.forEach(app => { + menu[app.name] = () => { + settings.apps.push(app); + saveSettings(settings); + showAppSubMenu(); + } + }); + E.showMenu(menu); + } + + loadSettings(); + showMenu(); +}) \ No newline at end of file diff --git a/apps/berlinc/ChangeLog b/apps/berlinc/ChangeLog index 9e9c1a6aa..1a0a9c9cf 100644 --- a/apps/berlinc/ChangeLog +++ b/apps/berlinc/ChangeLog @@ -4,3 +4,5 @@ 0.05: Update *on* the minute rather than every 15 secs Now show widgets Make compatible with themes, and Bangle.js 2 +0.06: Enable fastloading +0.07: Adds fullscreen mode setting \ No newline at end of file diff --git a/apps/berlinc/berlin-clock.js b/apps/berlinc/berlin-clock.js index 0dd8ff8ee..9391d2cc1 100644 --- a/apps/berlinc/berlin-clock.js +++ b/apps/berlinc/berlin-clock.js @@ -1,32 +1,41 @@ +{ // Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr // https://github.com/eska-muc/BangleApps + +var settings = require('Storage').readJSON("berlinc.json", true) || {}; const fields = [4, 4, 11, 4]; -const offset = 24; -const width = g.getWidth() - 2 * offset; -const height = g.getHeight() - 2 * offset; -const rowHeight = height / 4; -var show_date = false; -var show_time = false; -var yy = 0; +let fullscreen = !!settings.fullscreen; -var rowlights = []; -var time_digit = []; +let show_date = false; +let show_time = false; +let yy = 0; + +let rowlights = []; +let time_digit = []; // timeout used to update every minute -var drawTimeout; +let drawTimeout; // schedule a draw for the next minute -function queueDraw() { +let queueDraw = () => { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); }, 60000 - (Date.now() % 60000)); -} +}; -function draw() { - g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); +let draw = () => { + let width = Math.min(Bangle.appRect.w,Bangle.appRect.h); + let height = width; + let offset = g.getHeight() - height; + let x = Math.floor((g.getWidth() - width)/2); + + if (show_date) height -= 8; + let rowHeight = (height - 1) / 4; + g.setBgColor(g.theme.bg); + g.reset().clearRect(Bangle.appRect); var now = new Date(); // show date below the clock @@ -37,7 +46,7 @@ function draw() { var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; var strWidth = g.stringWidth(dateString); g.setColor(g.theme.fg).setFontAlign(-1,-1); - g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4); + g.drawString(dateString, ( Bangle.appRect.x + Bangle.appRect.w - strWidth ) / 2, Bangle.appRect.y2 - 5); } rowlights[0] = Math.floor(now.getHours() / 5); @@ -50,15 +59,16 @@ function draw() { time_digit[2] = Math.floor(now.getMinutes() / 10); time_digit[3] = now.getMinutes() % 10; - g.drawRect(offset, offset, width + offset, height + offset); + g.setColor(g.theme.fg); + g.drawRect(x, offset, x + width - 1, height + offset - 1); for (row = 0; row < 4; row++) { nfields = fields[row]; - boxWidth = width / nfields; + boxWidth = (width - 1) / nfields; for (col = 0; col < nfields; col++) { - x1 = col * boxWidth + offset; + x1 = col * boxWidth + x; y1 = row * rowHeight + offset; - x2 = (col + 1) * boxWidth + offset; + x2 = (col + 1) * boxWidth + x; y2 = (row + 1) * rowHeight + offset; g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2); @@ -84,33 +94,53 @@ function draw() { queueDraw(); } -function toggleDate() { +let toggleDate = () => { show_date = ! show_date; draw(); } -function toggleTime() { +let toggleTime = () => { show_time = ! show_time; draw(); } -// Stop updates when LCD is off, restart when on -Bangle.on('lcdPower',on=>{ +let clear = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; +} + +let onLcdPower = on => { if (on) { draw(); // draw immediately, queue redraw } else { // stop draw timer - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; + clear(); } -}); +} + +let cleanup = () => { + clear(); + Bangle.removeListener("lcdPower", onLcdPower); + require("widget_utils").show(); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',onLcdPower); // Show launcher when button pressed, handle up/down -Bangle.setUI("clockupdown", dir=> { +Bangle.setUI({mode: "clockupdown", remove: cleanup}, dir=> { if (dir<0) toggleTime(); if (dir>0) toggleDate(); }); g.clear(); Bangle.loadWidgets(); + +if (fullscreen){ + if (process.env.HWVERSION == 2) require("widget_utils").swipeOn(); + else require("widget_utils").hide(); +} + Bangle.drawWidgets(); + draw(); +} \ No newline at end of file diff --git a/apps/berlinc/metadata.json b/apps/berlinc/metadata.json index 85c42fc47..85567868b 100644 --- a/apps/berlinc/metadata.json +++ b/apps/berlinc/metadata.json @@ -1,7 +1,7 @@ { "id": "berlinc", "name": "Berlin Clock", - "version": "0.05", + "version": "0.07", "description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)", "icon": "berlin-clock.png", "type": "clock", @@ -12,6 +12,8 @@ "screenshots": [{"url":"berlin-clock-screenshot.png"}], "storage": [ {"name":"berlinc.app.js","url":"berlin-clock.js"}, + {"name":"berlinc.settings.js","url":"settings.js"}, {"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true} - ] + ], + "data": [{"name":"berlinc.json"}] } diff --git a/apps/berlinc/settings.js b/apps/berlinc/settings.js new file mode 100644 index 000000000..a1b655a62 --- /dev/null +++ b/apps/berlinc/settings.js @@ -0,0 +1,26 @@ +(function(back) { + var FILE = "berlinc.json"; + var settings = Object.assign({ + fullscreem: false, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + var mainmenu = { + "": { + "title": "Berlin clock" + }, + "< Back": () => back(), + "Fullscreen": { + value: !!settings.fullscreen, + onchange: v => { + settings.fullscreen = v; + writeSettings(); + } + } + }; + E.showMenu(mainmenu); + +}); diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 33cd7ef63..06f94854e 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -28,4 +28,6 @@ 0.27: Clean out some leftovers in the remove function after switching to clkinfo.addInteractive that would cause ReferenceError. 0.28: Option to show (1) time only and (2) week of year. -0.29: use setItem of clockInfoMenu to change the active item \ No newline at end of file +0.29: use setItem of clockInfoMenu to change the active item +0.30: Use widget_utils +0.31: Use clock_info module as an app diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index c2518361b..770c053c2 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -6,7 +6,7 @@ const locale = require('locale'); const storage = require('Storage'); const clock_info = require("clock_info"); - +const widget_utils = require("widget_utils"); /************************************************ * Globals @@ -132,6 +132,7 @@ clockInfoItems[0].items.unshift({ name : "nop", let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + app: "bwclk", x : 0, y: 135, w: W, @@ -277,7 +278,7 @@ let drawLock = function() { let drawWidgets = function() { if(isFullscreen()){ - for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} + widget_utils.hide(); } else { Bangle.drawWidgets(); } @@ -318,7 +319,7 @@ let lockListenerBw = function(isLocked) { if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ // If we have to show the widgets again, we load it from our // cache and not through Bangle.loadWidgets as its much faster! - for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;} + widget_utils.show(); } draw(); @@ -362,16 +363,13 @@ Bangle.setUI({ kill(); E.removeListener("kill", kill); g.setTheme(themeBackup); + widget_utils.show(); } }); // Load widgets and draw clock the first time Bangle.loadWidgets(); -// Cache draw function for dynamic screen to hide / show widgets -// Bangle.loadWidgets() could also be called later on but its much slower! -for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;} - // Draw first time draw(); diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index a29102bdf..430f466b2 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,7 +1,7 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.29", + "version": "0.31", "description": "A very minimalistic clock.", "readme": "README.md", "icon": "app.png", @@ -9,6 +9,7 @@ "type": "clock", "tags": "clock,clkinfo", "supports": ["BANGLEJS2"], + "dependencies" : { "clock_info":"module" }, "allow_emulator": true, "storage": [ {"name":"bwclk.app.js","url":"app.js"}, diff --git a/apps/calendar/ChangeLog b/apps/calendar/ChangeLog index db455679c..05eca83c0 100644 --- a/apps/calendar/ChangeLog +++ b/apps/calendar/ChangeLog @@ -10,3 +10,4 @@ 0.09: Fix scope of let variables 0.10: Use default Bangle formatter for booleans 0.11: Fix off-by-one-error on next year +0.12: Mark dated events on a day diff --git a/apps/calendar/calendar.js b/apps/calendar/calendar.js index f8785e52c..b61f2089c 100644 --- a/apps/calendar/calendar.js +++ b/apps/calendar/calendar.js @@ -22,15 +22,17 @@ let bgColorDow = color2; let bgColorWeekend = color3; let fgOtherMonth = gray1; let fgSameMonth = white; +let bgEvent = blue; +const eventsPerDay=6; // how much different events per day we can display +const timeutils = require("time_utils"); let settings = require('Storage').readJSON("calendar.json", true) || {}; let startOnSun = ((require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0) === 0; -if (settings.ndColors === undefined) - if (process.env.HWVERSION == 2) { - settings.ndColors = true; - } else { - settings.ndColors = false; - } +const events = (require("Storage").readJSON("sched.json",1) || []).filter(a => a.on && a.date); // all alarms that run on a specific date + +if (settings.ndColors === undefined) { + settings.ndColors = !g.theme.dark; +} if (settings.ndColors === true) { bgColor = white; @@ -39,6 +41,7 @@ if (settings.ndColors === true) { bgColorWeekend = yellow; fgOtherMonth = blue; fgSameMonth = black; + bgEvent = color2; } function getDowLbls(locale) { @@ -103,6 +106,12 @@ function getDowLbls(locale) { return dowLbls; } +function sameDay(d1, d2) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); +} + function drawCalendar(date) { g.setBgColor(bgColor); g.clearRect(0, 0, maxX, maxY); @@ -188,14 +197,15 @@ function drawCalendar(date) { for (x = 0; x < colN; x++) { i++; const day = days[i]; - const isToday = - today.year === year && today.month === month && today.day === day - 50; + const curMonth = day < 15 ? month+1 : day < 50 ? month-1 : month; + const curDay = new Date(year, curMonth, day > 50 ? day-50 : day); + const isToday = sameDay(curDay, new Date()); + const x1 = x * colW; + const y1 = y * rowH + headerH + rowH; + const x2 = x * colW + colW; + const y2 = y * rowH + headerH + rowH + rowH; if (isToday) { g.setColor(red); - let x1 = x * colW; - let y1 = y * rowH + headerH + rowH; - let x2 = x * colW + colW; - let y2 = y * rowH + headerH + rowH + rowH; g.drawRect(x1, y1, x2, y2); g.drawRect( x1 + 1, @@ -204,6 +214,22 @@ function drawCalendar(date) { y2 - 1 ); } + + // Display events for this day + const eventsCurDay = events.filter(ev => ev.date === curDay.toLocalISOString().substr(0, 10)); + if (eventsCurDay.length > 0) { + g.setColor(bgEvent); + eventsCurDay.forEach(ev => { + const time = timeutils.decodeTime(ev.t); + const hour = time.h + time.m/60.0; + const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59 + const height = (y2-2) - (y1+2); // height of a cell + const sliceHeight = height/eventsPerDay; + const ystart = (y1+2) + slice*sliceHeight; + g.fillRect(x1+1, ystart, x2-2, ystart+sliceHeight); + }); + } + require("Font8x12").add(Graphics); g.setFont("8x12", fontSize); g.setColor(day < 50 ? fgOtherMonth : fgSameMonth); @@ -217,11 +243,6 @@ function drawCalendar(date) { } const date = new Date(); -const today = { - day: date.getDate(), - month: date.getMonth(), - year: date.getFullYear() -}; drawCalendar(date); clearWatch(); Bangle.on("touch", area => { diff --git a/apps/calendar/metadata.json b/apps/calendar/metadata.json index 88f20026d..5bfc422fa 100644 --- a/apps/calendar/metadata.json +++ b/apps/calendar/metadata.json @@ -1,7 +1,7 @@ { "id": "calendar", "name": "Calendar", - "version": "0.11", + "version": "0.12", "description": "Simple calendar", "icon": "calendar.png", "screenshots": [{"url":"screenshot_calendar.png"}], diff --git a/apps/chargent/ChangeLog b/apps/chargent/ChangeLog new file mode 100644 index 000000000..7f837e50e --- /dev/null +++ b/apps/chargent/ChangeLog @@ -0,0 +1 @@ +0.01: First version diff --git a/apps/chargent/README.md b/apps/chargent/README.md new file mode 100644 index 000000000..56bc763b4 --- /dev/null +++ b/apps/chargent/README.md @@ -0,0 +1,13 @@ +# Charge Gently + +Charging Li-ion batteries to their full capacity has a significant impact on their lifespan. If possible, it is good practice to charge more often, but only to a certain lower capacity. + +The first stage of charging Li-ion ends at ~80% capacity when the charge voltage reaches its peak*. When that happens, the watch will buzz twice every 30s to remind you to disconnect the watch. + +This app has no UI and no configuration. To disable the app, you have to uninstall it. + +Side notes +- Full capacity is reached after charge current drops to an insignificant level. This is quite some time after charge voltage reached its peak / `E.getBattery()` returns 100. +- This app starts buzzing some time after `E.getBattery()` returns 100 (~15min on my watch), and at least 5min after the peak to account for noise. + +\* according to https://batteryuniversity.com/article/bu-409-charging-lithium-ion assuming similar characteristics and readouts from pin `D30` approximate charge voltage \ No newline at end of file diff --git a/apps/chargent/boot.js b/apps/chargent/boot.js new file mode 100644 index 000000000..802c3f55a --- /dev/null +++ b/apps/chargent/boot.js @@ -0,0 +1,30 @@ +(() => { + var id; + Bangle.on('charging', (charging) => { + if (charging) { + if (!id) { + var max = 0; + var count = 0; + id = setInterval(() => { + var d30 = analogRead(D30); + if (max < d30) { + max = d30; + count = 0; + } else { + count++; + if (10 <= count) { // 10 * 30s == 5 min // TODO ? customizable + // TODO ? customizable + Bangle.buzz(500); + setTimeout(() => Bangle.buzz(500), 1000); + } + } + }, 30*1000); + } + } else { + if (id) { + clearInterval(id); + id = undefined; + } + } + }); +})(); diff --git a/apps/chargent/boot.min.js b/apps/chargent/boot.min.js new file mode 100644 index 000000000..700198146 --- /dev/null +++ b/apps/chargent/boot.min.js @@ -0,0 +1 @@ +(function(){var a;Bangle.on("charging",function(e){if(e){if(!a){var c=0,b=0;a=setInterval(function(){var d=analogRead(D30);c