From 501c7e79cfe6916154a48a8b086ef589e682c893 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 2 Mar 2022 16:55:38 +0800 Subject: [PATCH 01/73] Update app.js Support some invalid base32 characters. --- apps/authentiwatch/app.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 73b8bdeea..69890c90c 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -32,9 +32,12 @@ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ function b32decode(seedstr) { // RFC4648 - var i, buf = 0, bitcount = 0, retstr = ""; - for (i in seedstr) { - var c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(seedstr.charAt(i).toUpperCase(), 0); + var buf = 0, bitcount = 0, retstr = ""; + for (var c of seedstr.toUpperCase()) { + if (c=='0')c='O'; + if (c=='1')c='I'; + if (c=='8')c='B'; + c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c); if (c != -1) { buf <<= 5; buf |= c; @@ -47,7 +50,7 @@ function b32decode(seedstr) { } } var retbuf = new Uint8Array(retstr.length); - for (i in retstr) { + for (var i in retstr) { retbuf[i] = retstr.charCodeAt(i); } return retbuf; From cb0aec47a43919235df08c4e2472c5dd64df5183 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 2 Mar 2022 16:57:48 +0800 Subject: [PATCH 02/73] Update app.js Add language translation tags --- apps/authentiwatch/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 69890c90c..e9d00a265 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -8,9 +8,9 @@ const algos = { "SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 }, "SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 }, }; -const calculating = "Calculating"; -const notokens = "No tokens"; -const notsupported = "Not supported"; +const calculating = /*LANG*/"Calculating"; +const notokens = /*LANG*/"No tokens"; +const notsupported = /*LANG*/"Not supported"; // sample settings: // {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} From fe8345ed15e5a1202dd4f2a027ede2705969f849 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 2 Mar 2022 22:44:13 +0800 Subject: [PATCH 03/73] Update interface.html Refactoring --- apps/authentiwatch/interface.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html index 5ee32fd8e..bfc0e55d1 100644 --- a/apps/authentiwatch/interface.html +++ b/apps/authentiwatch/interface.html @@ -54,9 +54,9 @@ var tokens = settings.tokens; */ function base32clean(val, nows) { var ret = val.replaceAll(/\s+/g, ' '); - ret = ret.replaceAll(/0/g, 'O'); - ret = ret.replaceAll(/1/g, 'I'); - ret = ret.replaceAll(/8/g, 'B'); + ret = ret.replaceAll('0', 'O'); + ret = ret.replaceAll('1', 'I'); + ret = ret.replaceAll('8', 'B'); ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, ''); if (nows) { ret = ret.replaceAll(/\s+/g, ''); @@ -81,9 +81,9 @@ function b32encode(str) { function b32decode(seedstr) { // RFC4648 - var i, buf = 0, bitcount = 0, ret = ''; - for (i in seedstr) { - var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0); + var buf = 0, bitcount = 0, ret = ''; + for (var c of seedstr.toUpperCase()) { + c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(c); if (c != -1) { buf <<= 5; buf |= c; @@ -487,7 +487,7 @@ function startScan(handler,cancel) { document.body.className = 'scanning'; navigator.mediaDevices .getUserMedia({video:{facingMode:'environment'}}) - .then(function(stream){ + .then(stream => { scanning=true; video.setAttribute('playsinline',true); video.srcObject = stream; From 7810dd2cfc01145b58998886b13ce78893219852 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Mon, 7 Mar 2022 23:33:23 +0800 Subject: [PATCH 04/73] Update app.js Bangle 2: Improve drag responsiveness and exit on button press --- apps/authentiwatch/app.js | 48 +++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index e9d00a265..b8181e4a2 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -34,9 +34,9 @@ function b32decode(seedstr) { // RFC4648 var buf = 0, bitcount = 0, retstr = ""; for (var c of seedstr.toUpperCase()) { - if (c=='0')c='O'; - if (c=='1')c='I'; - if (c=='8')c='B'; + if (c == '0') c = 'O'; + if (c == '1') c = 'I'; + if (c == '8') c = 'B'; c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c); if (c != -1) { buf <<= 5; @@ -166,11 +166,7 @@ function drawToken(id, r) { } while (g.stringWidth(state.otp) > (r.w - adj)); g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false); } - // shaded lines top and bottom - g.setColor(0.5, 0.5, 0.5) - .drawLine(x1, y1, x2, y1) - .drawLine(x1, y2, x2, y2) - .setClipRect(0, 0, g.getWidth(), g.getHeight()); + g.setClipRect(0, 0, g.getWidth(), g.getHeight()); } function draw() { @@ -212,7 +208,7 @@ function draw() { if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { drewcur = true; } - id += 1; + id++; y += tokenheight; } if (drewcur) { @@ -273,11 +269,33 @@ function onTouch(zone, e) { } function onDrag(e) { - if (e.x > g.getWidth() || e.y > g.getHeight()) return; - if (e.dx == 0 && e.dy == 0) return; - var newy = Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h); - state.listy = Math.max(0, newy); - draw(); + if (e.b != 0 && e.x < g.getWidth() && e.y < g.getHeight() && e.dy != 0) { + var y = Math.max(0, Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h)); + if (state.listy != y) { + var id, dy = state.listy - y; + state.listy = y; + g.setClipRect(Bangle.appRect.x,Bangle.appRect.y,Bangle.appRect.x2,Bangle.appRect.y2) + .scroll(0, dy); + if (dy > 0) { + id = Math.floor((state.listy + dy) / tokenheight); + y = id * tokenheight + Bangle.appRect.y - state.listy; + do { + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight}); + id--; + y -= tokenheight; + } while (y > 0); + } + if (dy < 0) { + id = Math.floor((state.listy + dy + Bangle.appRect.h) / tokenheight); + y = id * tokenheight + Bangle.appRect.y - state.listy; + while (y < Bangle.appRect.y2) { + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight}); + id++; + y += tokenheight; + } + } + } + } } function onSwipe(e) { @@ -332,6 +350,8 @@ if (typeof BTN2 == 'number') { setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true}); setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50}); setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true}); +} else { + setWatch(function(){exitApp(); }, BTN1, {edge:"falling", debounce:50}); } Bangle.loadWidgets(); From 929920fcb6708a715b0961e830c03cca7ef8a358 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Mon, 7 Mar 2022 23:33:53 +0800 Subject: [PATCH 05/73] Update ChangeLog 0.07 --- apps/authentiwatch/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog index bb2945db4..655916170 100644 --- a/apps/authentiwatch/ChangeLog +++ b/apps/authentiwatch/ChangeLog @@ -4,3 +4,4 @@ 0.04: Fix tapping at very bottom of list, exit on inactivity 0.05: Add support for bulk importing and exporting tokens 0.06: Add spaces to codes for improved readability (thanks @BartS23) +0.07: Bangle 2: Improve drag responsiveness and exit on button press From 51744f842df0484bab0b72bc3bcbe8f089e8c8e3 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Mon, 7 Mar 2022 23:34:18 +0800 Subject: [PATCH 06/73] Update metadata.json 0.07 --- apps/authentiwatch/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/authentiwatch/metadata.json b/apps/authentiwatch/metadata.json index b4ed34a12..36e1ea34d 100644 --- a/apps/authentiwatch/metadata.json +++ b/apps/authentiwatch/metadata.json @@ -4,7 +4,7 @@ "shortName": "AuthWatch", "icon": "app.png", "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot4.png"}], - "version": "0.06", + "version": "0.07", "description": "Google Authenticator compatible tool.", "tags": "tool", "interface": "interface.html", From 915ad6394795ea25e28d05480095ad23247836ec Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Tue, 8 Mar 2022 15:58:46 +0800 Subject: [PATCH 07/73] Update interface.html Use push instead of concat. --- apps/authentiwatch/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html index bfc0e55d1..7d567d34f 100644 --- a/apps/authentiwatch/interface.html +++ b/apps/authentiwatch/interface.html @@ -405,7 +405,7 @@ class proto3decoder { constructor(str) { this.buf = []; for (let i in str) { - this.buf = this.buf.concat(str.charCodeAt(i)); + this.buf.push(str.charCodeAt(i)); } } getVarint() { From d7def5d5e1c95fcd456a406ef8bda3da183e9f34 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Tue, 8 Mar 2022 21:52:57 +0800 Subject: [PATCH 08/73] Update app.js Cache font size calculations --- apps/authentiwatch/app.js | 45 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index b8181e4a2..8a8aeb962 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -18,18 +18,6 @@ var settings = require("Storage").readJSON("authentiwatch.json", true) || {token if (settings.data ) tokens = settings.data ; /* v0.02 settings */ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ -// QR Code Text -// -// Example: -// -// otpauth://totp/${url}:AA_${algorithm}_${digits}dig_${period}s@${url}?algorithm=${algorithm}&digits=${digits}&issuer=${url}&period=${period}&secret=${secret} -// -// ${algorithm} : one of SHA1 / SHA256 / SHA512 -// ${digits} : one of 6 / 8 -// ${period} : one of 30 / 60 -// ${url} : a domain name "example.com" -// ${secret} : the seed code - function b32decode(seedstr) { // RFC4648 var buf = 0, bitcount = 0, retstr = ""; @@ -75,6 +63,10 @@ function do_hmac(key, message, algo) { var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); return v.getUint32(0) & 0x7FFFFFFF; } +function formatOtp(otp, digits) { + var re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : "."; + return otp.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); +} function hotp(d, token, dohmac) { var tick; if (token.period > 0) { @@ -98,8 +90,7 @@ function hotp(d, token, dohmac) { ret = "0" + ret; } // add a space after every 3rd or 4th digit - var re = (token.digits % 3 == 0 || (token.digits % 3 >= token.digits % 4 && token.digits % 4 != 0)) ? "" : "."; - ret = ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); + ret = formatOtp(ret, token.digits); } catch(err) { ret = notsupported; } @@ -107,6 +98,7 @@ function hotp(d, token, dohmac) { return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; } +var fontsz_cache = {}; var state = { listy: 0, prevcur:0, @@ -117,12 +109,25 @@ var state = { hide:0 }; +function size_font(id, txt, w) { + var sz = fontsz_cache[id]; + if (sz) { + g.setFont("Vector", sz); + } else { + sz = tokendigitsheight; + do { + g.setFont("Vector", sz--); + } while (g.stringWidth(txt) > w); + fontsz_cache[id] = sz + 1; + } +} + function drawToken(id, r) { var x1 = r.x; var y1 = r.y; var x2 = r.x + r.w - 1; var y2 = r.y + r.h - 1; - var adj, lbl, sz; + var adj, lbl; g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ), Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2)); lbl = tokens[id].label.substr(0, 10); @@ -137,10 +142,7 @@ function drawToken(id, r) { } else { g.setColor(g.theme.fg) .setBgColor(g.theme.bg); - sz = tokendigitsheight; - do { - g.setFont("Vector", sz--); - } while (g.stringWidth(lbl) > r.w); + size_font("l" + id, lbl, r.w); // center in box g.setFontAlign(0, 0, 0); adj = (y1 + y2) / 2; @@ -160,10 +162,7 @@ function drawToken(id, r) { adj = 12; } // digits just below label - sz = tokendigitsheight; - do { - g.setFont("Vector", sz--); - } while (g.stringWidth(state.otp) > (r.w - adj)); + size_font("d" + id, state.otp, r.w - adj); g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false); } g.setClipRect(0, 0, g.getWidth(), g.getHeight()); From ebdea05bff9df19ea85ed7a636967b27f740a21f Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 9 Mar 2022 21:53:24 +0800 Subject: [PATCH 09/73] Update app.js Improve code style --- apps/authentiwatch/app.js | 93 ++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 8a8aeb962..fb130969f 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -1,6 +1,6 @@ -const tokenextraheight = 16; -var tokendigitsheight = 30; -var tokenheight = tokendigitsheight + tokenextraheight; +const TOKEN_EXTRA_HEIGHT = 16; +var TOKEN_DIGITS_HEIGHT = 30; +var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT; // Hash functions const crypto = require("crypto"); const algos = { @@ -8,9 +8,9 @@ const algos = { "SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 }, "SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 }, }; -const calculating = /*LANG*/"Calculating"; -const notokens = /*LANG*/"No tokens"; -const notsupported = /*LANG*/"Not supported"; +const CALCULATING = /*LANG*/"Calculating"; +const NO_TOKENS = /*LANG*/"No tokens"; +const NOT_SUPPORTED = /*LANG*/"Not supported"; // sample settings: // {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} @@ -43,7 +43,8 @@ function b32decode(seedstr) { } return retbuf; } -function do_hmac(key, message, algo) { + +function doHmac(key, message, algo) { var a = algos[algo]; // RFC2104 if (key.length > a.blksz) { @@ -63,11 +64,13 @@ function do_hmac(key, message, algo) { var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); return v.getUint32(0) & 0x7FFFFFFF; } + function formatOtp(otp, digits) { var re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : "."; return otp.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); } -function hotp(d, token, dohmac) { + +function hotp(d, token, calcHmac) { var tick; if (token.period > 0) { // RFC6238 - timed @@ -81,10 +84,10 @@ function hotp(d, token, dohmac) { var v = new DataView(msg.buffer); v.setUint32(0, tick >> 16 >> 16); v.setUint32(4, tick & 0xFFFFFFFF); - var ret = calculating; - if (dohmac) { + var ret = CALCULATING; + if (calcHmac) { try { - var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); + var hash = doHmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); ret = "" + hash % Math.pow(10, token.digits); while (ret.length < token.digits) { ret = "0" + ret; @@ -92,13 +95,13 @@ function hotp(d, token, dohmac) { // add a space after every 3rd or 4th digit ret = formatOtp(ret, token.digits); } catch(err) { - ret = notsupported; + ret = NOT_SUPPORTED; } } return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; } -var fontsz_cache = {}; +var fontszCache = {}; var state = { listy: 0, prevcur:0, @@ -109,16 +112,16 @@ var state = { hide:0 }; -function size_font(id, txt, w) { - var sz = fontsz_cache[id]; +function sizeFont(id, txt, w) { + var sz = fontszCache[id]; if (sz) { g.setFont("Vector", sz); } else { - sz = tokendigitsheight; + sz = TOKEN_DIGITS_HEIGHT; do { g.setFont("Vector", sz--); } while (g.stringWidth(txt) > w); - fontsz_cache[id] = sz + 1; + fontszCache[id] = sz + 1; } } @@ -135,14 +138,14 @@ function drawToken(id, r) { // current token g.setColor(g.theme.fgH) .setBgColor(g.theme.bgH) - .setFont("Vector", tokenextraheight) + .setFont("Vector", TOKEN_EXTRA_HEIGHT) // center just below top line .setFontAlign(0, -1, 0); adj = y1; } else { g.setColor(g.theme.fg) .setBgColor(g.theme.bg); - size_font("l" + id, lbl, r.w); + sizeFont("l" + id, lbl, r.w); // center in box g.setFontAlign(0, 0, 0); adj = (y1 + y2) / 2; @@ -162,8 +165,8 @@ function drawToken(id, r) { adj = 12; } // digits just below label - size_font("d" + id, state.otp, r.w - adj); - g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false); + sizeFont("d" + id, state.otp, r.w - adj); + g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + TOKEN_EXTRA_HEIGHT, false); } g.setClipRect(0, 0, g.getWidth(), g.getHeight()); } @@ -174,7 +177,7 @@ function draw() { var d = new Date(); if (state.curtoken != -1) { var t = tokens[state.curtoken]; - if (state.otp == calculating) { + if (state.otp == CALCULATING) { state.otp = hotp(d, t, true).hotp; } if (d.getTime() > state.nextTime) { @@ -200,20 +203,20 @@ function draw() { } if (tokens.length > 0) { var drewcur = false; - var id = Math.floor(state.listy / tokenheight); - var y = id * tokenheight + Bangle.appRect.y - state.listy; + var id = Math.floor(state.listy / TOKEN_HEIGHT); + var y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; while (id < tokens.length && y < Bangle.appRect.y2) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight}); + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { drewcur = true; } id++; - y += tokenheight; + y += TOKEN_HEIGHT; } if (drewcur) { // the current token has been drawn - schedule a redraw if (tokens[state.curtoken].period > 0) { - timerdly = (state.otp == calculating) ? 1 : 1000; // timed + timerdly = (state.otp == CALCULATING) ? 1 : 1000; // timed } else { timerdly = state.nexttime - d.getTime(); // counter } @@ -230,9 +233,9 @@ function draw() { state.nexttime = 0; } } else { - g.setFont("Vector", tokendigitsheight) + g.setFont("Vector", TOKEN_DIGITS_HEIGHT) .setFontAlign(0, 0, 0) - .drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); + .drawString(NO_TOKENS, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } if (state.drawtimer) { clearTimeout(state.drawtimer); @@ -242,18 +245,18 @@ function draw() { function onTouch(zone, e) { if (e) { - var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenheight); + var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / TOKEN_HEIGHT); if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { id = -1; } if (state.curtoken != id) { if (id != -1) { - var y = id * tokenheight - state.listy; + var y = id * TOKEN_HEIGHT - state.listy; if (y < 0) { state.listy += y; y = 0; } - y += tokenheight; + y += TOKEN_HEIGHT; if (y > Bangle.appRect.h) { state.listy += (y - Bangle.appRect.h); } @@ -269,28 +272,28 @@ function onTouch(zone, e) { function onDrag(e) { if (e.b != 0 && e.x < g.getWidth() && e.y < g.getHeight() && e.dy != 0) { - var y = Math.max(0, Math.min(state.listy - e.dy, tokens.length * tokenheight - Bangle.appRect.h)); + var y = Math.max(0, Math.min(state.listy - e.dy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h)); if (state.listy != y) { var id, dy = state.listy - y; state.listy = y; g.setClipRect(Bangle.appRect.x,Bangle.appRect.y,Bangle.appRect.x2,Bangle.appRect.y2) .scroll(0, dy); if (dy > 0) { - id = Math.floor((state.listy + dy) / tokenheight); - y = id * tokenheight + Bangle.appRect.y - state.listy; + id = Math.floor((state.listy + dy) / TOKEN_HEIGHT); + y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; do { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight}); + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); id--; - y -= tokenheight; + y -= TOKEN_HEIGHT; } while (y > 0); } if (dy < 0) { - id = Math.floor((state.listy + dy + Bangle.appRect.h) / tokenheight); - y = id * tokenheight + Bangle.appRect.y - state.listy; + id = Math.floor((state.listy + dy + Bangle.appRect.h) / TOKEN_HEIGHT); + y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; while (y < Bangle.appRect.y2) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenheight}); + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); id++; - y += tokenheight; + y += TOKEN_HEIGHT; } } } @@ -324,12 +327,12 @@ function bangle1Btn(e) { } state.curtoken = Math.max(state.curtoken, 0); state.curtoken = Math.min(state.curtoken, tokens.length - 1); - state.listy = state.curtoken * tokenheight; - state.listy -= (Bangle.appRect.h - tokenheight) / 2; - state.listy = Math.min(state.listy, tokens.length * tokenheight - Bangle.appRect.h); + state.listy = state.curtoken * TOKEN_HEIGHT; + state.listy -= (Bangle.appRect.h - TOKEN_HEIGHT) / 2; + state.listy = Math.min(state.listy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h); state.listy = Math.max(state.listy, 0); var fakee = {}; - fakee.y = state.curtoken * tokenheight - state.listy + Bangle.appRect.y; + fakee.y = state.curtoken * TOKEN_HEIGHT - state.listy + Bangle.appRect.y; state.curtoken = -1; state.nextTime = 0; onTouch(0, fakee); From f96ba8d6a5df56459ebbcf2249c925c3deae72a3 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 9 Mar 2022 21:56:52 +0800 Subject: [PATCH 10/73] Add files via upload Update screenshots --- apps/authentiwatch/screenshot1.png | Bin 2708 -> 1595 bytes apps/authentiwatch/screenshot2.png | Bin 2914 -> 1835 bytes apps/authentiwatch/screenshot3.png | Bin 2656 -> 1425 bytes apps/authentiwatch/screenshot4.png | Bin 2951 -> 1721 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/authentiwatch/screenshot1.png b/apps/authentiwatch/screenshot1.png index c7ca744b497c2098743be85b15d16878f7196a54..c3ac0e3b7e98b4db240c3928901179496a6ff69c 100644 GIT binary patch literal 1595 zcmaKsdpOez7{_NO4E6JzM09N9q@u)fEt)YWlWQiE#7K_he#>EwOIy>CYY5@NVXe$8 zA(q<`!Xmjdw~mxZiOpr~=v@9ePfve*-}iYw|GdxhdEV!nXlHXq2C4`Jfj}}?3(UEl zUHLT<;GGE{2$9@b;-Oe;bMd#LlF~{FBm{@EgCqkjoI^k$8~E3V>?7t(>_CV&+8PZ4 zF;k=j1ThdutP_htUiCUT^3bf;ePvt>O!RJe{)X0WQTjuM8@ zV_UiLF5Z(mI>!@&)T|=*8)RZrzG4a&QG2 zRP9mAW^TP<(+5{QYipjb)h|8FIqL;cBG;NdA=XuI5<@-@*AjY5$TTTo#~W{l}5^z;N31HlrE47D=RQ7G|7_@n_U~ z8}da@Y4msd_Tw_!>z(mQhu064@^^qdC@v zWH2FPoknSn%tp&l)a=A_W_vMZZ+jmW5e0u>H#SFEwQM&PNO9f3FJ|qLdDyI~T@JFp zt_)Fv?*uvhO+Gd{jStIX^WY{DPeiUS$D!($yGSS|t~gg(Y`6A)t$^jL{i= z9U5$6X#4T|;Z)@<&r$}!N?YfqAWa!;JApDjGWcAI`w3yX zfCR+^Ly;qs^Agz#s%K+kN%!WLbG3X zP<@yNtDT@M)NXw+O^)uK8bn{&R8=~id|0kHgJ>%#;5y%^;5!LKU#%Az@LFq~FL>7V z;5K-z`uGb`iK4Pahqn7m!={_M48r(U;T8U?vh@>gFD@XiDUPSipPw9YnLO7E{p^m` z6J(alfiu>eD6grLkn&O#n`(OF%ml}59Nk5L_ zs%^0D`cME`HT&j48+c#j{J=#-GE(JKLpTDL27B6QYsnAPRoW)N;M9t<$~8=1JH&(j zi>lkwkQs8vkm+uhc5#N1q89m|2Klb#|GMT0aevISEeUFd(P|@^36)>8-T<-GF&xMF zJ&h!SIwNLj0jUx!aO+p19Od%tUM!wJ-6rD{y|il^)}AfU__I2+V_&jG#xnNZA=}74LRqH5R3_^nq3n{K98|K5veg+h zB1<{SE{Z9GETcIk%W=}{e1AW@*Y$q5@BfG2eO>pL`~Sc0w6pe>0tg8N0008kR%T8I z+VpS1c@8?Qo#k^NAlb>%1ZW(VTmb-ty|tOK3&n?Bc8Of2cIalwuU}PUQx9EyNQV;k z#lYOΞ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 diff --git a/apps/authentiwatch/screenshot2.png b/apps/authentiwatch/screenshot2.png index 8156dd3e8a8070e3b57b93163f8cd44ca5e90e85..26bafdbb2db394b8652c4197ce828c4463281351 100644 GIT binary patch literal 1835 zcmb7FdpHvc8y}X2aTX=!wsmw?a*0|lCzlyQ>>@)#R?cyV$)!<3)@)Dh z005xs=IZ1n<3Iihki0CzMq-s@v^~zv(|P->f)W_Ie{e?9B$JfRyPk{%05GtBLQXR& z|D8+(hN3)C06<-aiiEfg08ngkb3%D1j4c*jOTTXeb|wx>rTxfS7f04!Y8N>|Sfp*+ zw%;g(`_@VYnPOM0)XGYcO2qkgycCy-_aEdPjFVka5feOLVG)=mU3AX7`XR?21r(GBw#e+P!MI^JFrqq zV;Bgj>FYYDx=d3-?W=$67!>8_00}p|z|aFZ-lCwrKL$F#nL1PEl{&rEEzXJzjmp#9 z!w6QP;9#r9R|icz`*xsv668^FcrwMwu8sopz^y3G!4y!e+kfe3f{b$T*5;4i^|ec6 zQ(kN%%F!9m9SD;U*i*P7B=SOxikh4}@W0`8TIC|i^9xeSu^50`-2G`oQOm+%0s%6H zZ~IYC{u+=9R{&xt!O6_Djybq80um4PmtNfXCJ1jEZi?rspA#^Ao;L=}(!y;fOc+g< zNT(uSucaO5a0&*dSo{Fe!d9cHW&&87=i?li!BRzTcKJrH64vh>>~S&47k_Q-f&jUHgS6Q{3ragojiD_`&{+# z!RBrVvY#opegx@Ju7>B2k`@zpHSSg(y+ewx#PZ;r=fv`y1qXcV?_7!D{ggGZI=ex_pak;dm7N9Y&oc59K)o&;7!$}RRY^L zV^+K}gdwMS=l6;r9NkAl7w3iN_{WD2ww;7jpE6Ein?~C2KkaYzZt*a|Kl2A|&d#2+ z&vSg@Q6V-bbw&;U+s62EL37qoS-0RpicYXTq5^3d#>d472J>lc{3g4xr#?VIlwFH= zP)ci{v!~-{c`f|C$*)wTVI*L{Cg1Z^oL zhF9iyEEaeq1#wQ@5L){W_uCUKFQI8u%7KE~%ddciv0`jRY7}_#G{I>ZQl?cRTFP*L z|LYI7*8Ve7#W*E)PG29WE}6Ujg|=!QmQrW?DZ=-@J&R0%4SiX|Y|5=M56s!+M@KWD zd56f30{@!gmEwHH;UEuv**ZhSBRJE`%SI;z-}KDMpr12L3^HQ%Nd*n(NRS3z&j4Kb zw`-D3Fn!bTrGXCJ$`)I0XC0>>kUqL@j@vvDJ!%(uJ}a$QCn@ zc$lzKH5(w&Fd|Mveo68h4tc!S>G&J>+3^=p;_3~y^=>H@>NK_(F(E|0=nc+qh0BET zB_jh+rtz}vwj55TH4UKf-DHpvYU!$OunJo7->(11{_jOe5uIh0u9#GBa-Vb{^WoA7 zAZnod#F_q)n6G2U;TsYG;7*r$&)nrJ%OrH2PmdGPHP zNTPixL?lX0&p_{_e+VJ?wKdsft$;&SPjbzJAa`!nA1W*etMXAV9ii)!qU3gTA3sO5y1bFQ>UQRmfl9qi7r}$`P1^aC&FZRXECmT6 zWKP9A9&?d+{YOSz>*`@&#z~hD%%7&RODWiR;x;ZEj+695ZklG8e}@$G9bZtq~GV)1NPy<|+^+d@RJo1z`)d7>&YDCs|9F;0mG%f1@G%^BnL I&@q_uFAa7*R{#J2 literal 2914 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 Z(A4{6$wZuM;{MYNIM|-BskI8c@jt}ERJ#BG diff --git a/apps/authentiwatch/screenshot3.png b/apps/authentiwatch/screenshot3.png index 6d14e0b965efa1b522d034bc0fee2f8415a950d5..80f2fb172b55fdfc19ac61f2c7878d718ebdc1fb 100644 GIT binary patch delta 1418 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 KMNUMnLSTZ>#1y{( literal 2951 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 From 31d835dad59e7d5c9c6d404c95c85ee5332a40c2 Mon Sep 17 00:00:00 2001 From: Joseph Paul Date: Sun, 20 Mar 2022 09:25:43 +0100 Subject: [PATCH 11/73] Cycling: Initial commit --- apps/cycling/ChangeLog | 1 + apps/cycling/README.md | 26 ++ apps/cycling/blecsc-emu.js | 111 ++++++++ apps/cycling/blecsc.js | 150 +++++++++++ apps/cycling/cscsensor.app.js | 420 +++++++++++++++++++++++++++++ apps/cycling/icons8-cycling-48.png | Bin 0 -> 1487 bytes apps/cycling/metadata.json | 16 ++ apps/cycling/settings.js | 37 +++ 8 files changed, 761 insertions(+) create mode 100644 apps/cycling/ChangeLog create mode 100644 apps/cycling/README.md create mode 100644 apps/cycling/blecsc-emu.js create mode 100644 apps/cycling/blecsc.js create mode 100644 apps/cycling/cscsensor.app.js create mode 100644 apps/cycling/icons8-cycling-48.png create mode 100644 apps/cycling/metadata.json create mode 100644 apps/cycling/settings.js diff --git a/apps/cycling/ChangeLog b/apps/cycling/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/cycling/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/cycling/README.md b/apps/cycling/README.md new file mode 100644 index 000000000..61ba1d455 --- /dev/null +++ b/apps/cycling/README.md @@ -0,0 +1,26 @@ +# Cycling +> Displays data from a BLE Cycling Speed and Cadence sensor. + +*Fork of the CSCSensor app using the layout library and separate module for CSC functionality* + +The following data are displayed: +- curent speed +- moving time +- average speed +- maximum speed +- trip distance +- total distance + +Total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, a absolute value that persists throughout the lifetime of the sensor and never rolls over. + +**Cadence / Crank features are currently not implemented** + +# TODO +* Settings: imperial/metric +* Store circumference per device address +* Sensor battery status +* Implement crank events / show cadence +* Bangle.js 1 compatibility + +# Development +There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage. diff --git a/apps/cycling/blecsc-emu.js b/apps/cycling/blecsc-emu.js new file mode 100644 index 000000000..ca5058545 --- /dev/null +++ b/apps/cycling/blecsc-emu.js @@ -0,0 +1,111 @@ +// UUID of the Bluetooth CSC Service +const SERVICE_UUID = "1816"; +// UUID of the CSC measurement characteristic +const MEASUREMENT_UUID = "2a5b"; + +// Wheel revolution present bit mask +const FLAGS_WREV_BM = 0x01; +// Crank revolution present bit mask +const FLAGS_CREV_BM = 0x02; + +/** + * Fake BLECSC implementation for the emulator, where it's hard to test + * with actual hardware. Generates "random" wheel events (no crank). + * + * To upload as a module, paste the entire file in the console using this + * command: require("Storage").write("blecsc-emu",``); + */ +class BLECSCEmulator { + constructor() { + this.timeout = undefined; + this.interval = 500; + this.ccr = 0; + this.lwt = 0; + this.handlers = { + // value + // disconnect + // wheelEvent + // crankEvent + }; + } + + getDeviceAddress() { + return 'fa:ke:00:de:vi:ce'; + } + + /** + * Callback for the GATT characteristicvaluechanged event. + * Consumers must not call this method! + */ + onValue(event) { + // Not interested in non-CSC characteristics + if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; + + // Notify the generic 'value' handler + if (this.handlers.value) this.handlers.value(event); + + const flags = event.target.value.getUint8(0, true); + // Notify the 'wheelEvent' handler + if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ + cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions + lwet: event.target.value.getUint16(5, true), // last wheel event time + }); + + // Notify the 'crankEvent' handler + if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ + ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions + lcet: event.target.value.getUint16(9, true), // last crank event time + }); + } + + /** + * Register an event handler. + * + * @param {string} event value|disconnect + * @param {function} handler handler function that receives the event as its first argument + */ + on(event, handler) { + this.handlers[event] = handler; + } + + fakeEvent() { + this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20)); + this.lwt = (this.lwt + this.interval) % 0x10000; + this.ccr++; + + var buffer = new ArrayBuffer(8); + var view = new DataView(buffer); + view.setUint8(0, 0x01); // Wheel revolution data present bit + view.setUint32(1, this.ccr, true); // Cumulative crank revolutions + view.setUint16(5, this.lwt, true); // Last wheel event time + + this.onValue({ + target: { + uuid: "0x2a5b", + value: view, + }, + }); + + this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); + } + + /** + * Find and connect to a device which exposes the CSC service. + * + * @return {Promise} + */ + connect() { + this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); + return Promise.resolve(true); + } + + /** + * Disconnect the device. + */ + disconnect() { + if (!this.timeout) return; + clearTimeout(this.timeout); + } +} + +exports = BLECSCEmulator; diff --git a/apps/cycling/blecsc.js b/apps/cycling/blecsc.js new file mode 100644 index 000000000..7a47108e5 --- /dev/null +++ b/apps/cycling/blecsc.js @@ -0,0 +1,150 @@ +const SERVICE_UUID = "1816"; +// UUID of the CSC measurement characteristic +const MEASUREMENT_UUID = "2a5b"; + +// Wheel revolution present bit mask +const FLAGS_WREV_BM = 0x01; +// Crank revolution present bit mask +const FLAGS_CREV_BM = 0x02; + +/** + * This class communicates with a Bluetooth CSC peripherial using the Espruino NRF library. + * + * ## Usage: + * 1. Register event handlers using the \`on(eventName, handlerFunction)\` method + * You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can + * have raw characteristic values passed through using the \`value\` event. + * 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method + * 3. To tear down the connection, call the \`disconnect()\` method + * + * ## Events + * - \`wheelEvent\` - the peripharial sends a notification containing wheel event data + * - \`crankEvent\` - the peripharial sends a notification containing crank event data + * - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event) + * - \`disconnect\` - the peripherial ends the connection or the connection is lost + * + * Each event can only have one handler. Any call to \`on()\` will + * replace a previously registered handler for the same event. + */ +class BLECSC { + constructor() { + this.device = undefined; + this.ccInterval = undefined; + this.gatt = undefined; + this.handlers = { + // wheelEvent + // crankEvent + // value + // disconnect + }; + } + + getDeviceAddress() { + if (!this.device || !this.device.id) + return '00:00:00:00:00:00'; + return this.device.id.split(" ")[0]; + } + + checkConnection() { + if (!this.device) + console.log("no device"); + // else + // console.log("rssi: " + this.device.rssi); + } + + /** + * Callback for the GATT characteristicvaluechanged event. + * Consumers must not call this method! + */ + onValue(event) { + // Not interested in non-CSC characteristics + if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; + + // Notify the generic 'value' handler + if (this.handlers.value) this.handlers.value(event); + + const flags = event.target.value.getUint8(0, true); + // Notify the 'wheelEvent' handler + if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ + cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions + lwet: event.target.value.getUint16(5, true), // last wheel event time + }); + + // Notify the 'crankEvent' handler + if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ + ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions + lcet: event.target.value.getUint16(9, true), // last crank event time + }); + } + + /** + * Callback for the NRF disconnect event. + * Consumers must not call this method! + */ + onDisconnect(event) { + console.log("disconnected"); + if (this.ccInterval) + clearInterval(this.ccInterval); + + if (!this.handlers.disconnect) return; + this.handlers.disconnect(event); + } + + /** + * Register an event handler. + * + * @param {string} event wheelEvent|crankEvent|value|disconnect + * @param {function} handler function that will receive the event as its first argument + */ + on(event, handler) { + this.handlers[event] = handler; + } + + /** + * Find and connect to a device which exposes the CSC service. + * + * @return {Promise} + */ + connect() { + // Register handler for the disconnect event to be passed throug + NRF.on('disconnect', this.onDisconnect.bind(this)); + + // Find a device, then get the CSC Service and subscribe to + // notifications on the CSC Measurement characteristic. + // NRF.setLowPowerConnection(true); + return NRF.requestDevice({ + timeout: 5000, + filters: [{ services: [SERVICE_UUID] }], + }).then(device => { + this.device = device; + this.device.on('gattserverdisconnected', this.onDisconnect.bind(this)); + this.ccInterval = setInterval(this.checkConnection.bind(this), 2000); + return device.gatt.connect(); + }).then(gatt => { + this.gatt = gatt; + return gatt.getPrimaryService(SERVICE_UUID); + }).then(service => { + return service.getCharacteristic(MEASUREMENT_UUID); + }).then(characteristic => { + characteristic.on('characteristicvaluechanged', this.onValue.bind(this)); + return characteristic.startNotifications(); + }); + } + + /** + * Disconnect the device. + */ + disconnect() { + if (this.ccInterval) + clearInterval(this.ccInterval); + + if (!this.gatt) return; + try { + this.gatt.disconnect(); + } catch { + // + } + } +} + +exports = BLECSC; diff --git a/apps/cycling/cscsensor.app.js b/apps/cycling/cscsensor.app.js new file mode 100644 index 000000000..9de3f5a3e --- /dev/null +++ b/apps/cycling/cscsensor.app.js @@ -0,0 +1,420 @@ +const Layout = require('Layout'); + +const SETTINGS_FILE = 'cscsensor.json'; +const storage = require('Storage'); + +const RECONNECT_TIMEOUT = 4000; +const MAX_CONN_ATTEMPTS = 2; + +class CSCSensor { + constructor(blecsc, display) { + // Dependency injection + this.blecsc = blecsc; + this.display = display; + + // Load settings + this.settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + this.wheelCirc = (this.settings.wheelcirc || 2230) / 1000; // unit: m + + // CSC runtime variables + this.movingTime = 0; // unit: s + this.lastBangleTime = Date.now(); // unit: ms + this.lwet = 0; // last wheel event time (unit: s/1024) + this.cwr = -1; // cumulative wheel revolutions + this.cwrTrip = 0; // wheel revolutions since trip start + this.speed = 0; // unit: m/s + this.maxSpeed = 0; // unit: m/s + this.speedFailed = 0; + + // Other runtime variables + this.connected = false; + this.failedAttempts = 0; + this.failed = false; + + // Layout configuration + this.layout = 0; + this.display.useMetricUnits(true); + // this.display.useMetricUnits(!require("locale").speed(1).toString().endsWith("mph")); + } + + onDisconnect(event) { + console.log("disconnected ", event); + + this.connected = false; + this.setLayout(0); + this.display.setDeviceAddress("unknown"); + + if (this.failedAttempts >= MAX_CONN_ATTEMPTS) { + this.failed = true; + this.display.setStatus("Connection failed after " + MAX_CONN_ATTEMPTS + " attempts."); + } else { + this.display.setStatus("Disconnected"); + setTimeout(this.connect.bind(this), RECONNECT_TIMEOUT); + } + + } + + connect() { + this.connected = false; + this.setLayout(0); + this.display.setStatus("Connecting..."); + console.log("Trying to connect to BLE CSC"); + + // Hook up events + this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this)); + this.blecsc.on('disconnect', this.onDisconnect.bind(this)); + + // Scan for BLE device and connect + this.blecsc.connect() + .then(function() { + this.failedAttempts = 0; + this.failed = false; + this.connected = true; + var addr = this.blecsc.getDeviceAddress(); + console.log("Connected to " + addr); + + this.display.setDeviceAddress(addr); + this.display.setStatus("Connected"); + + // Switch to speed screen in 2s + setTimeout(function() { + this.setLayout(1); + this.updateScreen(); + }.bind(this), 2000); + }.bind(this)) + .catch(function(e) { + this.failedAttempts++; + this.onDisconnect(e); + }.bind(this)); + } + + disconnect() { + this.blecsc.disconnect(); + this.connected = false; + this.setLayout(0); + this.display.setStatus("Disconnected") + } + + setLayout(num) { + this.layout = num; + if (this.layout == 0) { + this.display.updateLayout("status"); + } else if (this.layout == 1) { + this.display.updateLayout("speed"); + } else if (this.layout == 2) { + this.display.updateLayout("distance"); + } + } + + reset() { + this.connected = false; + this.failed = false; + this.failedAttempts = 0; + } + + interact(d) { + // Only interested in tap / center button + if (d) return; + + // Reconnect in failed state + if (this.failed) { + this.reset(); + this.connect(); + } else if (this.connected) { + this.setLayout((this.layout + 1) % 3); + } + } + + updateScreen() { + var tripDist = this.cwrTrip * this.wheelCirc; + var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0 + + this.display.setTotalDistance(this.cwr * this.wheelCirc); + this.display.setTripDistance(tripDist); + this.display.setSpeed(this.speed); + this.display.setAvg(avgSpeed); + this.display.setMax(this.maxSpeed); + this.display.setTime(Math.floor(this.movingTime)); + } + + onWheelEvent(event) { + // Calculate number of revolutions since last wheel event + var dRevs = (this.cwr > 0 ? event.cwr - this.cwr : 0); + this.cwr = event.cwr; + + // Increment the trip revolutions counter + this.cwrTrip += dRevs; + + // Calculate time delta since last wheel event + var dT = (event.lwet - this.lwet)/1024; + var now = Date.now(); + var dBT = (now-this.lastBangleTime)/1000; + this.lastBangleTime = now; + if (dT<0) dT+=64; // wheel event time wraps every 64s + if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this + this.lwet = event.lwet; + + // Recalculate current speed + if (dRevs>0 && dT>0) { + this.speed = dRevs * this.wheelCirc / dT; + this.speedFailed = 0; + this.movingTime += dT; + } else { + this.speedFailed++; + if (this.speedFailed>3) { + this.speed = 0; + } + } + + // Update max speed + if (this.speed>this.maxSpeed + && (this.movingTime>3 || this.speed<20) + && this.speed<50 + ) this.maxSpeed = this.speed; + + this.updateScreen(); + } +} + +class CSCDisplay { + constructor() { + this.metric = true; + this.fontLabel = "6x8"; + this.fontMed = "15%"; + this.fontLarge = "32%"; + this.currentLayout = "status"; + this.layouts = {}; + this.layouts.speed = new Layout({ + type: "v", + c: [ + { + type: "h", + id: "speed_g", + fillx: 1, + filly: 1, + pad: 4, + bgCol: "#fff", + c: [ + {type: undefined, width: 32, halign: -1}, + {type: "txt", id: "speed", label: "00.0", font: this.fontLarge, bgCol: "#fff", col: "#000", width: 122}, + {type: "txt", id: "speed_u", label: " km/h", font: this.fontLabel, col: "#000", width: 22, r: 90}, + ] + }, + { + type: "h", + id: "time_g", + fillx: 1, + pad: 4, + bgCol: "#000", + height: 32, + c: [ + {type: undefined, width: 32, halign: -1}, + {type: "txt", id: "time", label: "00:00", font: this.fontMed, bgCol: "#000", col: "#fff", width: 122}, + {type: "txt", id: "time_u", label: "mins", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90}, + ] + }, + { + type: "h", + id: "stats_g", + fillx: 1, + bgCol: "#fff", + height: 32, + c: [ + { + type: "v", + pad: 4, + bgCol: "#fff", + c: [ + {type: "txt", id: "max_l", label: "MAX", font: this.fontLabel, col: "#000"}, + {type: "txt", id: "max", label: "00.0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 69}, + ], + }, + { + type: "v", + pad: 4, + bgCol: "#fff", + c: [ + {type: "txt", id: "avg_l", label: "AVG", font: this.fontLabel, col: "#000"}, + {type: "txt", id: "avg", label: "00.0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 69}, + ], + }, + {type: "txt", id: "stats_u", label: " km/h", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, + ] + }, + ], + }); + this.layouts.distance = new Layout({ + type: "v", + c: [ + { + type: "h", + id: "tripd_g", + fillx: 1, + pad: 4, + bgCol: "#fff", + height: 32, + c: [ + {type: "txt", id: "tripd_l", label: "TRP", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36}, + {type: "txt", id: "tripd", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118}, + {type: "txt", id: "tripd_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, + ] + }, + { + type: "h", + id: "totald_g", + fillx: 1, + pad: 4, + bgCol: "#000", + height: 32, + c: [ + {type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 36}, + {type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#000", col: "#fff", width: 118}, + {type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90}, + ] + }, + ], + }); + this.layouts.status = new Layout({ + type: "v", + c: [ + { + type: "h", + id: "status_g", + fillx: 1, + bgCol: "#fff", + height: 100, + c: [ + {type: "txt", id: "status", label: "Bangle Cycling", font: this.fontMed, bgCol: "#fff", col: "#000", width: 176, wrap: 1}, + ] + }, + { + type: "h", + id: "addr_g", + fillx: 1, + pad: 4, + bgCol: "#fff", + height: 32, + c: [ + { type: "txt", id: "addr_l", label: "MAC", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 }, + { type: "txt", id: "addr", label: "unknown", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 140 }, + ] + }, + ], + }); + } + + updateLayout(layout) { + this.currentLayout = layout; + + g.clear(); + this.layouts[layout].update(); + this.layouts[layout].render(); + } + + renderIfLayoutActive(layout, node) { + if (layout != this.currentLayout) return; + this.layouts[layout].render(node) + } + + useMetricUnits(metric) { + this.metric = metric; + + console.log("using " + (metric ? "metric" : "imperial") + " units"); + + var speedUnit = metric ? "km/h" : "mph"; + this.layouts.speed.speed_u.label = speedUnit; + this.layouts.speed.stats_u.label = speedUnit; + + var distanceUnit = metric ? "km" : "mi"; + this.layouts.distance.tripd_u.label = distanceUnit; + this.layouts.distance.totald_u.label = distanceUnit; + + this.updateLayout(this.currentLayout); + } + + convertDistance(meters) { + if (this.metric) return meters / 1000; + return meters / 1609.344; + } + + convertSpeed(mps) { + if (this.metric) return mps * 3.6; + return mps * 2.23694; + } + + setSpeed(speed) { + this.layouts.speed.speed.label = this.convertSpeed(speed).toFixed(1); + this.renderIfLayoutActive("speed", this.layouts.speed.speed_g); + } + + setAvg(speed) { + this.layouts.speed.avg.label = this.convertSpeed(speed).toFixed(1); + this.renderIfLayoutActive("speed", this.layouts.speed.stats_g); + } + + setMax(speed) { + this.layouts.speed.max.label = this.convertSpeed(speed).toFixed(1); + this.renderIfLayoutActive("speed", this.layouts.speed.stats_g); + } + + setTime(seconds) { + var time = ''; + var hours = Math.floor(seconds/3600); + if (hours) { + time += hours + ":"; + this.layouts.speed.time_u.label = " hrs"; + } else { + this.layouts.speed.time_u.label = "mins"; + } + + time += String(Math.floor((seconds%3600)/60)).padStart(2, '0') + ":"; + time += String(seconds % 60).padStart(2, '0'); + + this.layouts.speed.time.label = time; + this.renderIfLayoutActive("speed", this.layouts.speed.time_g); + } + + setTripDistance(distance) { + this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1) + this.renderIfLayoutActive("distance", this.layouts.distance.tripd_g); + } + + setTotalDistance(distance) { + this.layouts.distance.totald.label = this.convertDistance(distance).toFixed(1) + this.renderIfLayoutActive("distance", this.layouts.distance.totald_g); + } + + setDeviceAddress(address) { + this.layouts.status.addr.label = address + this.renderIfLayoutActive("status", this.layouts.status.addr_g); + } + + setStatus(status) { + this.layouts.status.status.label = status + this.renderIfLayoutActive("status", this.layouts.status.status_g); + } +} + +var BLECSC; +if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") { + // Emulator + BLECSC = require("blecsc-emu"); +} else { + // Actual hardware + BLECSC = require("blecsc"); +} +var blecsc = new BLECSC(); +var display = new CSCDisplay(); +var sensor = new CSCSensor(blecsc, display); + +E.on('kill',()=>{ + sensor.disconnect(); +}); + +Bangle.setUI("updown", d => { + sensor.interact(d); +}); + +sensor.connect(); +// Bangle.loadWidgets(); +// Bangle.drawWidgets(); diff --git a/apps/cycling/icons8-cycling-48.png b/apps/cycling/icons8-cycling-48.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc83859f1ac8d5b1d40aa787ab3c8bf339913fd GIT binary patch literal 1487 zcmV;=1u*)FP)BH^< zyW9n~y*qr_|Cil8&-47wInUcYcLAS#^2w*s6pf~HE?fR2B+da?2cQbX+sIIO#vR6} zC14F+`r0W52Ju37k`R;+ZG@6I2ZJY&z?9<-X7myuo-S{P&K=C?CBSq=-i|7FFn)>z zGlWX>KF@8r7q(gy1B1o}w0CMluJ4M^%-R8xwu0#!IG+Jvn>&239pdkH7KGgqBYtRX z)lMB~7s!Iy(Le8ue>L-ME?t%irW_D-wwd2XDaI0a{CFjS^1Q>z4XxT!ZQUV(a_xcY zE{>a0k)&U{slroVil2d!T}O7;Hcu~g6+8fRnFFc%UtKZp$@2HqpJHdAXn#>i&uvRZ zZKHNXw@71aWbpT6W)}f4c;Z))6)4X;oZQ+NP~X@#vf%BiZqhIKW&a1Sc;egR1P<-k z{(fzXNHrJz#96RAa7J5=dQ<-qCQE z#sWaFzaLh`#6j$w3wFY4@B61VVsb-5+>0Ap0N~oE+XF5)&8&G~VolTxt;^u;5muAI z_>fl;n3a~n52Q6S+yfz- z48{XqNg(<8jIQPm1UTEr`Kz%iCf2MA719#3*1J_6yDN_lu|C{ll(V;sW_)P6sQ&EA z`GY5RFQ4d%kKyF$lbAMStthU%DJ&Fw5yKGPCguXz*q;DLIxetq zQ&GO1>TEb!UZJ-wtLOsWr^WLUFR55a1JFZ+%wuOS_P?kzO>*eW))i2U?0003g14xmMV5(D= zZ)+Reo$m(#vmg$-zE*C@{b}Wv+)8JAH_U(7zD^lnc3c6*O@;Zc5&+BCtRi9%4{8tU z3bbC@QYuHZ@;KzXthu_VPlk?lHvFL^Vv3A`CVBba!c>K#2 zh4}zvSAa?YfJv1h2a}T^AOir1(O)_<)&blI;0B0S5!q)?A-+9YXT$Nbb2jB0BV5p zhYQ{teA!y4z+_T~w~NKZFaYQQ_@E@wkzQ5L?*O2Y{AYqV9l*<0I7)5>`!8};uXvO1 zvY0C{nbhHJVlELf;rgAm7ohxG!J2nQ8obi{Vaw^`W#@D(s8-t^BGESUbz+>c_z9mZ pnU^5{H~9$VZN5)F`Q(##^&bl8rz8LGDIEX+002ovPDHLkV1j9?%0U1C literal 0 HcmV?d00001 diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json new file mode 100644 index 000000000..917658fad --- /dev/null +++ b/apps/cycling/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "cycling", + "name": "Bangle Cycling", + "shortName": "Cycling", + "version": "0.01", + "description": "Display live values from a BLE CSC sensor", + "icon": "icons8-cycling-48.png", + "tags": "outdoors,exercise,ble,bluetooth", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"cycling.app.js","url":"cycling.app.js"}, + {"name":"cycling.settings.js","url":"settings.js"}, + {"name":"blecsc","url":"blecsc.js"} + ] +} diff --git a/apps/cycling/settings.js b/apps/cycling/settings.js new file mode 100644 index 000000000..810d8afc0 --- /dev/null +++ b/apps/cycling/settings.js @@ -0,0 +1,37 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'cscsensor.json' + // initialize with default settings... + let s = { + 'wheelcirc': 2230, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + s[key] = saved[key]; + } + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + } + } + const menu = { + '': { 'title': 'Cycle speed sensor' }, + '< Back': back, + 'Wheel circ.(mm)': { + value: s.wheelcirc, + min: 800, + max: 2400, + step: 5, + onchange: save('wheelcirc'), + }, + } + E.showMenu(menu); +}) From 1523a15b8e9c271ee85da212fe66e5f3768874ce Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Sun, 20 Mar 2022 17:36:34 +0800 Subject: [PATCH 12/73] Update app.js Rewrite UI code and drawing --- apps/authentiwatch/app.js | 440 +++++++++++++++++++++----------------- 1 file changed, 248 insertions(+), 192 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index fb130969f..2734a064a 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -1,3 +1,4 @@ +const COUNTER_TRIANGLE_SIZE = 10; const TOKEN_EXTRA_HEIGHT = 16; var TOKEN_DIGITS_HEIGHT = 30; var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT; @@ -44,8 +45,8 @@ function b32decode(seedstr) { return retbuf; } -function doHmac(key, message, algo) { - var a = algos[algo]; +function hmac(key, message, algo) { + var a = algos[algo.toUpperCase()]; // RFC2104 if (key.length > a.blksz) { key = a.sha(key); @@ -66,50 +67,52 @@ function doHmac(key, message, algo) { } function formatOtp(otp, digits) { + // add 0 padding + var ret = "" + otp % Math.pow(10, digits); + while (ret.length < digits) { + ret = "0" + ret; + } + // add a space after every 3rd or 4th digit var re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : "."; - return otp.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); + return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); } -function hotp(d, token, calcHmac) { - var tick; +function hotp(token) { + var d = Date.now(); + var tick, next; if (token.period > 0) { // RFC6238 - timed - var seconds = Math.floor(d.getTime() / 1000); + var seconds = Math.floor(d / 1000); tick = Math.floor(seconds / token.period); + next = (tick + 1) * token.period * 1000; } else { // RFC4226 - counter tick = -token.period; + next = d + 30000; } var msg = new Uint8Array(8); var v = new DataView(msg.buffer); v.setUint32(0, tick >> 16 >> 16); v.setUint32(4, tick & 0xFFFFFFFF); - var ret = CALCULATING; - if (calcHmac) { - try { - var hash = doHmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); - ret = "" + hash % Math.pow(10, token.digits); - while (ret.length < token.digits) { - ret = "0" + ret; - } - // add a space after every 3rd or 4th digit - ret = formatOtp(ret, token.digits); - } catch(err) { - ret = NOT_SUPPORTED; - } + var ret; + try { + ret = hmac(b32decode(token.secret), msg, token.algorithm); + ret = formatOtp(ret, token.digits); + } catch(err) { + ret = NOT_SUPPORTED; } - return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; + return {hotp:ret, next:next}; } +// Tokens are displayed in three states: +// 1. Unselected (state.id==-1) +// 2. Selected, inactive (no code) (state.id!=-1,state.hotp.hotp=="") +// 3. Selected, active (code showing) (state.id!=-1,state.hotp.hotp!="") var fontszCache = {}; var state = { - listy: 0, - prevcur:0, - curtoken:-1, - nextTime:0, - otp:"", - rem:0, - hide:0 + listy:0, // list scroll position + id:-1, // current token ID + hotp:{hotp:"",next:0} }; function sizeFont(id, txt, w) { @@ -125,117 +128,42 @@ function sizeFont(id, txt, w) { } } -function drawToken(id, r) { - var x1 = r.x; - var y1 = r.y; - var x2 = r.x + r.w - 1; - var y2 = r.y + r.h - 1; - var adj, lbl; - g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ), - Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2)); - lbl = tokens[id].label.substr(0, 10); - if (id == state.curtoken) { - // current token - g.setColor(g.theme.fgH) - .setBgColor(g.theme.bgH) - .setFont("Vector", TOKEN_EXTRA_HEIGHT) - // center just below top line - .setFontAlign(0, -1, 0); - adj = y1; - } else { - g.setColor(g.theme.fg) - .setBgColor(g.theme.bg); - sizeFont("l" + id, lbl, r.w); - // center in box - g.setFontAlign(0, 0, 0); - adj = (y1 + y2) / 2; - } - g.clearRect(x1, y1, x2, y2) - .drawString(lbl, (x1 + x2) / 2, adj, false); - if (id == state.curtoken) { - if (tokens[id].period > 0) { - // timed - draw progress bar - let xr = Math.floor(Bangle.appRect.w * state.rem / tokens[id].period); - g.fillRect(x1, y2 - 4, xr, y2 - 1); - adj = 0; - } else { - // counter - draw triangle as swipe hint - let yc = (y1 + y2) / 2; - g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]); - adj = 12; - } - // digits just below label - sizeFont("d" + id, state.otp, r.w - adj); - g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + TOKEN_EXTRA_HEIGHT, false); - } - g.setClipRect(0, 0, g.getWidth(), g.getHeight()); -} +tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy; +half = n => Math.floor(n / 2); -function draw() { - var timerfn = exitApp; - var timerdly = 10000; - var d = new Date(); - if (state.curtoken != -1) { - var t = tokens[state.curtoken]; - if (state.otp == CALCULATING) { - state.otp = hotp(d, t, true).hotp; - } - if (d.getTime() > state.nextTime) { - if (state.hide == 0) { - // auto-hide the current token - if (state.curtoken != -1) { - state.prevcur = state.curtoken; - state.curtoken = -1; +function timerCalc() { + let timerfn = exitApp; + let timerdly = 10000; + let id = state.id; + if (id != -1) { + if (state.hotp.hotp != "") { + if (tokens[id].period > 0) { + // timed HOTP + if (state.hotp.next < Date.now()) { + if (state.cnt > 0) { + --state.cnt; + state.hotp = hotp(tokens[id]); + } else { + state.hotp.hotp = ""; + } + timerdly = 1; + timerfn = updateCurrentToken; + } else { + timerdly = 1000; + timerfn = updateProgressBar; } - state.nextTime = 0; } else { - // time to generate a new token - var r = hotp(d, t, state.otp != ""); - state.nextTime = r.next; - state.otp = r.hotp; - if (t.period <= 0) { - state.hide = 1; + // counter HOTP + if (state.cnt > 0) { + --state.cnt; + timerdly = 30000; + } else { + state.hotp.hotp = ""; + timerdly = 1; } - state.hide--; + timerfn = updateCurrentToken; } } - state.rem = Math.max(0, Math.floor((state.nextTime - d.getTime()) / 1000)); - } - if (tokens.length > 0) { - var drewcur = false; - var id = Math.floor(state.listy / TOKEN_HEIGHT); - var y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; - while (id < tokens.length && y < Bangle.appRect.y2) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); - if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { - drewcur = true; - } - id++; - y += TOKEN_HEIGHT; - } - if (drewcur) { - // the current token has been drawn - schedule a redraw - if (tokens[state.curtoken].period > 0) { - timerdly = (state.otp == CALCULATING) ? 1 : 1000; // timed - } else { - timerdly = state.nexttime - d.getTime(); // counter - } - timerfn = draw; - if (tokens[state.curtoken].period <= 0) { - state.hide = 0; - } - } else { - // de-select the current token if it is scrolled out of view - if (state.curtoken != -1) { - state.prevcur = state.curtoken; - state.curtoken = -1; - } - state.nexttime = 0; - } - } else { - g.setFont("Vector", TOKEN_DIGITS_HEIGHT) - .setFontAlign(0, 0, 0) - .drawString(NO_TOKENS, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } if (state.drawtimer) { clearTimeout(state.drawtimer); @@ -243,101 +171,230 @@ function draw() { state.drawtimer = setTimeout(timerfn, timerdly); } -function onTouch(zone, e) { - if (e) { - var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / TOKEN_HEIGHT); - if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { - id = -1; - } - if (state.curtoken != id) { - if (id != -1) { - var y = id * TOKEN_HEIGHT - state.listy; - if (y < 0) { - state.listy += y; - y = 0; +function updateCurrentToken() { + drawToken(state.id); + timerCalc(); +} + +function updateProgressBar() { + drawProgressBar(); + timerCalc(); +} + +function drawProgressBar() { + let id = state.id; + if (id != -1) { + if (tokens[id].period > 0) { + let rem = Math.floor((state.hotp.next - Date.now()) / 1000); + if (rem >= 0) { + let y1 = tokenY(id); + let y2 = y1 + TOKEN_HEIGHT - 1; + if (y2 >= AR.y && y1 <= AR.y2) { + // token visible + if ((y2 - 3) <= AR.y2) + { + // progress bar visible + y2 = Math.min(y2, AR.y2); + rem = Math.min(rem, tokens[id].period); + let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x; + g.setColor(g.theme.fgH) + .setBgColor(g.theme.bgH) + .fillRect(AR.x, y2 - 3, xr, y2) + .clearRect(xr + 1, y2 - 3, AR.x2, y2); + } + } else { + // token not visible + state.id = -1; } - y += TOKEN_HEIGHT; - if (y > Bangle.appRect.h) { - state.listy += (y - Bangle.appRect.h); - } - state.otp = ""; } - state.nextTime = 0; - state.curtoken = id; - state.hide = 2; } } - draw(); +} + +// id = token ID number (0...) +function drawToken(id) { + var x1 = AR.x; + var y1 = tokenY(id); + var x2 = AR.x2; + var y2 = y1 + TOKEN_HEIGHT - 1; + var adj, lbl; + g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2)); + lbl = tokens[id].label.substr(0, 10); + if (id === state.id) { + g.setColor(g.theme.fgH) + .setBgColor(g.theme.bgH); + } else { + g.setColor(g.theme.fg) + .setBgColor(g.theme.bg); + } + if (id == state.id && state.hotp.hotp != "") { + // small label centered just below top line + g.setFont("Vector", TOKEN_EXTRA_HEIGHT) + .setFontAlign(0, -1, 0); + adj = y1; + } else { + // large label centered in box + sizeFont("l" + id, lbl, AR.w); + g.setFontAlign(0, 0, 0); + adj = half(y1 + y2); + } + g.clearRect(x1, y1, x2, y2) + .drawString(lbl, half(x1 + x2), adj, false); + if (id == state.id && state.hotp.hotp != "") { + adj = 0; + if (tokens[id].period <= 0) { + // counter - draw triangle as swipe hint + let yc = half(y1 + y2); + adj = COUNTER_TRIANGLE_SIZE; + g.fillPoly([AR.x, yc, AR.x + adj, yc - adj, AR.x + adj, yc + adj]); + adj += 2; + } + // digits just below label + x1 = half(x1 + adj + x2); + y1 += TOKEN_EXTRA_HEIGHT; + if (state.hotp.hotp == CALCULATING) { + sizeFont("c", CALCULATING, AR.w - adj); + g.drawString(CALCULATING, x1, y1, false) + .flip(); + state.hotp = hotp(tokens[id]); + g.clearRect(AR.x + adj, y1, AR.x2, y2); + } + sizeFont("d" + id, state.hotp.hotp, AR.w - adj); + g.drawString(state.hotp.hotp, x1, y1, false); + if (tokens[id].period > 0) { + drawProgressBar(); + } + } + g.setClipRect(0, 0, g.getWidth(), g.getHeight()); +} + +function startupDraw() { + if (tokens.length > 0) { + let id = 0; + let y = tokenY(id); + while (id < tokens.length && y < AR.y2) { + if ((y + TOKEN_HEIGHT) > AR.y) { + drawToken(id); + } + id++; + y += TOKEN_HEIGHT; + } + } else { + let x = AR.x + half(AR.w); + let y = AR.y + half(AR.h); + g.setFont("Vector", TOKEN_DIGITS_HEIGHT) + .setFontAlign(0, 0, 0) + .drawString(NO_TOKENS, x, y, false); + } + timerCalc(); } function onDrag(e) { - if (e.b != 0 && e.x < g.getWidth() && e.y < g.getHeight() && e.dy != 0) { - var y = Math.max(0, Math.min(state.listy - e.dy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h)); + state.cnt = 1; + if (e.b != 0 && e.dy != 0) { + var y = E.clip(state.listy - e.dy, 0, tokens.length * TOKEN_HEIGHT - AR.h); if (state.listy != y) { var id, dy = state.listy - y; state.listy = y; - g.setClipRect(Bangle.appRect.x,Bangle.appRect.y,Bangle.appRect.x2,Bangle.appRect.y2) + g.setClipRect(AR.x, AR.y, AR.x2, AR.y2) .scroll(0, dy); if (dy > 0) { id = Math.floor((state.listy + dy) / TOKEN_HEIGHT); - y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; + y = tokenY(id + 1); do { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); + drawToken(id); id--; y -= TOKEN_HEIGHT; - } while (y > 0); + } while (y > AR.y); } if (dy < 0) { - id = Math.floor((state.listy + dy + Bangle.appRect.h) / TOKEN_HEIGHT); - y = id * TOKEN_HEIGHT + Bangle.appRect.y - state.listy; - while (y < Bangle.appRect.y2) { - drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:TOKEN_HEIGHT}); + id = Math.floor((state.listy + dy + AR.h) / TOKEN_HEIGHT); + y = tokenY(id); + while (y < AR.y2) { + drawToken(id); id++; y += TOKEN_HEIGHT; } } } } + if (e.b == 0) { + timerCalc(); + } +} + +function changeId(id) { + if (id != state.id) { + state.hotp.hotp = CALCULATING; + let pid = state.id; + state.id = id; + if (pid != -1) { + drawToken(pid); + } + if (id != -1) { + drawToken( id); + } + timerCalc(); + } +} + +function onTouch(zone, e) { + state.cnt = 1; + if (e) { + var id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT); + if (id == state.id || tokens.length == 0 || id >= tokens.length) { + id = -1; + } + if (state.id != id) { + if (id != -1) { + // scroll token into view if necessary + var fakee = {b:1,x:0,y:0,dx:0,dy:0}; + var y = id * TOKEN_HEIGHT - state.listy; + if (y < 0) { + fakee.dy -= y; + y = 0; + } + y += TOKEN_HEIGHT; + if (y > AR.h) { + fakee.dy -= (y - AR.h); + } + onDrag(fakee); + } + changeId(id); + } + } } function onSwipe(e) { + state.cnt = 1; + let id = state.id; if (e == 1) { exitApp(); } - if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { - tokens[state.curtoken].period--; + if (e == -1 && id != -1 && tokens[id].period <= 0) { + tokens[id].period--; let newsettings={tokens:tokens,misc:settings.misc}; require("Storage").writeJSON("authentiwatch.json", newsettings); - state.nextTime = 0; - state.otp = ""; - state.hide = 2; + state.hotp.hotp = CALCULATING; + drawToken(id); } - draw(); } function bangle1Btn(e) { + state.cnt = 1; if (tokens.length > 0) { - if (state.curtoken == -1) { - state.curtoken = state.prevcur; - } else { - switch (e) { - case -1: state.curtoken--; break; - case 1: state.curtoken++; break; - } + var id = state.id; + switch (e) { + case -1: id--; break; + case 1: id++; break; } - state.curtoken = Math.max(state.curtoken, 0); - state.curtoken = Math.min(state.curtoken, tokens.length - 1); - state.listy = state.curtoken * TOKEN_HEIGHT; - state.listy -= (Bangle.appRect.h - TOKEN_HEIGHT) / 2; - state.listy = Math.min(state.listy, tokens.length * TOKEN_HEIGHT - Bangle.appRect.h); - state.listy = Math.max(state.listy, 0); - var fakee = {}; - fakee.y = state.curtoken * TOKEN_HEIGHT - state.listy + Bangle.appRect.y; - state.curtoken = -1; - state.nextTime = 0; - onTouch(0, fakee); + id = E.clip(id, 0, tokens.length - 1); + var fakee = {b:1,x:0,y:0,dx:0}; + fakee.dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, tokens.length * TOKEN_HEIGHT - AR.h); + onDrag(fakee); + changeId(id); } else { - draw(); // resets idle timer + timerCalc(); } } @@ -356,8 +413,7 @@ if (typeof BTN2 == 'number') { setWatch(function(){exitApp(); }, BTN1, {edge:"falling", debounce:50}); } Bangle.loadWidgets(); - -// Clear the screen once, at startup -g.clear(); -draw(); +const AR = Bangle.appRect; +g.clear(); // Clear the screen once, at startup +startupDraw(); Bangle.drawWidgets(); From 14aca8db0fc687e162ec59b735399f09c9c6bf68 Mon Sep 17 00:00:00 2001 From: Joseph Paul Date: Sun, 20 Mar 2022 09:55:00 +0100 Subject: [PATCH 13/73] Cycling: Re-enable widgets; optimise font sizes --- .../{cscsensor.app.js => cycling.app.js} | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) rename apps/cycling/{cscsensor.app.js => cycling.app.js} (93%) diff --git a/apps/cycling/cscsensor.app.js b/apps/cycling/cycling.app.js similarity index 93% rename from apps/cycling/cscsensor.app.js rename to apps/cycling/cycling.app.js index 9de3f5a3e..4b53046d9 100644 --- a/apps/cycling/cscsensor.app.js +++ b/apps/cycling/cycling.app.js @@ -1,6 +1,6 @@ const Layout = require('Layout'); -const SETTINGS_FILE = 'cscsensor.json'; +const SETTINGS_FILE = 'cycling.json'; const storage = require('Storage'); const RECONNECT_TIMEOUT = 4000; @@ -57,7 +57,7 @@ class CSCSensor { connect() { this.connected = false; this.setLayout(0); - this.display.setStatus("Connecting..."); + this.display.setStatus("Connecting"); console.log("Trying to connect to BLE CSC"); // Hook up events @@ -180,7 +180,8 @@ class CSCDisplay { constructor() { this.metric = true; this.fontLabel = "6x8"; - this.fontMed = "15%"; + this.fontSmall = "15%"; + this.fontMed = "18%"; this.fontLarge = "32%"; this.currentLayout = "status"; this.layouts = {}; @@ -206,7 +207,7 @@ class CSCDisplay { fillx: 1, pad: 4, bgCol: "#000", - height: 32, + height: 36, c: [ {type: undefined, width: 32, halign: -1}, {type: "txt", id: "time", label: "00:00", font: this.fontMed, bgCol: "#000", col: "#fff", width: 122}, @@ -218,7 +219,7 @@ class CSCDisplay { id: "stats_g", fillx: 1, bgCol: "#fff", - height: 32, + height: 36, c: [ { type: "v", @@ -226,7 +227,7 @@ class CSCDisplay { bgCol: "#fff", c: [ {type: "txt", id: "max_l", label: "MAX", font: this.fontLabel, col: "#000"}, - {type: "txt", id: "max", label: "00.0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 69}, + {type: "txt", id: "max", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69}, ], }, { @@ -235,7 +236,7 @@ class CSCDisplay { bgCol: "#fff", c: [ {type: "txt", id: "avg_l", label: "AVG", font: this.fontLabel, col: "#000"}, - {type: "txt", id: "avg", label: "00.0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 69}, + {type: "txt", id: "avg", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69}, ], }, {type: "txt", id: "stats_u", label: " km/h", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, @@ -245,6 +246,7 @@ class CSCDisplay { }); this.layouts.distance = new Layout({ type: "v", + bgCol: "#fff", c: [ { type: "h", @@ -264,12 +266,12 @@ class CSCDisplay { id: "totald_g", fillx: 1, pad: 4, - bgCol: "#000", + bgCol: "#fff", height: 32, c: [ - {type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 36}, - {type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#000", col: "#fff", width: 118}, - {type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90}, + {type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36}, + {type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118}, + {type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, ] }, ], @@ -284,7 +286,7 @@ class CSCDisplay { bgCol: "#fff", height: 100, c: [ - {type: "txt", id: "status", label: "Bangle Cycling", font: this.fontMed, bgCol: "#fff", col: "#000", width: 176, wrap: 1}, + {type: "txt", id: "status", label: "Bangle Cycling", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 176, wrap: 1}, ] }, { @@ -309,6 +311,7 @@ class CSCDisplay { g.clear(); this.layouts[layout].update(); this.layouts[layout].render(); + Bangle.drawWidgets(); } renderIfLayoutActive(layout, node) { @@ -380,7 +383,12 @@ class CSCDisplay { } setTotalDistance(distance) { - this.layouts.distance.totald.label = this.convertDistance(distance).toFixed(1) + const distance = this.convertDistance(distance); + if (distance >= 1000) { + this.layouts.distance.totald.label = String(Math.round(distance)); + } else { + this.layouts.distance.totald.label = distance.toFixed(1); + } this.renderIfLayoutActive("distance", this.layouts.distance.totald_g); } @@ -415,6 +423,5 @@ Bangle.setUI("updown", d => { sensor.interact(d); }); +Bangle.loadWidgets(); sensor.connect(); -// Bangle.loadWidgets(); -// Bangle.drawWidgets(); From a44c62630a0149abd9324c1a00b5ac13319abeaa Mon Sep 17 00:00:00 2001 From: Joseph Paul Date: Sun, 20 Mar 2022 10:51:00 +0100 Subject: [PATCH 14/73] Cycling: Implement settings --- apps/cycling/README.md | 20 +++++++---- apps/cycling/cycling.app.js | 60 ++++++++++++++++++++++--------- apps/cycling/settings.js | 72 +++++++++++++++++++++++-------------- 3 files changed, 103 insertions(+), 49 deletions(-) diff --git a/apps/cycling/README.md b/apps/cycling/README.md index 61ba1d455..7ba8ee224 100644 --- a/apps/cycling/README.md +++ b/apps/cycling/README.md @@ -1,7 +1,7 @@ # Cycling > Displays data from a BLE Cycling Speed and Cadence sensor. -*Fork of the CSCSensor app using the layout library and separate module for CSC functionality* +*This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.* The following data are displayed: - curent speed @@ -11,16 +11,24 @@ The following data are displayed: - trip distance - total distance -Total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, a absolute value that persists throughout the lifetime of the sensor and never rolls over. +Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over. **Cadence / Crank features are currently not implemented** -# TODO -* Settings: imperial/metric -* Store circumference per device address +## Usage +Open the app and connect to a CSC sensor. + +Upon first connection, close the app afain and enter the settings app to configure the wheel circumference. The total circumference is (cm + mm) - it is split up into two values for ease of configuration. Check the status screen inside the Cycling app while connected to see the address of the currently connected sensor (if you need to differentiate between multiple sensors). + +Inside the Cycling app, use button / tap screen to: +- cycle through screens (if connected) +- reconnect (if connection aborted) + +## TODO * Sensor battery status * Implement crank events / show cadence * Bangle.js 1 compatibility +* Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike) -# Development +## Development There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage. diff --git a/apps/cycling/cycling.app.js b/apps/cycling/cycling.app.js index 4b53046d9..268284a29 100644 --- a/apps/cycling/cycling.app.js +++ b/apps/cycling/cycling.app.js @@ -1,7 +1,11 @@ const Layout = require('Layout'); +const storage = require('Storage'); const SETTINGS_FILE = 'cycling.json'; -const storage = require('Storage'); +const SETTINGS_DEFAULT = { + sensors: {}, + metric: true, +}; const RECONNECT_TIMEOUT = 4000; const MAX_CONN_ATTEMPTS = 2; @@ -13,8 +17,8 @@ class CSCSensor { this.display = display; // Load settings - this.settings = storage.readJSON(SETTINGS_FILE, 1) || {}; - this.wheelCirc = (this.settings.wheelcirc || 2230) / 1000; // unit: m + this.settings = storage.readJSON(SETTINGS_FILE, true) || SETTINGS_DEFAULT; + this.wheelCirc = undefined; // CSC runtime variables this.movingTime = 0; // unit: s @@ -34,13 +38,16 @@ class CSCSensor { // Layout configuration this.layout = 0; this.display.useMetricUnits(true); - // this.display.useMetricUnits(!require("locale").speed(1).toString().endsWith("mph")); + this.deviceAddress = undefined; + this.display.useMetricUnits((this.settings.metric)); } onDisconnect(event) { console.log("disconnected ", event); this.connected = false; + this.wheelCirc = undefined; + this.setLayout(0); this.display.setDeviceAddress("unknown"); @@ -51,7 +58,23 @@ class CSCSensor { this.display.setStatus("Disconnected"); setTimeout(this.connect.bind(this), RECONNECT_TIMEOUT); } + } + loadCircumference() { + if (!this.deviceAddress) return; + + // Add sensor to settings if not present + if (!this.settings.sensors[this.deviceAddress]) { + this.settings.sensors[this.deviceAddress] = { + cm: 223, + mm: 0, + }; + storage.writeJSON(SETTINGS_FILE, this.settings); + } + + const high = this.settings.sensors[this.deviceAddress].cm || 223; + const low = this.settings.sensors[this.deviceAddress].mm || 0; + this.wheelCirc = (10*high + low) / 1000; } connect() { @@ -70,12 +93,14 @@ class CSCSensor { this.failedAttempts = 0; this.failed = false; this.connected = true; - var addr = this.blecsc.getDeviceAddress(); - console.log("Connected to " + addr); + this.deviceAddress = this.blecsc.getDeviceAddress(); + console.log("Connected to " + this.deviceAddress); - this.display.setDeviceAddress(addr); + this.display.setDeviceAddress(this.deviceAddress); this.display.setStatus("Connected"); + this.loadCircumference(); + // Switch to speed screen in 2s setTimeout(function() { this.setLayout(1); @@ -90,9 +115,9 @@ class CSCSensor { disconnect() { this.blecsc.disconnect(); - this.connected = false; + this.reset(); this.setLayout(0); - this.display.setStatus("Disconnected") + this.display.setStatus("Disconnected"); } setLayout(num) { @@ -110,6 +135,7 @@ class CSCSensor { this.connected = false; this.failed = false; this.failedAttempts = 0; + this.wheelCirc = undefined; } interact(d) { @@ -127,7 +153,7 @@ class CSCSensor { updateScreen() { var tripDist = this.cwrTrip * this.wheelCirc; - var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0 + var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0; this.display.setTotalDistance(this.cwr * this.wheelCirc); this.display.setTripDistance(tripDist); @@ -297,7 +323,7 @@ class CSCDisplay { bgCol: "#fff", height: 32, c: [ - { type: "txt", id: "addr_l", label: "MAC", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 }, + { type: "txt", id: "addr_l", label: "ADDR", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 }, { type: "txt", id: "addr", label: "unknown", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 140 }, ] }, @@ -316,13 +342,13 @@ class CSCDisplay { renderIfLayoutActive(layout, node) { if (layout != this.currentLayout) return; - this.layouts[layout].render(node) + this.layouts[layout].render(node); } useMetricUnits(metric) { this.metric = metric; - console.log("using " + (metric ? "metric" : "imperial") + " units"); + // console.log("using " + (metric ? "metric" : "imperial") + " units"); var speedUnit = metric ? "km/h" : "mph"; this.layouts.speed.speed_u.label = speedUnit; @@ -378,12 +404,12 @@ class CSCDisplay { } setTripDistance(distance) { - this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1) + this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1); this.renderIfLayoutActive("distance", this.layouts.distance.tripd_g); } setTotalDistance(distance) { - const distance = this.convertDistance(distance); + distance = this.convertDistance(distance); if (distance >= 1000) { this.layouts.distance.totald.label = String(Math.round(distance)); } else { @@ -393,12 +419,12 @@ class CSCDisplay { } setDeviceAddress(address) { - this.layouts.status.addr.label = address + this.layouts.status.addr.label = address; this.renderIfLayoutActive("status", this.layouts.status.addr_g); } setStatus(status) { - this.layouts.status.status.label = status + this.layouts.status.status.label = status; this.renderIfLayoutActive("status", this.layouts.status.status_g); } } diff --git a/apps/cycling/settings.js b/apps/cycling/settings.js index 810d8afc0..445c4ca33 100644 --- a/apps/cycling/settings.js +++ b/apps/cycling/settings.js @@ -3,35 +3,55 @@ * @param {function} back Use back() to return to settings menu */ (function(back) { - const SETTINGS_FILE = 'cscsensor.json' - // initialize with default settings... - let s = { - 'wheelcirc': 2230, - } - // ...and overwrite them with any saved values - // This way saved values are preserved if a new version adds more settings const storage = require('Storage') - const saved = storage.readJSON(SETTINGS_FILE, 1) || {} - for (const key in saved) { - s[key] = saved[key]; - } - // creates a function to safe a specific setting, e.g. save('color')(1) - function save(key) { - return function (value) { - s[key] = value; - storage.write(SETTINGS_FILE, s); - } - } + const SETTINGS_FILE = 'cycling.json' + + // Set default values and merge with stored values + let settings = Object.assign({ + metric: true, + sensors: {}, + }, (storage.readJSON(SETTINGS_FILE, true) || {})); + const menu = { - '': { 'title': 'Cycle speed sensor' }, + '': { 'title': 'Cycling' }, '< Back': back, - 'Wheel circ.(mm)': { - value: s.wheelcirc, - min: 800, - max: 2400, - step: 5, - onchange: save('wheelcirc'), + 'Units': { + value: settings.metric, + format: v => v ? 'metric' : 'imperial', + onchange: (metric) => { + settings.metric = metric; + storage.writeJSON(SETTINGS_FILE, settings); + }, }, } + + const sensorMenus = {}; + for (var addr of Object.keys(settings.sensors)) { + // Define sub menu + sensorMenus[addr] = { + '': { title: addr }, + '< Back': () => E.showMenu(menu), + 'cm': { + value: settings.sensors[addr].cm, + min: 80, max: 240, step: 1, + onchange: (v) => { + settings.sensors[addr].cm = v; + storage.writeJSON(SETTINGS_FILE, settings); + }, + }, + '+ mm': { + value: settings.sensors[addr].mm, + min: 0, max: 9, step: 1, + onchange: (v) => { + settings.sensors[addr].mm = v; + storage.writeJSON(SETTINGS_FILE, settings); + }, + }, + }; + + // Add entry to main menu + menu[addr] = () => E.showMenu(sensorMenus[addr]); + } + E.showMenu(menu); -}) +})(load) From 433e3a4967579e056f067675011ee3205491e00b Mon Sep 17 00:00:00 2001 From: Joseph Paul Date: Sun, 20 Mar 2022 10:59:24 +0100 Subject: [PATCH 15/73] Cycling: Add JS icon --- apps/cycling/cycling.icon.js | 1 + apps/cycling/metadata.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 apps/cycling/cycling.icon.js diff --git a/apps/cycling/cycling.icon.js b/apps/cycling/cycling.icon.js new file mode 100644 index 000000000..12c597956 --- /dev/null +++ b/apps/cycling/cycling.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA")) diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json index 917658fad..cb4260bb2 100644 --- a/apps/cycling/metadata.json +++ b/apps/cycling/metadata.json @@ -11,6 +11,7 @@ "storage": [ {"name":"cycling.app.js","url":"cycling.app.js"}, {"name":"cycling.settings.js","url":"settings.js"}, - {"name":"blecsc","url":"blecsc.js"} + {"name":"blecsc","url":"blecsc.js"}, + {"name":"cycling.img","url":"cycling.icon.js","evaluate": true} ] } From bda630ed0869bb74ce72c517773ee2caea216e55 Mon Sep 17 00:00:00 2001 From: Joseph Paul Date: Sun, 20 Mar 2022 11:05:54 +0100 Subject: [PATCH 16/73] Cycling: don't call settings function --- apps/cycling/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cycling/settings.js b/apps/cycling/settings.js index 445c4ca33..76303379d 100644 --- a/apps/cycling/settings.js +++ b/apps/cycling/settings.js @@ -54,4 +54,4 @@ } E.showMenu(menu); -})(load) +}) From ed36b7286e2b8ff5505a504df0e429c898233f99 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Sun, 20 Mar 2022 23:11:18 +0800 Subject: [PATCH 17/73] Update app.js Workaround Bangle1 issues --- apps/authentiwatch/app.js | 97 ++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 2734a064a..8e25e5ef2 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -135,34 +135,32 @@ function timerCalc() { let timerfn = exitApp; let timerdly = 10000; let id = state.id; - if (id != -1) { - if (state.hotp.hotp != "") { - if (tokens[id].period > 0) { - // timed HOTP - if (state.hotp.next < Date.now()) { - if (state.cnt > 0) { - --state.cnt; - state.hotp = hotp(tokens[id]); - } else { - state.hotp.hotp = ""; - } - timerdly = 1; - timerfn = updateCurrentToken; - } else { - timerdly = 1000; - timerfn = updateProgressBar; - } - } else { - // counter HOTP + if (id != -1 && state.hotp.hotp != "") { + if (tokens[id].period > 0) { + // timed HOTP + if (state.hotp.next < Date.now()) { if (state.cnt > 0) { --state.cnt; - timerdly = 30000; + state.hotp = hotp(tokens[id]); } else { state.hotp.hotp = ""; - timerdly = 1; } + timerdly = 1; timerfn = updateCurrentToken; + } else { + timerdly = 1000; + timerfn = updateProgressBar; } + } else { + // counter HOTP + if (state.cnt > 0) { + --state.cnt; + timerdly = 30000; + } else { + state.hotp.hotp = ""; + timerdly = 1; + } + timerfn = updateCurrentToken; } } if (state.drawtimer) { @@ -183,29 +181,27 @@ function updateProgressBar() { function drawProgressBar() { let id = state.id; - if (id != -1) { - if (tokens[id].period > 0) { - let rem = Math.floor((state.hotp.next - Date.now()) / 1000); - if (rem >= 0) { - let y1 = tokenY(id); - let y2 = y1 + TOKEN_HEIGHT - 1; - if (y2 >= AR.y && y1 <= AR.y2) { - // token visible - if ((y2 - 3) <= AR.y2) - { - // progress bar visible - y2 = Math.min(y2, AR.y2); - rem = Math.min(rem, tokens[id].period); - let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x; - g.setColor(g.theme.fgH) - .setBgColor(g.theme.bgH) - .fillRect(AR.x, y2 - 3, xr, y2) - .clearRect(xr + 1, y2 - 3, AR.x2, y2); - } - } else { - // token not visible - state.id = -1; + if (id != -1 && tokens[id].period > 0) { + let rem = Math.floor((state.hotp.next - Date.now()) / 1000); + if (rem >= 0) { + let y1 = tokenY(id); + let y2 = y1 + TOKEN_HEIGHT - 1; + if (y2 >= AR.y && y1 <= AR.y2) { + // token visible + if ((y2 - 3) <= AR.y2) + { + // progress bar visible + y2 = Math.min(y2, AR.y2); + rem = Math.min(rem, tokens[id].period); + let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x; + g.setColor(g.theme.fgH) + .setBgColor(g.theme.bgH) + .fillRect(AR.x, y2 - 3, xr, y2) + .clearRect(xr + 1, y2 - 3, AR.x2, y2); } + } else { + // token not visible + state.id = -1; } } } @@ -265,10 +261,10 @@ function drawToken(id) { drawProgressBar(); } } - g.setClipRect(0, 0, g.getWidth(), g.getHeight()); + g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); } -function startupDraw() { +function drawAll() { if (tokens.length > 0) { let id = 0; let y = tokenY(id); @@ -391,8 +387,13 @@ function bangle1Btn(e) { id = E.clip(id, 0, tokens.length - 1); var fakee = {b:1,x:0,y:0,dx:0}; fakee.dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, tokens.length * TOKEN_HEIGHT - AR.h); - onDrag(fakee); - changeId(id); + //onDrag(fakee); + //changeId(id); + // onDrag() (specifically g.scroll()) doesn't appear to work with the Bangle1 + state.id = id; + state.hotp.hotp = CALCULATING; + state.listy -= fakee.dy; + drawAll(); } else { timerCalc(); } @@ -415,5 +416,5 @@ if (typeof BTN2 == 'number') { Bangle.loadWidgets(); const AR = Bangle.appRect; g.clear(); // Clear the screen once, at startup -startupDraw(); +drawAll(); Bangle.drawWidgets(); From 338a659bbdf205f7ca2925aedab893b2a052a24e Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Mon, 21 Mar 2022 11:02:52 +0100 Subject: [PATCH 18/73] bledetect - Set bangleJs compatible - Show widgets --- apps/bledetect/ChangeLog | 1 + apps/bledetect/bledetect.js | 2 ++ apps/bledetect/metadata.json | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog index e52015f04..e9b98e08c 100644 --- a/apps/bledetect/ChangeLog +++ b/apps/bledetect/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fixed issue with wrong device informations 0.03: Ensure manufacturer:undefined doesn't overflow screen +0.04: Set Bangle.js 2 compatible, show widgets diff --git a/apps/bledetect/bledetect.js b/apps/bledetect/bledetect.js index ca8699f9a..f3fc70e92 100644 --- a/apps/bledetect/bledetect.js +++ b/apps/bledetect/bledetect.js @@ -5,6 +5,7 @@ let menu = { function showMainMenu() { menu["< Back"] = () => load(); + Bangle.drawWidgets(); return E.showMenu(menu); } @@ -55,5 +56,6 @@ function waitMessage() { E.showMessage("scanning"); } +Bangle.loadWidgets(); scan(); waitMessage(); diff --git a/apps/bledetect/metadata.json b/apps/bledetect/metadata.json index f5e0ffb19..0c30fe8f6 100644 --- a/apps/bledetect/metadata.json +++ b/apps/bledetect/metadata.json @@ -2,11 +2,11 @@ "id": "bledetect", "name": "BLE Detector", "shortName": "BLE Detector", - "version": "0.03", + "version": "0.04", "description": "Detect BLE devices and show some informations.", "icon": "bledetect.png", "tags": "app,bluetooth,tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"bledetect.app.js","url":"bledetect.js"}, From 3c7e830e1c536cf68f21f548fba276265d23dad2 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Mon, 21 Mar 2022 11:03:32 +0100 Subject: [PATCH 19/73] sleepphasealarm fix comment typo --- apps/sleepphasealarm/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index 39f9b59db..e963f2c40 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -29,11 +29,11 @@ function calc_ess(val) { if (nonmot) { slsnds+=1; if (slsnds >= sleepthresh) { - return true; // awake + return true; // sleep } } else { slsnds=0; - return false; // sleep + return false; // awake } } } From 127624b19fb293fc93a0d67f629fca1b269c29be Mon Sep 17 00:00:00 2001 From: chiefdaft Date: Mon, 21 Mar 2022 16:17:16 +0100 Subject: [PATCH 20/73] Changed screenshot pictures with ide dumps --- apps/game1024/README.md | 7 +++++-- apps/game1024/game1024_sc_dump_dark.png | Bin 0 -> 4146 bytes apps/game1024/game1024_sc_dump_light.png | Bin 0 -> 3806 bytes 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 apps/game1024/game1024_sc_dump_dark.png create mode 100644 apps/game1024/game1024_sc_dump_light.png diff --git a/apps/game1024/README.md b/apps/game1024/README.md index e52961424..474e8c97e 100644 --- a/apps/game1024/README.md +++ b/apps/game1024/README.md @@ -27,5 +27,8 @@ Use the side **BTN** to exit the game, score and tile positions will be saved. Game 1024 is based on Saming's 2048 and Misho M. Petkovic 1024game.org and conceptually similar to Threes by Asher Vollmer. -![Screenshot from the Banglejs 2 watch with the game in dark theme](./scrnshot_dn_300.jpg) -![Screenshot from the Banglejs 2 watch with the game in light theme](./scrnshot_lc_300.jpg) \ No newline at end of file +In Dark theme with numbers: +![Screenshot from the Banglejs 2 watch with the game in dark theme](./game1024_sc_dump_dark.png) + +In Light theme with characters: +![Screenshot from the Banglejs 2 watch with the game in light theme](./game1024_sc_dump_light.png) \ No newline at end of file diff --git a/apps/game1024/game1024_sc_dump_dark.png b/apps/game1024/game1024_sc_dump_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..15ecb87d9e7e0180b7443b717e0aeb5409563fa0 GIT binary patch literal 4146 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 literal 0 HcmV?d00001 diff --git a/apps/game1024/game1024_sc_dump_light.png b/apps/game1024/game1024_sc_dump_light.png new file mode 100644 index 0000000000000000000000000000000000000000..286257be4e4c689b6770b99db2a5a5f6b1b1e4e5 GIT binary patch literal 3806 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 Date: Tue, 22 Mar 2022 09:05:53 +1100 Subject: [PATCH 21/73] touchtimer: ADD ability to repeat the timer - By adding the REPEAT value at the end of buzzCount loop it allows for finshed to be shown for the full buzzCount before it changes the text to REPEAT --- apps/touchtimer/ChangeLog | 3 ++- apps/touchtimer/app.js | 7 +++++++ apps/touchtimer/metadata.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/touchtimer/ChangeLog b/apps/touchtimer/ChangeLog index 01904c6ea..fbabe6cb7 100644 --- a/apps/touchtimer/ChangeLog +++ b/apps/touchtimer/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial creation of the touch timer app -0.02: Add settings menu \ No newline at end of file +0.02: Add settings menu +0.03: Add ability to repeat last timer diff --git a/apps/touchtimer/app.js b/apps/touchtimer/app.js index ffa1af80a..61d3a08fd 100644 --- a/apps/touchtimer/app.js +++ b/apps/touchtimer/app.js @@ -141,6 +141,13 @@ var main = () => { if (buzzCount >= settings.buzzCount) { clearInterval(buzzIntervalId); buzzIntervalId = undefined; + + buttonStartPause.value = "REPEAT"; + buttonStartPause.draw(); + buttonStartPause.value = "START"; + timerCountDown = undefined; + timerEdit.draw(); + return; } else { Bangle.buzz(settings.buzzDuration * 1000, 1); diff --git a/apps/touchtimer/metadata.json b/apps/touchtimer/metadata.json index 645a0ce18..50c5f03dd 100644 --- a/apps/touchtimer/metadata.json +++ b/apps/touchtimer/metadata.json @@ -2,7 +2,7 @@ "id": "touchtimer", "name": "Touch Timer", "shortName": "Touch Timer", - "version": "0.02", + "version": "0.03", "description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.", "icon": "app.png", "tags": "tools", From 9f4119930259752b0d3f8b450d232ea3467b76ba Mon Sep 17 00:00:00 2001 From: Ronin0000 <89286474+Ronin0000@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:47:53 -0700 Subject: [PATCH 22/73] Update mmind.app.js --- apps/mmind/mmind.app.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js index e7def025d..10d315285 100644 --- a/apps/mmind/mmind.app.js +++ b/apps/mmind/mmind.app.js @@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) { break; case 4: //new game + turn = 0; play = [-1,-1,-1,-1]; game = []; endgame=false; @@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) { game = []; get_secret(); draw(); -//Bangle.loadWidgets(); -//Bangle.drawWidgets(); - - - - - From afb7a4846a705c6e9fc2165a8c031aabf3e6d054 Mon Sep 17 00:00:00 2001 From: Ronin0000 <89286474+Ronin0000@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:48:21 -0700 Subject: [PATCH 23/73] Update ChangeLog --- apps/mmind/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog index 939ac3b5d..040e62671 100644 --- a/apps/mmind/ChangeLog +++ b/apps/mmind/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Make sure to reset turns From 347f6783b72eb5950d4b39876111d093c38d1092 Mon Sep 17 00:00:00 2001 From: Ronin0000 <89286474+Ronin0000@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:48:31 -0700 Subject: [PATCH 24/73] Update metadata.json --- apps/mmind/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mmind/metadata.json b/apps/mmind/metadata.json index c2ed474b6..ea970ee23 100644 --- a/apps/mmind/metadata.json +++ b/apps/mmind/metadata.json @@ -3,7 +3,7 @@ "name": "Classic Mind Game", "shortName":"Master Mind", "icon": "mmind.png", - "version":"0.01", + "version":"0.02", "description": "This is the classic game for masterminds", "screenshots": [{"url":"screenshot_mmind.png"}], "type": "app", From 06a58ae1496e052b0fd245ac8780cc3663853acc Mon Sep 17 00:00:00 2001 From: chiefdaft Date: Tue, 22 Mar 2022 00:35:51 +0100 Subject: [PATCH 25/73] Added a carret to the newly randomly added tiles --- apps/game1024/app.js | 47 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/apps/game1024/app.js b/apps/game1024/app.js index 3b96b327f..337350e30 100644 --- a/apps/game1024/app.js +++ b/apps/game1024/app.js @@ -1,4 +1,4 @@ -const debugMode = 'off'; // valid values are: off, test, production, development +const debugMode = 'development'; // valid values are: off, test, production, development const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)}; const rows = 4, cols = 4; const borderWidth = 6; @@ -17,7 +17,7 @@ const cellChars = [ const maxUndoLevels = 4; const noExceptions = true; let charIndex = 0; // plain numbers on the grid - +const themeBg = g.theme.bg; const scores = { @@ -304,13 +304,29 @@ class Cell { this.previousExpVals=[]; this.idx = idx; this.cb = cb; + this.isRndm = false; + this.ax = x0; + this.ay = Math.floor(0.2*width+y0); + this.bx = Math.floor(0.3*width+x0); + this.by = Math.floor(0.5*width+y0); + this.cx = x0; + this.cy = Math.floor(0.8*width+y0); } getColor(i) { return cellColors[i >= cellColors.length ? cellColors.length -1 : i]; } drawBg() { - g.setColor(this.getColor(this.expVal).bg) - .fillRect(this.x0, this.y0, this.x1, this.y1); + debug(()=>console.log("Drawbg!!")); + if (this.isRndm == true) { + debug(()=>console.log('Random: (ax)', this.ax)); + g.setColor(this.getColor(this.expVal).bg) + .fillRect(this.x0, this.y0, this.x1, this.y1) + .setColor(themeBg) + .fillPoly([this.cx,this.cy,this.bx,this.by,this.ax,this.ay]); + } else { + g.setColor(this.getColor(this.expVal).bg) + .fillRect(this.x0, this.y0, this.x1, this.y1); + } } drawNumber() { if (this.expVal !== 0) { @@ -346,6 +362,19 @@ class Cell { this.cb(this.expVal); } } + setRndmFalse() { + this.isRndm = false; + } + setRndmTrue() { + this.isRndm = true; + } + drawRndmIndicator(){ + if (this.isRndm == true) { + debug(()=>console.log('Random: (ax)', this.ax)); + g.setColor(this.getColor(0).bg) + .fillPoly(this.ax,this.ay,this.bx,this.by,this.cx,this.cy); + } + } } function undoGame() { @@ -387,11 +416,12 @@ function createGrid () { } } function messageGameOver () { - g.setColor("#1a0d00") + const c = (g.theme.dark) ? {"fg": "#FFFFFF", "bg": "#808080"} : {"fg": "#FF0000", "bg": "#000000"}; + g.setColor(c.bg) .setFont12x20(2).setFontAlign(0,0,0) .drawString("G A M E", middle.x+13, middle.y-24) .drawString("O V E R !", middle.x+13, middle.y+24); - g.setColor("#ffffff") + g.setColor(c.fg) .drawString("G A M E", middle.x+12, middle.y-25) .drawString("O V E R !", middle.x+12, middle.y+25); } @@ -417,11 +447,13 @@ function addRandomNumber() { if (emptySquaresIdxs.length > 0) { let randomIdx = Math.floor( emptySquaresIdxs.length * Math.random() ); allSquares[emptySquaresIdxs[randomIdx]].setExpVal(makeRandomNumber()); + allSquares[emptySquaresIdxs[randomIdx]].setRndmTrue(); } } function drawGrid() { allSquares.forEach(sq => { sq.drawBg(); + // sq.drawRndmIndicator(); sq.drawNumber(); }); } @@ -497,7 +529,7 @@ function handlePopUpClicks(btn) { function resetGame() { g.clear(); scores.reset(); - allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();}); + allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();sq.setRndmFalse();}); addRandomNumber(); addRandomNumber(); drawGrid(); @@ -614,6 +646,7 @@ function runGame(dir){ mover.nonEmptyCells(dir); mover.mergeEqlCells(dir); mover.nonEmptyCells(dir); + allSquares.forEach(sq => {sq.setRndmFalse();}); addRandomNumber(); drawGrid(); scores.check(); From f2faa4e0de42093913d1560caffd8206c4bc59b2 Mon Sep 17 00:00:00 2001 From: chiefdaft Date: Tue, 22 Mar 2022 01:01:14 +0100 Subject: [PATCH 26/73] Updated the description for the chevron add-on, and added new images --- apps/game1024/ChangeLog | 3 ++- apps/game1024/README.md | 2 ++ apps/game1024/app.js | 2 +- apps/game1024/game1024_sc_dump_dark.png | Bin 4146 -> 4404 bytes apps/game1024/game1024_sc_dump_light.png | Bin 3806 -> 4054 bytes apps/game1024/metadata.json | 2 +- apps/game1024/screenshot.png | Bin 5313 -> 6263 bytes apps/game1024/scrnshot_dn_300.jpg | Bin 56911 -> 0 bytes apps/game1024/scrnshot_lc_300.jpg | Bin 55178 -> 0 bytes 9 files changed, 6 insertions(+), 3 deletions(-) delete mode 100644 apps/game1024/scrnshot_dn_300.jpg delete mode 100644 apps/game1024/scrnshot_lc_300.jpg diff --git a/apps/game1024/ChangeLog b/apps/game1024/ChangeLog index af0925b0f..ffb1f94bc 100644 --- a/apps/game1024/ChangeLog +++ b/apps/game1024/ChangeLog @@ -1,4 +1,5 @@ 0.01: Initial version 0.02: Temporary intermediate version 0.03: Basic colors -0.04: Bug fix score reset after Game Over, new icon \ No newline at end of file +0.04: Bug fix score reset after Game Over, new icon +0.05: Chevron marker on the randomly added square \ No newline at end of file diff --git a/apps/game1024/README.md b/apps/game1024/README.md index 474e8c97e..500453145 100644 --- a/apps/game1024/README.md +++ b/apps/game1024/README.md @@ -11,6 +11,8 @@ When two tiles with the same number are squashed together they will add up as ex **3 + 3 = 4** or **C + C = D** which is a representation of **2^3 + 2^3 = 2^4 = 16** +After each move a new tile will be added on a random empty square. The value can be 1 or 2, and will be marked with a chevron. + So you can continue till you reach **1024** which equals **2^(10)**. So when you reach tile **10** you have won. The score is maintained by adding the outcome of the sum of all pairs of squashed tiles (4+16+4+8 etc.) diff --git a/apps/game1024/app.js b/apps/game1024/app.js index 337350e30..9f6081376 100644 --- a/apps/game1024/app.js +++ b/apps/game1024/app.js @@ -1,4 +1,4 @@ -const debugMode = 'development'; // valid values are: off, test, production, development +const debugMode = 'off'; // valid values are: off, test, production, development const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)}; const rows = 4, cols = 4; const borderWidth = 6; diff --git a/apps/game1024/game1024_sc_dump_dark.png b/apps/game1024/game1024_sc_dump_dark.png index 15ecb87d9e7e0180b7443b717e0aeb5409563fa0..87577ecfa39c5f663d4c883425a8d4625168631d 100644 GIT binary patch delta 4397 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 delta 4137 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 CG8;Gm diff --git a/apps/game1024/scrnshot_dn_300.jpg b/apps/game1024/scrnshot_dn_300.jpg deleted file mode 100644 index 2f4cc4bdae9e3b315db280c378f2ca4dd10d5e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56911 zcmeFZcUTn7@-RAxh-46u%pySqBxeLxM9Db_5*A?zl5-BLf}ltc5m3^K2uNIzoO2M6 z468)RNsuMEz_Q%I_kGVf&;5Si_uPB`y{BQAn(pfAuA1(y>KeMvzMcI5s5Mm7RR97o z-6XgP0B7^J^zH>X+5>>PIzMn30Kn@C0Uv4z)iHZn` z$$(Tu#Kh!8#O1_9IK;%|L`1<~;35FLB0m2MfQ2Fi@BiJ$3-AA;%>fn?{tZLU5y3

ta@h|)hAIX9R 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%0A@s4$i z@1?9`6yb6G6Y;(zasT39TyRgvSwVmXUk2M)%^Bg128s7GCg^qyphZR~OmDFDu-qmH z%9k(Znt%>ywSeWSfciEGQffC0A7`CJ6LFRqz*=yo4nLI+`dPvLn_OjbQkakAnt3{o}k=)WT`-Gu|!iFY%924w-#9egd)B=6~ z|01IdFJ5U;M|Hrz`}e78TJ~QDR4!~Lz9Vk9t?G%Mp5nCy5Opnuh7(Tsbz6i{e^<{n z%Y$Fjq0pg0{+93|bh4JDEzE}H!s+su(A{<|kF^#t$5nR$f$#RX7J@s(8zfR*HTf6h z??JV^aCve8?zc)weSHODIUj-wpQ&#SgNSd+f)K-b8@3_8(2lyN1Ms{!RWybV%O-da zMk@)*3e9}{jSe!{D`L*n4j7t)(cF{lL(^LAGtqx5;kX!}#!Z8bmY9wQ7@h%Uj9zq- zhuKdJ`d>7UC;y)Sp9Nt0TygE*N9ssY^Pc>NSdB>{VY+&E-T^#;TlGq7-2CI(RqVkG zN6UEmq=)Bi_&vBX3^iqCK6};_OrVIREcM?;ER_}vqyGS}IYR9w&rXqVADosFkP5Y9 z{v2cg9W+zf{#C?h%DEG*oOs1cs1`Mv_fI)_2r$K=^^>=phue*Gb;d+ z**&I})x zP!dG7F1r3+Fl;v64rIJb^>v>G*70Bp-6Vlmr~YQ1Kyo0PAkyr^(XFWw;5Gq*Ui@_=u>=fi-`eqb#-1y{2<=b^n^+z<>1q$MtvL1}IpST0RV5NzX< zE&vspn=RZ$5rSE%^SmFxr~kOS7fF=VS-XRYn6a57*kkSsSi zYri-oPhq&c=U)bHOa$2VzAvNBSRqIWqhagcoO+OeVB6^U>CP)J-eKfh)A?jB3#}|a z0p2fatr%@}*z|RbYOMfoTAcBl0(St{EOZYT23pjKS5Ex##9&Yc3#*Q?M~p>c2Zeb% zOObU0nKu9^3e9VrAcHzc|vyoXI?Da2oF-PDt$Pvq0^xGV?14P-EY^t*(|~T(!hfas=K-opLJsGz<&PcxbtOv>g~e?fp2U5u-O0`1 zK)DNi>lC)})`;4@Vi*EMdj#3-=*rdaF%Cmi<=$x_3U0uJ*9RV>ITV(geBI?#0YF4{ z``h{Vh;7F}y;t$Wiqo*&_4zPps!}3&Hrt#cC0&<-UzZ=8=AzD5oP7m&hBof5n@+#S z&CYO&@-HI&~HNMX9w2fO`gt4m8R72%Ah=MJbFFjmmtRxqWI=K^NA(p zI4iyE4E4>9xN{+sxQ37Nj(dKp!;{vydCAM&ow;{9C^5*)=A8Y{okzFTc6s` ztG;ltvd?&9_i@LZl7!l_kGcN6FCaenZ853zt z`UfPgT{YHyNt;2`FQLQXgkr+Gc}Ej1L?%X`DT%rS9O*H^eMwUGx9x!kt`3N9d;b7> znLtp~M+h3=f9=N$Ng&(Xj(->>t-ObhIKe@5Y&<`_J`{H?^7+Gu36?l(o-#kAr>|c{ z)<8+zS53a|1O-+l(s&+>;X<=-C)n%8CMz^5CljLg=L(W+T--_5)^6a9Nh{0mpa0o5 CtLv}; From 286ec4cb806503eedbd53e7b3865b956728ac628 Mon Sep 17 00:00:00 2001 From: chiefdaft Date: Tue, 22 Mar 2022 01:15:51 +0100 Subject: [PATCH 27/73] Repaired the screenshot withe wrong caret. --- apps/game1024/screenshot.png | Bin 6263 -> 6079 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/game1024/screenshot.png b/apps/game1024/screenshot.png index 5537a5eb5c1f16ec1406b833b8fd587f724c2f23..8be52f8cb7a7f0650f6b454fc9a7fd41ff6a58d6 100644 GIT binary patch literal 6079 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 literal 6263 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 Date: Tue, 22 Mar 2022 09:48:45 +0000 Subject: [PATCH 28/73] lightswitch 0.03: Settings page now uses built-in min/max/wrap (fix #1607) --- apps/lightswitch/ChangeLog | 1 + apps/lightswitch/metadata.json | 2 +- apps/lightswitch/settings.js | 16 ++++++++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog index 7a7ecd027..4210ccf03 100644 --- a/apps/lightswitch/ChangeLog +++ b/apps/lightswitch/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Add the option to enable touching the widget only on clock and settings. +0.03: Settings page now uses built-in min/max/wrap (fix #1607) diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json index 902b1536b..9ac388eda 100644 --- a/apps/lightswitch/metadata.json +++ b/apps/lightswitch/metadata.json @@ -2,7 +2,7 @@ "id": "lightswitch", "name": "Light Switch Widget", "shortName": "Light Switch", - "version": "0.02", + "version": "0.03", "description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.", "icon": "images/app.png", "screenshots": [ diff --git a/apps/lightswitch/settings.js b/apps/lightswitch/settings.js index aac159148..bebb16d15 100644 --- a/apps/lightswitch/settings.js +++ b/apps/lightswitch/settings.js @@ -44,9 +44,11 @@ // return entry for string value return { value: entry.value.indexOf(settings[key]), + min : 0, + max : entry.value.length-1, + wrap : true, format: v => entry.title ? entry.title[v] : entry.value[v], onchange: function(v) { - this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v; writeSetting(key, entry.value[v], entry.drawWidgets); if (entry.exec) entry.exec(entry.value[v]); } @@ -57,8 +59,10 @@ value: settings[key] * entry.factor, step: entry.step, format: v => v > 0 ? v + entry.unit : "off", + min : entry.min, + max : entry.max, + wrap : true, onchange: function(v) { - this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v; writeSetting(key, v / entry.factor, entry.drawWidgets); }, }; @@ -133,16 +137,16 @@ title: "Light Switch" }, "< Back": () => back(), - "-- Widget --------": 0, + "-- Widget": 0, "Bulb col": getEntry("colors"), "Image": getEntry("image"), - "-- Control -------": 0, + "-- Control": 0, "Touch": getEntry("touchOn"), "Drag Delay": getEntry("dragDelay"), "Min Value": getEntry("minValue"), - "-- Unlock --------": 0, + "-- Unlock": 0, "TapSide": getEntry("unlockSide"), - "-- Flash ---------": 0, + "-- Flash": 0, "TapSide ": getEntry("tapSide"), "Tap": getEntry("tapOn"), "Timeout": getEntry("tOut"), From 9886125fbd796feabfea5beef484487c67324e63 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Tue, 22 Mar 2022 23:16:17 +0800 Subject: [PATCH 29/73] Update app.js Refactoring and extra range checks. --- apps/authentiwatch/app.js | 117 ++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 67 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 8e25e5ef2..b2608a5dc 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -2,6 +2,7 @@ const COUNTER_TRIANGLE_SIZE = 10; const TOKEN_EXTRA_HEIGHT = 16; var TOKEN_DIGITS_HEIGHT = 30; var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT; +const SETTINGS = "authentiwatch.json"; // Hash functions const crypto = require("crypto"); const algos = { @@ -15,7 +16,7 @@ const NOT_SUPPORTED = /*LANG*/"Not supported"; // sample settings: // {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} -var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}}; +var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[],misc:{}}; if (settings.data ) tokens = settings.data ; /* v0.02 settings */ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ @@ -134,14 +135,13 @@ half = n => Math.floor(n / 2); function timerCalc() { let timerfn = exitApp; let timerdly = 10000; - let id = state.id; - if (id != -1 && state.hotp.hotp != "") { - if (tokens[id].period > 0) { + if (state.id != -1 && state.hotp.hotp != "") { + if (tokens[state.id].period > 0) { // timed HOTP if (state.hotp.next < Date.now()) { if (state.cnt > 0) { - --state.cnt; - state.hotp = hotp(tokens[id]); + state.cnt--; + state.hotp = hotp(tokens[state.id]); } else { state.hotp.hotp = ""; } @@ -154,7 +154,7 @@ function timerCalc() { } else { // counter HOTP if (state.cnt > 0) { - --state.cnt; + state.cnt--; timerdly = 30000; } else { state.hotp.hotp = ""; @@ -215,7 +215,7 @@ function drawToken(id) { var y2 = y1 + TOKEN_HEIGHT - 1; var adj, lbl; g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2)); - lbl = tokens[id].label.substr(0, 10); + lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : ""; if (id === state.id) { g.setColor(g.theme.fgH) .setBgColor(g.theme.bgH); @@ -264,31 +264,24 @@ function drawToken(id) { g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1); } -function drawAll() { - if (tokens.length > 0) { - let id = 0; - let y = tokenY(id); - while (id < tokens.length && y < AR.y2) { - if ((y + TOKEN_HEIGHT) > AR.y) { - drawToken(id); - } - id++; - y += TOKEN_HEIGHT; +function changeId(id) { + if (id != state.id) { + state.hotp.hotp = CALCULATING; + let pid = state.id; + state.id = id; + if (pid != -1) { + drawToken(pid); + } + if (id != -1) { + drawToken( id); } - } else { - let x = AR.x + half(AR.w); - let y = AR.y + half(AR.h); - g.setFont("Vector", TOKEN_DIGITS_HEIGHT) - .setFontAlign(0, 0, 0) - .drawString(NO_TOKENS, x, y, false); } - timerCalc(); } function onDrag(e) { state.cnt = 1; if (e.b != 0 && e.dy != 0) { - var y = E.clip(state.listy - e.dy, 0, tokens.length * TOKEN_HEIGHT - AR.h); + var y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); if (state.listy != y) { var id, dy = state.listy - y; state.listy = y; @@ -319,21 +312,6 @@ function onDrag(e) { } } -function changeId(id) { - if (id != state.id) { - state.hotp.hotp = CALCULATING; - let pid = state.id; - state.id = id; - if (pid != -1) { - drawToken(pid); - } - if (id != -1) { - drawToken( id); - } - timerCalc(); - } -} - function onTouch(zone, e) { state.cnt = 1; if (e) { @@ -344,36 +322,39 @@ function onTouch(zone, e) { if (state.id != id) { if (id != -1) { // scroll token into view if necessary - var fakee = {b:1,x:0,y:0,dx:0,dy:0}; + var dy = 0; var y = id * TOKEN_HEIGHT - state.listy; if (y < 0) { - fakee.dy -= y; + dy -= y; y = 0; } y += TOKEN_HEIGHT; if (y > AR.h) { - fakee.dy -= (y - AR.h); + dy -= (y - AR.h); } - onDrag(fakee); + onDrag({b:1, dy:dy}); } changeId(id); } } + timerCalc(); } function onSwipe(e) { state.cnt = 1; - let id = state.id; - if (e == 1) { + switch (e) { + case 1: exitApp(); + break; + case -1: + if (state.id != -1 && tokens[state.id].period <= 0) { + tokens[state.id].period--; + require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc}); + state.hotp.hotp = CALCULATING; + drawToken(state.id); + } } - if (e == -1 && id != -1 && tokens[id].period <= 0) { - tokens[id].period--; - let newsettings={tokens:tokens,misc:settings.misc}; - require("Storage").writeJSON("authentiwatch.json", newsettings); - state.hotp.hotp = CALCULATING; - drawToken(id); - } + timerCalc(); } function bangle1Btn(e) { @@ -385,18 +366,11 @@ function bangle1Btn(e) { case 1: id++; break; } id = E.clip(id, 0, tokens.length - 1); - var fakee = {b:1,x:0,y:0,dx:0}; - fakee.dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, tokens.length * TOKEN_HEIGHT - AR.h); - //onDrag(fakee); - //changeId(id); - // onDrag() (specifically g.scroll()) doesn't appear to work with the Bangle1 - state.id = id; - state.hotp.hotp = CALCULATING; - state.listy -= fakee.dy; - drawAll(); - } else { - timerCalc(); + var dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); + onDrag({b:1, dy:dy}); + changeId(id); } + timerCalc(); } function exitApp() { @@ -415,6 +389,15 @@ if (typeof BTN2 == 'number') { } Bangle.loadWidgets(); const AR = Bangle.appRect; -g.clear(); // Clear the screen once, at startup -drawAll(); +// draw the initial display +g.clear(); +if (tokens.length > 0) { + state.listy = AR.h; + onDrag({b:1, dy:AR.h}); +} else { + g.setFont("Vector", TOKEN_DIGITS_HEIGHT) + .setFontAlign(0, 0, 0) + .drawString(NO_TOKENS, AR.x + half(AR.w), AR.y + half(AR.h), false); +} +timerCalc(); Bangle.drawWidgets(); From 9abc9296df0be1144faec8a6b49109b8d9ef9b04 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 22 Mar 2022 15:22:05 +0000 Subject: [PATCH 30/73] hrm 0.09: Grey out BPM until confidence is over 50% --- apps/hrm/ChangeLog | 1 + apps/hrm/heartrate.js | 5 ++--- apps/hrm/metadata.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog index e559adfb6..f05a9dc13 100644 --- a/apps/hrm/ChangeLog +++ b/apps/hrm/ChangeLog @@ -6,3 +6,4 @@ 0.06: Add widgets 0.07: Update scaling for new firmware 0.08: Don't force backlight on/watch unlocked on Bangle 2 +0.09: Grey out BPM until confidence is over 50% diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js index 305f0e1bc..703b60c01 100644 --- a/apps/hrm/heartrate.js +++ b/apps/hrm/heartrate.js @@ -35,9 +35,9 @@ function onHRM(h) { g.clearRect(0,24,g.getWidth(),80); g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75); var str = hrmInfo.bpm; - g.setFontVector(40).drawString(str,px,45); + g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45); px += g.stringWidth(str)/2; - g.setFont("6x8"); + g.setFont("6x8").setColor(g.theme.fg); g.drawString("BPM",px+15,45); } Bangle.on('HRM', onHRM); @@ -101,4 +101,3 @@ function readHRM() { lastHrmPt = [hrmOffset, y]; } } - diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json index 3e94c163c..10821d094 100644 --- a/apps/hrm/metadata.json +++ b/apps/hrm/metadata.json @@ -1,7 +1,7 @@ { "id": "hrm", "name": "Heart Rate Monitor", - "version": "0.08", + "version": "0.09", "description": "Measure your heart rate and see live sensor data", "icon": "heartrate.png", "tags": "health", From 1d7c090c21a10aad2dbc64ebb9c95de05d432bdb Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Tue, 22 Mar 2022 23:26:50 +0800 Subject: [PATCH 31/73] Update app.js Update progress bar on Bangle1 button presses. Clean up timer before exiting. --- apps/authentiwatch/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index b2608a5dc..58d712451 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -369,11 +369,15 @@ function bangle1Btn(e) { var dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); onDrag({b:1, dy:dy}); changeId(id); + drawProgressBar(); } timerCalc(); } function exitApp() { + if (state.drawtimer) { + clearTimeout(state.drawtimer); + } Bangle.showLauncher(); } From 94207739c08a368b7f76c38eb9f3879757a77f94 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 14:22:17 +0800 Subject: [PATCH 32/73] Update app.js Refactoring --- apps/authentiwatch/app.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 58d712451..aae570e13 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -16,7 +16,7 @@ const NOT_SUPPORTED = /*LANG*/"Not supported"; // sample settings: // {tokens:[{"algorithm":"SHA1","digits":6,"period":30,"issuer":"","account":"","secret":"Bbb","label":"Aaa"}],misc:{}} -var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[],misc:{}}; +var settings = require("Storage").readJSON(SETTINGS, true) || {tokens:[], misc:{}}; if (settings.data ) tokens = settings.data ; /* v0.02 settings */ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ @@ -106,9 +106,9 @@ function hotp(token) { } // Tokens are displayed in three states: -// 1. Unselected (state.id==-1) -// 2. Selected, inactive (no code) (state.id!=-1,state.hotp.hotp=="") -// 3. Selected, active (code showing) (state.id!=-1,state.hotp.hotp!="") +// 1. Unselected (state.id<0) +// 2. Selected, inactive (no code) (state.id>=0,state.hotp.hotp=="") +// 3. Selected, active (code showing) (state.id>=0,state.hotp.hotp!="") var fontszCache = {}; var state = { listy:0, // list scroll position @@ -135,7 +135,7 @@ half = n => Math.floor(n / 2); function timerCalc() { let timerfn = exitApp; let timerdly = 10000; - if (state.id != -1 && state.hotp.hotp != "") { + if (state.id >= 0 && state.hotp.hotp != "") { if (tokens[state.id].period > 0) { // timed HOTP if (state.hotp.next < Date.now()) { @@ -181,7 +181,7 @@ function updateProgressBar() { function drawProgressBar() { let id = state.id; - if (id != -1 && tokens[id].period > 0) { + if (id >= 0 && tokens[id].period > 0) { let rem = Math.floor((state.hotp.next - Date.now()) / 1000); if (rem >= 0) { let y1 = tokenY(id); @@ -269,10 +269,10 @@ function changeId(id) { state.hotp.hotp = CALCULATING; let pid = state.id; state.id = id; - if (pid != -1) { + if (pid >= 0) { drawToken(pid); } - if (id != -1) { + if (id >= 0) { drawToken( id); } } @@ -320,7 +320,7 @@ function onTouch(zone, e) { id = -1; } if (state.id != id) { - if (id != -1) { + if (id >= 0) { // scroll token into view if necessary var dy = 0; var y = id * TOKEN_HEIGHT - state.listy; @@ -347,7 +347,7 @@ function onSwipe(e) { exitApp(); break; case -1: - if (state.id != -1 && tokens[state.id].period <= 0) { + if (state.id >= 0 && tokens[state.id].period <= 0) { tokens[state.id].period--; require("Storage").writeJSON(SETTINGS, {tokens:tokens, misc:settings.misc}); state.hotp.hotp = CALCULATING; From e76535fece1bad8660c986c228f663f1faff0817 Mon Sep 17 00:00:00 2001 From: Brendan Hubble Date: Wed, 23 Mar 2022 18:44:01 +1100 Subject: [PATCH 33/73] touchtimer: ADD 5 second count down buzzer - This is a configurable option (Default is off) for the timer to buzz each second when there are 5 seconds left. --- apps/touchtimer/ChangeLog | 1 + apps/touchtimer/app.js | 8 ++++++++ apps/touchtimer/metadata.json | 2 +- apps/touchtimer/settings.js | 8 ++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/touchtimer/ChangeLog b/apps/touchtimer/ChangeLog index fbabe6cb7..0969a3da4 100644 --- a/apps/touchtimer/ChangeLog +++ b/apps/touchtimer/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial creation of the touch timer app 0.02: Add settings menu 0.03: Add ability to repeat last timer +0.04: Add 5 second count down buzzer diff --git a/apps/touchtimer/app.js b/apps/touchtimer/app.js index 61d3a08fd..c2f2fb5e9 100644 --- a/apps/touchtimer/app.js +++ b/apps/touchtimer/app.js @@ -126,6 +126,14 @@ var main = () => { timerIntervalId = setInterval(() => { timerCountDown.draw(); + // Buzz lightly when there are less then 5 seconds left + if (settings.countDownBuzz) { + var remainingSeconds = timerCountDown.getAdjustedTime().seconds; + if (remainingSeconds <= 5 && remainingSeconds > 0) { + Bangle.buzz(); + } + } + if (timerCountDown.isFinished()) { buttonStartPause.value = "FINISHED!"; buttonStartPause.draw(); diff --git a/apps/touchtimer/metadata.json b/apps/touchtimer/metadata.json index 50c5f03dd..0f2b9f491 100644 --- a/apps/touchtimer/metadata.json +++ b/apps/touchtimer/metadata.json @@ -2,7 +2,7 @@ "id": "touchtimer", "name": "Touch Timer", "shortName": "Touch Timer", - "version": "0.03", + "version": "0.04", "description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.", "icon": "app.png", "tags": "tools", diff --git a/apps/touchtimer/settings.js b/apps/touchtimer/settings.js index 885670f57..79424f250 100644 --- a/apps/touchtimer/settings.js +++ b/apps/touchtimer/settings.js @@ -31,6 +31,14 @@ writeSettings(settings); }, }, + "CountDown Buzz": { + value: !!settings.countDownBuzz, + format: value => value?"On":"Off", + onchange: (value) => { + settings.countDownBuzz = value; + writeSettings(settings); + }, + }, "Pause Between": { value: settings.pauseBetween, min: 1, From b26d3416092f8ad12fa790b012a3336a93e8a18a Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 16:13:57 +0800 Subject: [PATCH 34/73] Update app.js Refactoring --- apps/authentiwatch/app.js | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index aae570e13..5ea8dd476 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -182,7 +182,7 @@ function updateProgressBar() { function drawProgressBar() { let id = state.id; if (id >= 0 && tokens[id].period > 0) { - let rem = Math.floor((state.hotp.next - Date.now()) / 1000); + let rem = Math.min(tokens[id].period, Math.floor((state.hotp.next - Date.now()) / 1000)); if (rem >= 0) { let y1 = tokenY(id); let y2 = y1 + TOKEN_HEIGHT - 1; @@ -192,7 +192,6 @@ function drawProgressBar() { { // progress bar visible y2 = Math.min(y2, AR.y2); - rem = Math.min(rem, tokens[id].period); let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x; g.setColor(g.theme.fgH) .setBgColor(g.theme.bgH) @@ -209,13 +208,13 @@ function drawProgressBar() { // id = token ID number (0...) function drawToken(id) { - var x1 = AR.x; - var y1 = tokenY(id); - var x2 = AR.x2; - var y2 = y1 + TOKEN_HEIGHT - 1; - var adj, lbl; + let x1 = AR.x; + let y1 = tokenY(id); + let x2 = AR.x2; + let y2 = y1 + TOKEN_HEIGHT - 1; + let lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : ""; + let adj; g.setClipRect(x1, Math.max(y1, AR.y), x2, Math.min(y2, AR.y2)); - lbl = (id >= 0 && id < tokens.length) ? tokens[id].label.substr(0, 10) : ""; if (id === state.id) { g.setColor(g.theme.fgH) .setBgColor(g.theme.bgH); @@ -281,9 +280,9 @@ function changeId(id) { function onDrag(e) { state.cnt = 1; if (e.b != 0 && e.dy != 0) { - var y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); + let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); if (state.listy != y) { - var id, dy = state.listy - y; + let id, dy = state.listy - y; state.listy = y; g.setClipRect(AR.x, AR.y, AR.x2, AR.y2) .scroll(0, dy); @@ -315,15 +314,15 @@ function onDrag(e) { function onTouch(zone, e) { state.cnt = 1; if (e) { - var id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT); + let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT); if (id == state.id || tokens.length == 0 || id >= tokens.length) { id = -1; } if (state.id != id) { if (id >= 0) { // scroll token into view if necessary - var dy = 0; - var y = id * TOKEN_HEIGHT - state.listy; + let dy = 0; + let y = id * TOKEN_HEIGHT - state.listy; if (y < 0) { dy -= y; y = 0; @@ -357,17 +356,16 @@ function onSwipe(e) { timerCalc(); } -function bangle1Btn(e) { +function bangleBtn(e) { state.cnt = 1; if (tokens.length > 0) { - var id = state.id; + let id = state.id; switch (e) { case -1: id--; break; case 1: id++; break; } id = E.clip(id, 0, tokens.length - 1); - var dy = state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); - onDrag({b:1, dy:dy}); + onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))}); changeId(id); drawProgressBar(); } @@ -384,12 +382,14 @@ function exitApp() { Bangle.on('touch', onTouch); Bangle.on('drag' , onDrag ); Bangle.on('swipe', onSwipe); -if (typeof BTN2 == 'number') { - setWatch(function(){bangle1Btn(-1);}, BTN1, {edge:"rising" , debounce:50, repeat:true}); - setWatch(function(){exitApp(); }, BTN2, {edge:"falling", debounce:50}); - setWatch(function(){bangle1Btn( 1);}, BTN3, {edge:"rising" , debounce:50, repeat:true}); -} else { - setWatch(function(){exitApp(); }, BTN1, {edge:"falling", debounce:50}); +if (typeof BTN1 == 'number') { + if (typeof BTN2 == 'number' && typeof BTN3 == 'number') { + setWatch(()=>bangleBtn(-1), BTN1, {edge:"rising" , debounce:50, repeat:true}); + setWatch(()=>exitApp() , BTN2, {edge:"falling", debounce:50}); + setWatch(()=>bangleBtn( 1), BTN3, {edge:"rising" , debounce:50, repeat:true}); + } else { + setWatch(()=>exitApp() , BTN1, {edge:"falling", debounce:50}); + } } Bangle.loadWidgets(); const AR = Bangle.appRect; From bf30b35ac07fc3bde3299e0dd5dc6dc2886c9d82 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 16:18:21 +0800 Subject: [PATCH 35/73] Update app.js Fix progress bar drawing. --- apps/authentiwatch/app.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 5ea8dd476..69e4a1ae1 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -188,15 +188,16 @@ function drawProgressBar() { let y2 = y1 + TOKEN_HEIGHT - 1; if (y2 >= AR.y && y1 <= AR.y2) { // token visible - if ((y2 - 3) <= AR.y2) + y1 = y2 - 3; + if (y1 <= AR.y2) { // progress bar visible y2 = Math.min(y2, AR.y2); let xr = Math.floor(AR.w * rem / tokens[id].period) + AR.x; g.setColor(g.theme.fgH) .setBgColor(g.theme.bgH) - .fillRect(AR.x, y2 - 3, xr, y2) - .clearRect(xr + 1, y2 - 3, AR.x2, y2); + .fillRect(AR.x, y1, xr, y2) + .clearRect(xr + 1, y1, AR.x2, y2); } } else { // token not visible From 47332eba50f743ccfb7e85439cc41f5321bf910f Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 16:26:48 +0800 Subject: [PATCH 36/73] Update app.js Simplify. --- apps/authentiwatch/app.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 69e4a1ae1..3920ebddd 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -360,12 +360,7 @@ function onSwipe(e) { function bangleBtn(e) { state.cnt = 1; if (tokens.length > 0) { - let id = state.id; - switch (e) { - case -1: id--; break; - case 1: id++; break; - } - id = E.clip(id, 0, tokens.length - 1); + let id = E.clip(state.id + e, 0, tokens.length - 1); onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))}); changeId(id); drawProgressBar(); From 05b75d2241844d4858992b46335f1d141b680f1a Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 16:30:24 +0800 Subject: [PATCH 37/73] Update app.js Fix missing break. --- apps/authentiwatch/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 3920ebddd..3a452b6e9 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -353,6 +353,7 @@ function onSwipe(e) { state.hotp.hotp = CALCULATING; drawToken(state.id); } + break; } timerCalc(); } From b3cc125a3f733b8f8a9cd591d537f947ca2488b1 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 16:36:42 +0800 Subject: [PATCH 38/73] Update app.js Use let instead of var. --- apps/authentiwatch/app.js | 41 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 3a452b6e9..5198254af 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -22,8 +22,8 @@ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ function b32decode(seedstr) { // RFC4648 - var buf = 0, bitcount = 0, retstr = ""; - for (var c of seedstr.toUpperCase()) { + let buf = 0, bitcount = 0, retstr = ""; + for (let c of seedstr.toUpperCase()) { if (c == '0') c = 'O'; if (c == '1') c = 'I'; if (c == '8') c = 'B'; @@ -39,63 +39,62 @@ function b32decode(seedstr) { } } } - var retbuf = new Uint8Array(retstr.length); - for (var i in retstr) { + let retbuf = new Uint8Array(retstr.length); + for (let i in retstr) { retbuf[i] = retstr.charCodeAt(i); } return retbuf; } function hmac(key, message, algo) { - var a = algos[algo.toUpperCase()]; + let a = algos[algo.toUpperCase()]; // RFC2104 if (key.length > a.blksz) { key = a.sha(key); } - var istr = new Uint8Array(a.blksz + message.length); - var ostr = new Uint8Array(a.blksz + a.retsz); - for (var i = 0; i < a.blksz; ++i) { - var c = (i < key.length) ? key[i] : 0; + let istr = new Uint8Array(a.blksz + message.length); + let ostr = new Uint8Array(a.blksz + a.retsz); + for (let i = 0; i < a.blksz; ++i) { + let c = (i < key.length) ? key[i] : 0; istr[i] = c ^ 0x36; ostr[i] = c ^ 0x5C; } istr.set(message, a.blksz); ostr.set(a.sha(istr), a.blksz); - var ret = a.sha(ostr); + let ret = a.sha(ostr); // RFC4226 dynamic truncation - var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); + let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); return v.getUint32(0) & 0x7FFFFFFF; } function formatOtp(otp, digits) { // add 0 padding - var ret = "" + otp % Math.pow(10, digits); + let ret = "" + otp % Math.pow(10, digits); while (ret.length < digits) { ret = "0" + ret; } // add a space after every 3rd or 4th digit - var re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : "."; + let re = (digits % 3 == 0 || (digits % 3 >= digits % 4 && digits % 4 != 0)) ? "" : "."; return ret.replace(new RegExp("(..." + re + ")", "g"), "$1 ").trim(); } function hotp(token) { - var d = Date.now(); - var tick, next; + let d = Date.now(); + let tick, next; if (token.period > 0) { // RFC6238 - timed - var seconds = Math.floor(d / 1000); - tick = Math.floor(seconds / token.period); + tick = Math.floor(Math.floor(d / 1000) / token.period); next = (tick + 1) * token.period * 1000; } else { // RFC4226 - counter tick = -token.period; next = d + 30000; } - var msg = new Uint8Array(8); - var v = new DataView(msg.buffer); + let msg = new Uint8Array(8); + let v = new DataView(msg.buffer); v.setUint32(0, tick >> 16 >> 16); v.setUint32(4, tick & 0xFFFFFFFF); - var ret; + let ret; try { ret = hmac(b32decode(token.secret), msg, token.algorithm); ret = formatOtp(ret, token.digits); @@ -117,7 +116,7 @@ var state = { }; function sizeFont(id, txt, w) { - var sz = fontszCache[id]; + let sz = fontszCache[id]; if (sz) { g.setFont("Vector", sz); } else { From 6158d76f176315b014498e997a93a6ad1a267b30 Mon Sep 17 00:00:00 2001 From: Micha <97034053+foostuff@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:37:44 +0100 Subject: [PATCH 39/73] configurable drag gestures --- apps/clockcal/app.js | 172 ++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 77 deletions(-) diff --git a/apps/clockcal/app.js b/apps/clockcal/app.js index 86fa0815a..5e8c7f796 100644 --- a/apps/clockcal/app.js +++ b/apps/clockcal/app.js @@ -4,12 +4,13 @@ var s = Object.assign({ CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually MODE24: true, //24h mode vs 12h mode - FIRSTDAYOFFSET: 6, //First day of the week: 0-6: Sun, Sat, Fri, Thu, Wed, Tue, Mon + 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 + DRAGDOWN: "[AI:messg]", + DRAGRIGHT: "[AI:music]", + DRAGLEFT: "[ignore]", + DRAGUP: "[calend.]" }, require('Storage').readJSON("clockcal.json", true) || {}); const h = g.getHeight(); @@ -27,13 +28,13 @@ var monthOffset = 0; */ function drawFullCalendar(monthOffset) { addMonths = function (_d, _am) { - var ay = 0, m = _d.getMonth(), y = _d.getFullYear(); - while ((m + _am) > 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; + var ay = 0, m = _d.getMonth(), y = _d.getFullYear(); + while ((m + _am) > 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"; @@ -45,22 +46,22 @@ function drawFullCalendar(monthOffset) { if (typeof minuteInterval !== "undefined") clearTimeout(minuteInterval); d = addMonths(Date(), monthOffset); tdy = Date().getDate() + "." + Date().getMonth(); - newmonth=false; + newmonth = false; c_y = 0; g.reset(); g.setBgColor(0); g.clear(); - var prevmonth = addMonths(d, -1) + 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; + const dow = (s.FIRSTDAY + 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]; + 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; @@ -69,14 +70,14 @@ function drawFullCalendar(monthOffset) { rMonth = rD.getMonth(); rDate = rD.getDate(); if (tdy == rDate + "." + rMonth) { - caldrawToday(rDate); + caldrawToday(rDate); } else if (rDate == 1) { - caldrawFirst(rDate); + caldrawFirst(rDate); } else { - caldrawNormal(rDate,fg[rD.getDay()]); + caldrawNormal(rDate, fg[rD.getDay()]); } if (newmonth && x == 7) { - caldrawMonth(rDate,monthclr[rMonth % 6],months[rMonth],rD); + caldrawMonth(rDate, monthclr[rMonth % 6], months[rMonth], rD); } rD.setDate(rDate + 1); } @@ -84,7 +85,7 @@ function drawFullCalendar(monthOffset) { delete addMonths; if (DEBUG) console.log("Calendar performance (ms):" + (Date().getTime() - start)); } -function caldrawMonth(rDate,c,m,rD) { +function caldrawMonth(rDate, c, m, rD) { g.setColor(c); g.setFont("Vector", 18); g.setFontAlign(-1, 1, 1); @@ -93,29 +94,29 @@ function caldrawMonth(rDate,c,m,rD) { 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); + 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); + 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 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"); @@ -163,7 +164,7 @@ function drawWatch() { g.clear(); drawMinutes(); if (!dimSeconds) drawSeconds(); - const dow = (s.FIRSTDAYOFFSET + d.getDay()) % 7; //MO=0, SU=6 + const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6 const today = d.getDate(); var rD = new Date(d.getTime()); rD.setDate(rD.getDate() - dow); @@ -205,27 +206,52 @@ function BTevent() { setTimeout(function () { Bangle.buzz(interval); }, interval * 3); } } - +function action(a) { + g.reset(); + if (typeof secondInterval !== "undefined") clearTimeout(secondInterval); + if (DEBUG) console.log("action:" + a); + switch (a) { + case "[ignore]": + break; + case "[calend.]": + drawFullCalendar(); + break; + case "[AI:music]": + l = require("Storage").list(RegExp("music.*app.js")); + if (l.length > 0) { + load(l[0]); + } else E.showAlert("Music app not found", "Not found").then(drawWatch); + break; + case "[AI:messg]": + l = require("Storage").list(RegExp("message.*app.js")); + if (l.length > 0) { + load(l[0]); + } else E.showAlert("Message app not found", "Not found").then(drawWatch); + break; + default: + l = require("Storage").list(RegExp(a + ".app.js")); + if (l.length > 0) { + load(l[0]); + } else E.showAlert(a + ": App not found", "Not found").then(drawWatch); + break; + } +} function input(dir) { - if (s.DRAGENABLED) { - Bangle.buzz(100,1); - console.log("swipe:"+dir); + Bangle.buzz(100, 1); + if (DEBUG) 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 - } + action(s.DRAGRIGHT); } break; case "l": if (state == "calendar") { drawWatch(); + } else { + action(s.DRAGLEFT); } break; case "d": @@ -233,21 +259,15 @@ function input(dir) { 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 - } + action(s.DRAGDOWN); } break; case "u": - if (state == "watch") { - state = "calendar"; - drawFullCalendar(0); - } else if (state == "calendar") { + if (state == "calendar") { monthOffset++; drawFullCalendar(monthOffset); + } else { + action(s.DRAGUP); } break; default: @@ -255,26 +275,24 @@ function input(dir) { 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); + 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) + 20) { + dir = (dx > 0) ? "r" : "l"; + } else if (Math.abs(dy) > Math.abs(dx) + 20) { + dir = (dy > 0) ? "d" : "u"; } + drag = null; + input(dir); } }); From d0426a2093caf025b0fdf3b4ae930bca83b9ee75 Mon Sep 17 00:00:00 2001 From: Micha <97034053+foostuff@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:38:42 +0100 Subject: [PATCH 40/73] Configurable drag gestures --- apps/clockcal/settings.js | 65 +++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index c4ec764c9..abedad99b 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -1,19 +1,22 @@ (function (back) { var FILE = "clockcal.json"; - - settings = Object.assign({ + defaults={ CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually MODE24: true, //24h mode vs 12h mode 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) || {}); - + DRAGDOWN: "[AI:messg]", + DRAGRIGHT: "[AI:music]", + DRAGLEFT: "[ignore]", + DRAGUP: "[calend.]" + }; + settings = Object.assign(defaults, require('Storage').readJSON(FILE, true) || {}); + actions = ["[ignore]","[calend.]","[AI:music]","[AI:messg]"]; + require("Storage").list(RegExp(".app.js")).forEach(element => actions.push(element.replace(".app.js",""))); + function writeSettings() { require('Storage').writeJSON(FILE, settings); } @@ -70,27 +73,39 @@ writeSettings(); } }, - 'Swipes (big cal.)?': { - value: settings.DRAGENABLED, - format: v => v ? "On" : "Off", + 'Drag Up ': { + min:0, max:actions.length-1, + value: actions.indexOf(settings.DRAGUP), + format: v => actions[v], onchange: v => { - settings.DRAGENABLED = v; + settings.DRAGUP = actions[v]; writeSettings(); } }, - 'Swipes (music)?': { - value: settings.DRAGMUSIC, - format: v => v ? "On" : "Off", + 'Drag Right': { + min:0, max:actions.length-1, + value: actions.indexOf(settings.DRAGRIGHT), + format: v => actions[v], onchange: v => { - settings.DRAGMUSIC = v; + settings.DRAGRIGHT = actions[v]; writeSettings(); } }, - 'Swipes (messg)?': { - value: settings.DRAGMESSAGES, - format: v => v ? "On" : "Off", + 'Drag Down': { + min:0, max:actions.length-1, + value: actions.indexOf(settings.DRAGDOWN), + format: v => actions[v], onchange: v => { - settings.DRAGMESSAGES = v; + settings.DRGDOWN = actions[v]; + writeSettings(); + } + }, + 'Drag Left': { + min:0, max:actions.length-1, + value: actions.indexOf(settings.DRAGLEFT), + format: v => actions[v], + onchange: v => { + settings.DRAGLEFT = actions[v]; writeSettings(); } }, @@ -100,17 +115,7 @@ format: v => ["No", "Yes"][v], onchange: v => { if (v == 1) { - settings = { - CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. - BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. - MODE24: true, //24h mode vs 12h mode - 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 - }; + settings = defaults; writeSettings(); load(); } From abf684449780be69a38ad881f9f7c61ed9894a42 Mon Sep 17 00:00:00 2001 From: Micha <97034053+foostuff@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:40:31 +0100 Subject: [PATCH 41/73] increment version --- apps/clockcal/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index a42e1ad2e..dde32f746 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.2", + "version": "0.3", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", From 00d74ceef61fb3596b8468db3219b7e6598962d5 Mon Sep 17 00:00:00 2001 From: Micha <97034053+foostuff@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:46:03 +0100 Subject: [PATCH 42/73] Update ChangeLog --- apps/clockcal/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 299b1ec69..f73c6ed97 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial upload 0.2: Added scrollable calendar and swipe gestures +0.3: Configurable drag gestures From d6c8f634c7689662dea624ab07f91ee2469d1bd3 Mon Sep 17 00:00:00 2001 From: Micha <97034053+foostuff@users.noreply.github.com> Date: Wed, 23 Mar 2022 14:56:17 +0100 Subject: [PATCH 43/73] Update README.md --- apps/clockcal/README.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/clockcal/README.md b/apps/clockcal/README.md index da6887177..d30205be0 100644 --- a/apps/clockcal/README.md +++ b/apps/clockcal/README.md @@ -9,25 +9,24 @@ I know that it seems redundant because there already **is** a *time&cal*-app, bu |![unlocked screen](screenshot2.png)|unlocked: smaller clock, but with seconds| |![big calendar](screenshot3.png)|swipe up for big calendar, (up down to scroll, left/right to exit)| - - - ## Configurable Features - Number of calendar rows (weeks) - Buzz on connect/disconnect (I know, this should be an extra widget, but for now, it is included) -- Clock Mode (24h/12h). Doesn't have an am/pm indicator. It's only there because it was easy. +- Clock Mode (24h/12h). (No am/pm indicator) - First day of the week -- Red Saturday -- Red Sunday -- Swipes (to disable all gestures) -- Swipes: music (swipe down) -- Spipes: messages (swipe right) +- Red Saturday/Sunday +- Swipe/Drag gestures to launch features or apps. ## Auto detects your message/music apps: -- swiping down will search your files for an app with the string "music" in its filename and launch it -- swiping right will search your files for an app with the string "message" in its filename and launch it. -- Configurable apps coming soon. +- swiping down will search your files for an app with the string "message" in its filename and launch it. (configurable) +- swiping right will search your files for an app with the string "music" in its filename and launch it. (configurable) ## Feedback The clock works for me in a 24h/MondayFirst/WeekendFree environment but is not well-tested with other settings. So if something isn't working, please tell me: https://github.com/foostuff/BangleApps/issues + +## Planned features: + - Internal lightweight music control, because switching apps has a loading time. + - Clean up settings + - Maybe am/pm indicator for 12h-users + - Step count (optional) From 00ad2a12780da264d897ddd1c39d06074211cb97 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Wed, 23 Mar 2022 22:24:14 +0800 Subject: [PATCH 44/73] Update app.js Define some constants. Add RFC comments. --- apps/authentiwatch/app.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 5198254af..6e2951b9f 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -2,6 +2,8 @@ const COUNTER_TRIANGLE_SIZE = 10; const TOKEN_EXTRA_HEIGHT = 16; var TOKEN_DIGITS_HEIGHT = 30; var TOKEN_HEIGHT = TOKEN_DIGITS_HEIGHT + TOKEN_EXTRA_HEIGHT; +const PROGRESSBAR_HEIGHT = 3; +const IDLE_REPEATS = 1; // when idle, the number of extra timed periods to show before hiding const SETTINGS = "authentiwatch.json"; // Hash functions const crypto = require("crypto"); @@ -21,7 +23,7 @@ if (settings.data ) tokens = settings.data ; /* v0.02 settings */ if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ function b32decode(seedstr) { - // RFC4648 + // RFC4648 Base16/32/64 Data Encodings let buf = 0, bitcount = 0, retstr = ""; for (let c of seedstr.toUpperCase()) { if (c == '0') c = 'O'; @@ -48,7 +50,7 @@ function b32decode(seedstr) { function hmac(key, message, algo) { let a = algos[algo.toUpperCase()]; - // RFC2104 + // RFC2104 HMAC if (key.length > a.blksz) { key = a.sha(key); } @@ -62,7 +64,7 @@ function hmac(key, message, algo) { istr.set(message, a.blksz); ostr.set(a.sha(istr), a.blksz); let ret = a.sha(ostr); - // RFC4226 dynamic truncation + // RFC4226 HOTP (dynamic truncation) let v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); return v.getUint32(0) & 0x7FFFFFFF; } @@ -187,7 +189,7 @@ function drawProgressBar() { let y2 = y1 + TOKEN_HEIGHT - 1; if (y2 >= AR.y && y1 <= AR.y2) { // token visible - y1 = y2 - 3; + y1 = y2 - PROGRESSBAR_HEIGHT; if (y1 <= AR.y2) { // progress bar visible @@ -278,7 +280,7 @@ function changeId(id) { } function onDrag(e) { - state.cnt = 1; + state.cnt = IDLE_REPEATS; if (e.b != 0 && e.dy != 0) { let y = E.clip(state.listy - E.clip(e.dy, -AR.h, AR.h), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h)); if (state.listy != y) { @@ -312,7 +314,7 @@ function onDrag(e) { } function onTouch(zone, e) { - state.cnt = 1; + state.cnt = IDLE_REPEATS; if (e) { let id = Math.floor((state.listy + e.y - AR.y) / TOKEN_HEIGHT); if (id == state.id || tokens.length == 0 || id >= tokens.length) { @@ -340,7 +342,7 @@ function onTouch(zone, e) { } function onSwipe(e) { - state.cnt = 1; + state.cnt = IDLE_REPEATS; switch (e) { case 1: exitApp(); @@ -358,7 +360,7 @@ function onSwipe(e) { } function bangleBtn(e) { - state.cnt = 1; + state.cnt = IDLE_REPEATS; if (tokens.length > 0) { let id = E.clip(state.id + e, 0, tokens.length - 1); onDrag({b:1, dy:state.listy - E.clip(id * TOKEN_HEIGHT - half(AR.h - TOKEN_HEIGHT), 0, Math.max(0, tokens.length * TOKEN_HEIGHT - AR.h))}); From 287cbf6eea1376f02e1d5472a1c9b9956c953205 Mon Sep 17 00:00:00 2001 From: marko Date: Wed, 23 Mar 2022 14:56:07 -0400 Subject: [PATCH 45/73] New app 2047++ --- apps/2047++/2047++.app.js | 129 ++++++++++++++++++++++++++++++++++++++ apps/2047++/README.md | 6 ++ apps/2047++/app-icon.js | 1 + apps/2047++/app.png | Bin 0 -> 759 bytes apps/2047++/metadata.json | 14 +++++ 5 files changed, 150 insertions(+) create mode 100644 apps/2047++/2047++.app.js create mode 100644 apps/2047++/README.md create mode 100644 apps/2047++/app-icon.js create mode 100644 apps/2047++/app.png create mode 100644 apps/2047++/metadata.json diff --git a/apps/2047++/2047++.app.js b/apps/2047++/2047++.app.js new file mode 100644 index 000000000..b508d8f07 --- /dev/null +++ b/apps/2047++/2047++.app.js @@ -0,0 +1,129 @@ + + +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) { + 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; moved = true; + } + 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; moved = true; + } + } + } + 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 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; + } +} + +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(); + } +} + +var twok = new TwoK(); +twok.addDigit(); twok.addDigit(); +twok.render(); +Bangle.on("drag", dragHandler); diff --git a/apps/2047++/README.md b/apps/2047++/README.md new file mode 100644 index 000000000..5b70423e4 --- /dev/null +++ b/apps/2047++/README.md @@ -0,0 +1,6 @@ + +# Game of 2047++ + +Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024. + +Attempt to combine equal numbers by swiping left, right, up or down. diff --git a/apps/2047++/app-icon.js b/apps/2047++/app-icon.js new file mode 100644 index 000000000..4086d1879 --- /dev/null +++ b/apps/2047++/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI=")) \ No newline at end of file diff --git a/apps/2047++/app.png b/apps/2047++/app.png new file mode 100644 index 0000000000000000000000000000000000000000..d1fb4a5e5cbadce3da7b36806d4907f680f8f1bb GIT binary patch literal 759 zcmVYCuD-J|F04S(qype>Ll^oVPK(#&(MEqX}dr+-4N41U&D6G<#pk8p=ry~PL z8P~wDv0Fxk@G(D*-Esf`5DS+uaYFpJB+Wpns^Zb}E7&tzFF7%tm102J6nd69|4+P3 zr1VfMt5{y0fI>`0^KD2mu=F8{y6M673*Til--d7lyX64hte$~F4EL^Xh;F_M;RY5n zQPi8Q(T|*z{|6UpV5c0w+w9;*9}v8ZE@h%j4t73gCg!Qfe`}$Ac#sB~}C%=m9 zQmlcEAIAXzTR($HE?;WPt>nU3$%Ta*5c>_tUp2cB`Ud77yzh$Lczg#y>rX6t^Z|D> zcQA?REP&Q#P6pBq$e1?!8Tl#X8W=W?3|OSe*3omHjttb47#RG02|5f6e$ zObTJ!ml%i%20ylab9U#WUDz$7W@n({nZhq+5~`IO*5Pi07qm053E*;P(4-Kmo@>1; z>;uNw7hc?M3Z*1!=?Nlw%8PRi7>4l#z$>YW4#!KwFcx?T+e^N5I_>#$tusSJ+zSuc p5YZ-MEM*wRg54#bi;K&M^BdjW!v$Q*XjK3J002ovPDHLkV1lLySIGbX literal 0 HcmV?d00001 diff --git a/apps/2047++/metadata.json b/apps/2047++/metadata.json new file mode 100644 index 000000000..f887d95d9 --- /dev/null +++ b/apps/2047++/metadata.json @@ -0,0 +1,14 @@ +{ "id": "2047++", + "name": "2047++", + "shortName":"2047++", + "icon": "app.png", + "version":"0.01", + "description": "Bangle version of a tile shifting game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "tags": "game", + "storage": [ + {"name":"2047++.app.js","url":"2047++.app.js"}, + {"name":"2047++.img","url":"app-icon.js","evaluate":true} + ] +} From 1e10f4a25eb1c7a48513694b71fa1b0aee623217 Mon Sep 17 00:00:00 2001 From: marko Date: Wed, 23 Mar 2022 14:59:23 -0400 Subject: [PATCH 46/73] Change name of new app --- apps/2047++/2047++.app.js | 129 -------------------------------------- apps/2047++/README.md | 6 -- apps/2047++/app-icon.js | 1 - apps/2047++/app.png | Bin 759 -> 0 bytes apps/2047++/metadata.json | 14 ----- 5 files changed, 150 deletions(-) delete mode 100644 apps/2047++/2047++.app.js delete mode 100644 apps/2047++/README.md delete mode 100644 apps/2047++/app-icon.js delete mode 100644 apps/2047++/app.png delete mode 100644 apps/2047++/metadata.json diff --git a/apps/2047++/2047++.app.js b/apps/2047++/2047++.app.js deleted file mode 100644 index b508d8f07..000000000 --- a/apps/2047++/2047++.app.js +++ /dev/null @@ -1,129 +0,0 @@ - - -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) { - 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; moved = true; - } - 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; moved = true; - } - } - } - 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 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; - } -} - -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(); - } -} - -var twok = new TwoK(); -twok.addDigit(); twok.addDigit(); -twok.render(); -Bangle.on("drag", dragHandler); diff --git a/apps/2047++/README.md b/apps/2047++/README.md deleted file mode 100644 index 5b70423e4..000000000 --- a/apps/2047++/README.md +++ /dev/null @@ -1,6 +0,0 @@ - -# Game of 2047++ - -Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024. - -Attempt to combine equal numbers by swiping left, right, up or down. diff --git a/apps/2047++/app-icon.js b/apps/2047++/app-icon.js deleted file mode 100644 index 4086d1879..000000000 --- a/apps/2047++/app-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI=")) \ No newline at end of file diff --git a/apps/2047++/app.png b/apps/2047++/app.png deleted file mode 100644 index d1fb4a5e5cbadce3da7b36806d4907f680f8f1bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 759 zcmVYCuD-J|F04S(qype>Ll^oVPK(#&(MEqX}dr+-4N41U&D6G<#pk8p=ry~PL z8P~wDv0Fxk@G(D*-Esf`5DS+uaYFpJB+Wpns^Zb}E7&tzFF7%tm102J6nd69|4+P3 zr1VfMt5{y0fI>`0^KD2mu=F8{y6M673*Til--d7lyX64hte$~F4EL^Xh;F_M;RY5n zQPi8Q(T|*z{|6UpV5c0w+w9;*9}v8ZE@h%j4t73gCg!Qfe`}$Ac#sB~}C%=m9 zQmlcEAIAXzTR($HE?;WPt>nU3$%Ta*5c>_tUp2cB`Ud77yzh$Lczg#y>rX6t^Z|D> zcQA?REP&Q#P6pBq$e1?!8Tl#X8W=W?3|OSe*3omHjttb47#RG02|5f6e$ zObTJ!ml%i%20ylab9U#WUDz$7W@n({nZhq+5~`IO*5Pi07qm053E*;P(4-Kmo@>1; z>;uNw7hc?M3Z*1!=?Nlw%8PRi7>4l#z$>YW4#!KwFcx?T+e^N5I_>#$tusSJ+zSuc p5YZ-MEM*wRg54#bi;K&M^BdjW!v$Q*XjK3J002ovPDHLkV1lLySIGbX diff --git a/apps/2047++/metadata.json b/apps/2047++/metadata.json deleted file mode 100644 index f887d95d9..000000000 --- a/apps/2047++/metadata.json +++ /dev/null @@ -1,14 +0,0 @@ -{ "id": "2047++", - "name": "2047++", - "shortName":"2047++", - "icon": "app.png", - "version":"0.01", - "description": "Bangle version of a tile shifting game", - "supports" : ["BANGLEJS2"], - "readme": "README.md", - "tags": "game", - "storage": [ - {"name":"2047++.app.js","url":"2047++.app.js"}, - {"name":"2047++.img","url":"app-icon.js","evaluate":true} - ] -} From 80246d2398a93311aaa131886f873b54da705a66 Mon Sep 17 00:00:00 2001 From: marko Date: Wed, 23 Mar 2022 15:00:54 -0400 Subject: [PATCH 47/73] New app 2047pp --- apps/2047pp/2047pp.app.js | 129 ++++++++++++++++++++++++++++++++++++++ apps/2047pp/README.md | 6 ++ apps/2047pp/app-icon.js | 1 + apps/2047pp/app.png | Bin 0 -> 759 bytes apps/2047pp/metadata.json | 14 +++++ 5 files changed, 150 insertions(+) create mode 100644 apps/2047pp/2047pp.app.js create mode 100644 apps/2047pp/README.md create mode 100644 apps/2047pp/app-icon.js create mode 100644 apps/2047pp/app.png create mode 100644 apps/2047pp/metadata.json diff --git a/apps/2047pp/2047pp.app.js b/apps/2047pp/2047pp.app.js new file mode 100644 index 000000000..b508d8f07 --- /dev/null +++ b/apps/2047pp/2047pp.app.js @@ -0,0 +1,129 @@ + + +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) { + 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; moved = true; + } + 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; moved = true; + } + } + } + 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 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; + } +} + +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(); + } +} + +var twok = new TwoK(); +twok.addDigit(); twok.addDigit(); +twok.render(); +Bangle.on("drag", dragHandler); diff --git a/apps/2047pp/README.md b/apps/2047pp/README.md new file mode 100644 index 000000000..e685946c1 --- /dev/null +++ b/apps/2047pp/README.md @@ -0,0 +1,6 @@ + +# Game of 2047pp (2047++) + +Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024. + +Attempt to combine equal numbers by swiping left, right, up or down. diff --git a/apps/2047pp/app-icon.js b/apps/2047pp/app-icon.js new file mode 100644 index 000000000..4086d1879 --- /dev/null +++ b/apps/2047pp/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A31gAeFtoxPF9wujGBYQG1YAWF6ur5gAYGIovOFzIABF6ReaMAwv/F/4v/F7ejv9/0Yvq1Eylksv4vqvIuBF9ZeDF9ZeBqovr1AsB0YvrLwXMF9ReDF9ZeBq1/v4vBqowKF7lWFYIAFF/7vXAAa/qF+jxB0YvsABov/F/4v/F6WsF7YgEF5xgaLwgvPGIQAWDwwvQADwvJGEguKF+AxhFpoA/AH4A/AFI=")) \ No newline at end of file diff --git a/apps/2047pp/app.png b/apps/2047pp/app.png new file mode 100644 index 0000000000000000000000000000000000000000..d1fb4a5e5cbadce3da7b36806d4907f680f8f1bb GIT binary patch literal 759 zcmVYCuD-J|F04S(qype>Ll^oVPK(#&(MEqX}dr+-4N41U&D6G<#pk8p=ry~PL z8P~wDv0Fxk@G(D*-Esf`5DS+uaYFpJB+Wpns^Zb}E7&tzFF7%tm102J6nd69|4+P3 zr1VfMt5{y0fI>`0^KD2mu=F8{y6M673*Til--d7lyX64hte$~F4EL^Xh;F_M;RY5n zQPi8Q(T|*z{|6UpV5c0w+w9;*9}v8ZE@h%j4t73gCg!Qfe`}$Ac#sB~}C%=m9 zQmlcEAIAXzTR($HE?;WPt>nU3$%Ta*5c>_tUp2cB`Ud77yzh$Lczg#y>rX6t^Z|D> zcQA?REP&Q#P6pBq$e1?!8Tl#X8W=W?3|OSe*3omHjttb47#RG02|5f6e$ zObTJ!ml%i%20ylab9U#WUDz$7W@n({nZhq+5~`IO*5Pi07qm053E*;P(4-Kmo@>1; z>;uNw7hc?M3Z*1!=?Nlw%8PRi7>4l#z$>YW4#!KwFcx?T+e^N5I_>#$tusSJ+zSuc p5YZ-MEM*wRg54#bi;K&M^BdjW!v$Q*XjK3J002ovPDHLkV1lLySIGbX literal 0 HcmV?d00001 diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json new file mode 100644 index 000000000..7c120efec --- /dev/null +++ b/apps/2047pp/metadata.json @@ -0,0 +1,14 @@ +{ "id": "2047pp", + "name": "2047pp", + "shortName":"2047pp", + "icon": "app.png", + "version":"0.01", + "description": "Bangle version of a tile shifting game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "tags": "game", + "storage": [ + {"name":"2047pp.app.js","url":"2047pp.app.js"}, + {"name":"2047pp.img","url":"app-icon.js","evaluate":true} + ] +} From 0342b131249937c8a692f0a500748d264b3ab05e Mon Sep 17 00:00:00 2001 From: marko Date: Wed, 23 Mar 2022 15:38:16 -0400 Subject: [PATCH 48/73] Support Bangles 1&2 --- apps/2047pp/2047pp.app.js | 31 +++++++++++++++++++++---------- apps/2047pp/README.md | 3 ++- apps/2047pp/metadata.json | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/2047pp/2047pp.app.js b/apps/2047pp/2047pp.app.js index b508d8f07..9b2283036 100644 --- a/apps/2047pp/2047pp.app.js +++ b/apps/2047pp/2047pp.app.js @@ -1,15 +1,13 @@ - - 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) { + 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)); @@ -38,14 +36,14 @@ class TwoK { 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; moved = true; + 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; moved = true; + this.b[y][3] = 0; } } } @@ -123,7 +121,20 @@ function dragHandler(e) { } } +function swipeHandler() { + +} + +function buttonHandler() { + +} + var twok = new TwoK(); twok.addDigit(); twok.addDigit(); twok.render(); -Bangle.on("drag", dragHandler); +if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); +else 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}); +} diff --git a/apps/2047pp/README.md b/apps/2047pp/README.md index e685946c1..8228892fd 100644 --- a/apps/2047pp/README.md +++ b/apps/2047pp/README.md @@ -3,4 +3,5 @@ Tile shifting game inspired by the well known 2048 game. Also very similar to another Bangle game, Game1024. -Attempt to combine equal numbers by swiping left, right, up or down. +Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using +the top/bottom button (Bangle 1). diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json index 7c120efec..0241e9933 100644 --- a/apps/2047pp/metadata.json +++ b/apps/2047pp/metadata.json @@ -4,7 +4,7 @@ "icon": "app.png", "version":"0.01", "description": "Bangle version of a tile shifting game", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS1","BANGLEJS2"], "readme": "README.md", "tags": "game", "storage": [ From 0c24bd397aeb82f5d3d1af2794e95942e9039c5f Mon Sep 17 00:00:00 2001 From: marko Date: Wed, 23 Mar 2022 15:40:28 -0400 Subject: [PATCH 49/73] Use correct string for supported devices --- apps/2047pp/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json index 0241e9933..0a362c583 100644 --- a/apps/2047pp/metadata.json +++ b/apps/2047pp/metadata.json @@ -4,7 +4,7 @@ "icon": "app.png", "version":"0.01", "description": "Bangle version of a tile shifting game", - "supports" : ["BANGLEJS1","BANGLEJS2"], + "supports" : ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "tags": "game", "storage": [ From 60c3a3249459c7219ed28b8b1c8d6b9f7cd98fe8 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Wed, 23 Mar 2022 21:52:26 +0100 Subject: [PATCH 50/73] waypointer Bangle.js 2 compatibility --- apps/waypointer/ChangeLog | 2 ++ apps/waypointer/README.md | 2 +- apps/waypointer/app.js | 55 ++++++++++++++++++----------------- apps/waypointer/metadata.json | 4 +-- 4 files changed, 34 insertions(+), 29 deletions(-) create mode 100644 apps/waypointer/ChangeLog diff --git a/apps/waypointer/ChangeLog b/apps/waypointer/ChangeLog new file mode 100644 index 000000000..1b584f7dd --- /dev/null +++ b/apps/waypointer/ChangeLog @@ -0,0 +1,2 @@ +0.01: New app! +0.02: Make Bangle.js 2 compatible diff --git a/apps/waypointer/README.md b/apps/waypointer/README.md index e98fdbb7e..c0b4c5125 100644 --- a/apps/waypointer/README.md +++ b/apps/waypointer/README.md @@ -24,7 +24,7 @@ need to travel in to reach the selected waypoint. The blue text is the name of the current waypoint. NONE means that there is no waypoint set and so bearing and distance will remain at 0. To select a waypoint, press BTN2 (middle) and wait for the blue text to turn -white. Then use BTN1 and BTN3 to select a waypoint. The waypoint +white. Then use BTN1 and BTN3 (swipe up/down on Bangle.js 2) to select a waypoint. The waypoint choice is fixed by pressing BTN2 again. In the screen shot below a waypoint giving the location of Stone Henge has been selected. diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js index d3aab7c50..615fbbc36 100644 --- a/apps/waypointer/app.js +++ b/apps/waypointer/app.js @@ -1,24 +1,25 @@ -var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow -var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white -var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue +const scale = g.getWidth()/240; +var pal_by = new Uint16Array([g.getBgColor(),0xFFC0],0,1); // black, yellow +var pal_bw = new Uint16Array([g.getBgColor(),g.getColor()],0,1); // black, white +var pal_bb = new Uint16Array([g.getBgColor(),0x07ff],0,1); // black, blue // having 3 2 color pallette keeps the memory requirement lower -var buf1 = Graphics.createArrayBuffer(160,160,1, {msb:true}); -var buf2 = Graphics.createArrayBuffer(80,40,1, {msb:true}); +var buf1 = Graphics.createArrayBuffer(160*scale,160*scale,1, {msb:true}); +var buf2 = Graphics.createArrayBuffer(g.getWidth()/3,40*scale,1, {msb:true}); var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo=")); function flip1(x,y) { - g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y); + g.drawImage({width:160*scale,height:160*scale,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y); buf1.clear(); } function flip2_bw(x,y) { - g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y); + g.drawImage({width:g.getWidth()/3,height:40*scale,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y); buf2.clear(); } function flip2_bb(x,y) { - g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y); + g.drawImage({width:g.getWidth()/3,height:40*scale,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y); buf2.clear(); } @@ -51,12 +52,12 @@ function drawCompass(course) { previous.course = course; buf1.setColor(1); - buf1.fillCircle(80,80,79,79); + buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,79*scale); buf1.setColor(0); - buf1.fillCircle(80,80,69,69); + buf1.fillCircle(buf1.getWidth()/2,buf1.getHeight()/2,69*scale); buf1.setColor(1); - buf1.drawImage(arrow_img, 80, 80, {scale:3, rotate:radians(course)} ); - flip1(40, 30); + buf1.drawImage(arrow_img, buf1.getWidth()/2, buf1.getHeight()/2, {scale:3*scale, rotate:radians(course)} ); + flip1(40*scale, Bangle.appRect.y+6*scale); } /***** COMPASS CODE ***********/ @@ -138,7 +139,7 @@ function distance(a,b){ function drawN(){ - buf2.setFont("Vector",24); + buf2.setFont("Vector",24*scale); var bs = wp_bearing.toString(); bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs; var dst = loc.distance(dist); @@ -147,12 +148,12 @@ function drawN(){ // show distance on the left if (previous.dst !== dst) { - previous.dst = dst + previous.dst = dst; buf2.setColor(1); buf2.setFontAlign(-1,-1); - buf2.setFont("Vector", 20); + buf2.setFont("Vector", 20*scale); buf2.drawString(dst,0,0); - flip2_bw(0, 200); + flip2_bw(0, g.getHeight()-40*scale); } // bearing, place in middle at bottom of compass @@ -160,9 +161,9 @@ function drawN(){ previous.bs = bs; buf2.setColor(1); buf2.setFontAlign(0, -1); - buf2.setFont("Vector",38); - buf2.drawString(bs,40,0); - flip2_bw(80, 200); + buf2.setFont("Vector",38*scale); + buf2.drawString(bs,40*scale,0); + flip2_bw(g.getWidth()/3, g.getHeight()-40*scale); } // waypoint name on right @@ -170,13 +171,13 @@ function drawN(){ previous.selected = selected; buf2.setColor(1); buf2.setFontAlign(1,-1); // right, bottom - buf2.setFont("Vector", 20); - buf2.drawString(wp.name, 80, 0); + buf2.setFont("Vector", 20*scale); + buf2.drawString(wp.name, 80*scale, 0); if (selected) - flip2_bw(160, 200); + flip2_bw(g.getWidth()/3*2, g.getHeight()-40*scale); else - flip2_bb(160, 200); + flip2_bb(g.getWidth()/3*2, g.getHeight()-40*scale); } } @@ -229,9 +230,11 @@ function startdraw(){ } function setButtons(){ - setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"}); - setWatch(doselect, BTN2, {repeat:true,edge:"falling"}); - setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"}); + Bangle.setUI("updown", d=>{ + if (d<0) { nextwp(-1); } + else if (d>0) { nextwp(1); } + else { doselect(); } + }); } Bangle.on('lcdPower',function(on) { diff --git a/apps/waypointer/metadata.json b/apps/waypointer/metadata.json index cb477107b..111259bbc 100644 --- a/apps/waypointer/metadata.json +++ b/apps/waypointer/metadata.json @@ -1,11 +1,11 @@ { "id": "waypointer", "name": "Way Pointer", - "version": "0.01", + "version": "0.02", "description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation", "icon": "waypointer.png", "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "interface": "waypoints.html", "storage": [ From fd8cc60283512c3aa649f525a4d01482af79e531 Mon Sep 17 00:00:00 2001 From: marko Date: Wed, 23 Mar 2022 17:24:42 -0400 Subject: [PATCH 51/73] Make border thicknesses more consistent; add screenshot; allow running in emulator. --- apps/2047pp/2047pp.app.js | 8 ++++---- apps/2047pp/2047pp_screenshot.png | Bin 0 -> 4541 bytes apps/2047pp/README.md | 2 ++ apps/2047pp/metadata.json | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 apps/2047pp/2047pp_screenshot.png diff --git a/apps/2047pp/2047pp.app.js b/apps/2047pp/2047pp.app.js index 9b2283036..58738d04a 100644 --- a/apps/2047pp/2047pp.app.js +++ b/apps/2047pp/2047pp.app.js @@ -18,11 +18,11 @@ class TwoK { bw = Math.floor(w/4); g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0); g.setFont("Vector", 16).setColor("#fff").drawString("Score:"+this.score.toString(), g.getWidth()/2, 8); - this.drawBRect(xo-2, yo-2, xo+w+2, yo+h, 2, "#a88", "#caa", false); + this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false); for (y=0; y<4; ++y) for (x=0; x<4; ++x) { b = this.b[y][x]; - this.drawBRect(xo+x*bw, yo+y*bh-2, xo+(x+1)*bh, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true); + this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true); if (b > 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)); @@ -35,7 +35,7 @@ class TwoK { 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]; + 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) @@ -133,7 +133,7 @@ var twok = new TwoK(); twok.addDigit(); twok.addDigit(); twok.render(); if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); -else if (process.env.HWVERSION==1) { +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}); diff --git a/apps/2047pp/2047pp_screenshot.png b/apps/2047pp/2047pp_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..8c407fb6fe4b78d2f60c30651c60f8513b5c658d GIT binary patch literal 4541 zcmb7IeLR!v|G)0dHnvO;jF41Ehnc7j<{^iv6cR%7w1%Pxqhd_8IW4K^(L)KTwDOR7 zikP~kA}SAgs>WFdQN%op%J1f!*ZKYN{pb7rW4rHN_x1T)*XMJ6-tW)*Uc94&jXYiz z4*-zgxnrv{a+NN8WUeIRz-D5Dw-l)7IVsiw%R9=Da08V82n~HFK1fT@J?grx`!HyioMg5>d)+R z)=tU^O%<$7YFRznr?2Dctv@_G{IjCxcSTE{HCc+{4)%T>oPTT)G$45L!$t7%MLxUM z$?UZLv8I}-miAbHx__&i2#@BnW$#&?7oSgTZjay$ji z@{!9^I_h)9+xxNlzCh8VFmL%OZaO4+dgngih@*7>ozy>27Z!52DG9&xFqXTm z+We!2wEg|N_nIEDBk8jnM*MMMVU zx~KeGk+_4bkuvp+oi0|ri|bZ|yH7-q_)I1>p40}gQchO;ixw@-lrCqZ%w&b+Z-3g= zI=o)$9Db+!h+y`bYQw~Q{-GHDRk8gF;HA@Mcc!hYtMvYaN1@=Z(9>b)(65tAi!{Lf z#wvx&wqt)D-V9=j?*~ZMA)eRmzqC=Dhx(6!H1LGOE_e4)q2ecQ!D(}I`}czK6iTy# z;ts^>Di*I!hkylZY`_!`3=Fv_oEzXO+%T_5M6Cgt4uTr;+{{e*yT}*q0fG6Vs_&D> zGFL@Td})Zg@KPHvcK9iwfEhU?ZmDi45bdaM=P=L4#Ni{vE#$FF<5(EPxG{Uzu8ukk zmcU?$^{tAQ_YMQl24G~Hpyx4&(Q}@1s=p-gJ^$M3EpZJpDFBm41Cmt(N5w7mo)e0* zW&0T3lVd;ncPD0iSR>kAWEg!`h=;hCjcTVoqRz>%<+~3J0I?8!@^t3|*zA|*VRpKq zPCy{RFl}1a(iWV_wl#bx8Nt5~bn;EXMRH3GSjiYX;7IBF8cEuH493a#$7{;c`Tjz`VP(zrvYANgF03ql9M0W~v4&Z;Pn4Iy zv14v)z(4DPSE{LoENjg+Pe@wo``xKBiTsr3TJruGa!{E{3;F&4?sq@0SKD*&Zj*hM zYmVxR4lMnvX=PK6uhppA8VPLmRM{w~Z`UzqsTs&4lN3XdvcSph>�s2LAZhiWN(X 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 literal 0 HcmV?d00001 diff --git a/apps/2047pp/README.md b/apps/2047pp/README.md index 8228892fd..cac3323a6 100644 --- a/apps/2047pp/README.md +++ b/apps/2047pp/README.md @@ -5,3 +5,5 @@ Tile shifting game inspired by the well known 2048 game. Also very similar to an Attempt to combine equal numbers by swiping left, right, up or down (on Bangle 2) or swiping left/right and using the top/bottom button (Bangle 1). + +![Screenshot](./2047pp_screenshot.png) diff --git a/apps/2047pp/metadata.json b/apps/2047pp/metadata.json index 0a362c583..f0fd6c1e3 100644 --- a/apps/2047pp/metadata.json +++ b/apps/2047pp/metadata.json @@ -5,6 +5,7 @@ "version":"0.01", "description": "Bangle version of a tile shifting game", "supports" : ["BANGLEJS","BANGLEJS2"], + "allow_emulator": true, "readme": "README.md", "tags": "game", "storage": [ From dd27b11eb87b5087e668549ab59b908260c10545 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Thu, 24 Mar 2022 09:43:32 +0800 Subject: [PATCH 52/73] Update app.js Fix automatic font sizing. --- apps/authentiwatch/app.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 6e2951b9f..05d94fc46 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -119,15 +119,14 @@ var state = { function sizeFont(id, txt, w) { let sz = fontszCache[id]; - if (sz) { - g.setFont("Vector", sz); - } else { + if (!sz) { sz = TOKEN_DIGITS_HEIGHT; do { g.setFont("Vector", sz--); } while (g.stringWidth(txt) > w); - fontszCache[id] = sz + 1; + fontszCache[id] = ++sz; } + g.setFont("Vector", sz); } tokenY = id => id * TOKEN_HEIGHT + AR.y - state.listy; From ae74a508384a9f92ed3acc7bf776106a251fcd58 Mon Sep 17 00:00:00 2001 From: Alessandro Cocco Date: Wed, 23 Mar 2022 22:54:32 +0100 Subject: [PATCH 53/73] [Wave Clock] Show the day of the week --- apps/waveclk/ChangeLog | 1 + apps/waveclk/app.js | 4 ++++ apps/waveclk/metadata.json | 2 +- apps/waveclk/screenshot.png | Bin 2884 -> 6237 bytes 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/waveclk/ChangeLog b/apps/waveclk/ChangeLog index 8c2a33143..f1fb77c59 100644 --- a/apps/waveclk/ChangeLog +++ b/apps/waveclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Load widgets after setUI so widclk knows when to hide +0.03: Show the day of the week diff --git a/apps/waveclk/app.js b/apps/waveclk/app.js index f1c67ce2f..18b28500b 100644 --- a/apps/waveclk/app.js +++ b/apps/waveclk/app.js @@ -41,6 +41,7 @@ function draw() { var date = new Date(); var timeStr = require("locale").time(date,1); var dateStr = require("locale").date(date).toUpperCase(); + var dowStr = require("locale").dow(date).toUpperCase(); // draw time g.setFontAlign(0,0).setFont("ZCOOL"); g.drawString(timeStr,x,y); @@ -48,6 +49,9 @@ function draw() { y += 35; g.setFontAlign(0,0,1).setFont("6x8"); g.drawString(dateStr,g.getWidth()-8,g.getHeight()/2); + // draw the day of the week + g.setFontAlign(0,0,3).setFont("6x8"); + g.drawString(dowStr,8,g.getHeight()/2); // queue draw in one minute queueDraw(); } diff --git a/apps/waveclk/metadata.json b/apps/waveclk/metadata.json index a8d270da2..9ba2798ff 100644 --- a/apps/waveclk/metadata.json +++ b/apps/waveclk/metadata.json @@ -1,7 +1,7 @@ { "id": "waveclk", "name": "Wave Clock", - "version": "0.02", + "version": "0.03", "description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/)", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/waveclk/screenshot.png b/apps/waveclk/screenshot.png index 7f05ce68880935db0de4aaf840077e9b30413db7..161ef96ef0194e95c23821555e073a21607dfc83 100644 GIT binary patch literal 6237 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 Hu0mjfXH(f- literal 2884 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 iR539+GBi3hI4dwQIxsMs^94u%0000 Date: Thu, 24 Mar 2022 15:52:19 +0000 Subject: [PATCH 54/73] first hack at support for running the App Loader inside Gadgetbridge --- gadgetbridge.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 1 + 2 files changed, 163 insertions(+) create mode 100644 gadgetbridge.js diff --git a/gadgetbridge.js b/gadgetbridge.js new file mode 100644 index 000000000..679fffc60 --- /dev/null +++ b/gadgetbridge.js @@ -0,0 +1,162 @@ +/* Detects if we're running under Gadgetbridge in a WebView, and if +so it overwrites the 'Puck' library with a special one that calls back +into Gadgetbridge to handle watch communications */ + +/*// test code +Android = { + bangleTx : function(data) { + console.log("TX : "+JSON.stringify(data)); + } +};*/ + +if (typeof Android!=="undefined") { + console.log("Running under Gadgetbridge, overwrite Puck library"); + + var isBusy = false; + var queue = []; + var connection = { + cb : function(data) {}, + write : function(data, writecb) { + Android.bangleTx(data); + Puck.writeProgress(data.length, data.length); + if (writecb) setTimeout(writecb,10); + }, + close : function() {}, + received : "", + hadData : false + } + + function bangleRx(data) { +// document.getElementById("status").innerText = "RX:"+data; + connection.received += data; + connection.hadData = true; + if (connection.cb) connection.cb(data); + } + + function log(level, s) { + if (Puck.log) Puck.log(level, s); + } + + function handleQueue() { + if (!queue.length) return; + var q = queue.shift(); + log(3,"Executing "+JSON.stringify(q)+" from queue"); + if (q.type == "write") Puck.write(q.data, q.callback, q.callbackNewline); + else log(1,"Unknown queue item "+JSON.stringify(q)); + } + + /* convenience function... Write data, call the callback with data: + callbackNewline = false => if no new data received for ~0.2 sec + callbackNewline = true => after a newline */ + function write(data, callback, callbackNewline) { + let result; + /// If there wasn't a callback function, then promisify + if (typeof callback !== 'function') { + callbackNewline = callback; + + result = new Promise((resolve, reject) => callback = (value, err) => { + if (err) reject(err); + else resolve(value); + }); + } + + if (isBusy) { + log(3, "Busy - adding Puck.write to queue"); + queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline}); + return result; + } + + var cbTimeout; + function onWritten() { + if (callbackNewline) { + connection.cb = function(d) { + var newLineIdx = connection.received.indexOf("\n"); + if (newLineIdx>=0) { + var l = connection.received.substr(0,newLineIdx); + connection.received = connection.received.substr(newLineIdx+1); + connection.cb = undefined; + if (cbTimeout) clearTimeout(cbTimeout); + cbTimeout = undefined; + if (callback) + callback(l); + isBusy = false; + handleQueue(); + } + }; + } + // wait for any received data if we have a callback... + var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data + var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/; + var maxDataTime = dataWaitTime; // max time we wait after having received data + cbTimeout = setTimeout(function timeout() { + cbTimeout = undefined; + if (maxTime) maxTime--; + if (maxDataTime) maxDataTime--; + if (connection.hadData) maxDataTime=dataWaitTime; + if (maxDataTime && maxTime) { + cbTimeout = setTimeout(timeout, 100); + } else { + connection.cb = undefined; + if (callback) + callback(connection.received); + isBusy = false; + handleQueue(); + connection.received = ""; + } + connection.hadData = false; + }, 100); + } + + if (!connection.txInProgress) connection.received = ""; + isBusy = true; + connection.write(data, onWritten); + return result + } + + // ---------------------------------------------------------- + + Puck = { + /// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all. + debug : Puck.debug, + /// Should we use flow control? Default is true + flowControl : true, + /// Used internally to write log information - you can replace this with your own function + log : function(level, s) { if (level <= this.debug) console.log(" "+s)}, + /// Called with the current send progress or undefined when done - you can replace this with your own function + writeProgress : Puck.writeProgress, + connect : function(callback) { + setTimeout(callback, 10); + }, + write : write, + eval : function(expr, cb) { + const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true) + .then(function (d) { + try { + return JSON.parse(d); + } catch (e) { + log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString()); + return Promise.reject(d); + } + }); + if (cb) { + return void response.then(cb, (err) => cb(null, err)); + } else { + return response; + } + }, + isConnected : function() { return true; }, + getConnection : function() { return connection; }, + close : function() { + if (connection) + connection.close(); + }, + }; + // no need for header + document.getElementsByTagName("header")[0].style="display:none"; + // force connection attempt automatically + setTimeout(function() { + getInstalledApps(true).catch(err => { + showToast("Device connection failed, "+err,"error"); + }); + }, 100); +} diff --git a/index.html b/index.html index a418b48eb..bd8ddea5a 100644 --- a/index.html +++ b/index.html @@ -179,5 +179,6 @@ + From 0a347ee03166662d5dbed0eb3d7ba292079dd15f Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 24 Mar 2022 16:16:25 +0000 Subject: [PATCH 55/73] boot 0.46: Fix no clock found error on Bangle.js 2 --- apps/boot/ChangeLog | 1 + apps/boot/bootloader.js | 2 +- apps/boot/metadata.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index ef437fe3b..87b5f7def 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -49,3 +49,4 @@ 0.43: Fix Gadgetbridge handling with Programmable:off 0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM) 0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk) +0.46: Fix no clock found error on Bangle.js 2 diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js index 3cf885ac9..45e271f30 100644 --- a/apps/boot/bootloader.js +++ b/apps/boot/bootloader.js @@ -14,6 +14,6 @@ if (!clockApp) { if (clockApp) clockApp = require("Storage").read(clockApp.src); } -if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, BTN2, {repeat:false,edge:"falling"});`; +if (!clockApp) clockApp=`E.showMessage("No Clock Found");setWatch(()=>{Bangle.showLauncher();}, global.BTN2||BTN, {repeat:false,edge:"falling"});`; eval(clockApp); delete clockApp; diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index c21ab6833..11884576f 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.45", + "version": "0.46", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", From 6dbe7dfee1ddc3a661c509fc298b36063f033a28 Mon Sep 17 00:00:00 2001 From: chiefdaft Date: Fri, 25 Mar 2022 00:31:38 +0100 Subject: [PATCH 56/73] Bug: Fixed issue #1609 added a message popup state handler to control unwanted screen redraw --- apps/game1024/app.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/game1024/app.js b/apps/game1024/app.js index 9f6081376..133630634 100644 --- a/apps/game1024/app.js +++ b/apps/game1024/app.js @@ -144,6 +144,13 @@ const buttons = { }, add: function(btn) { this.all.push(btn); + }, + isPopUpActive: false, + activatePopUp: function() { + this.isPopUpActive = true; + }, + deActivatePopUp: function() { + this.isPopUpActive = false; } }; /** @@ -253,7 +260,6 @@ const dragThreshold = 10; const clickThreshold = 3; let allSquares = []; -// let buttons = []; class Button { constructor(name, x0, y0, width, height, text, bg, fg, cb, enabled) { @@ -483,6 +489,7 @@ function initGame() { Bangle.drawWidgets(); } function drawPopUp(message,cb) { + buttons.activatePopUp(); g.setColor('#FFFFFF'); let rDims = Bangle.appRect; g.fillPoly([rDims.x+10, rDims.y+20, @@ -505,6 +512,7 @@ function drawPopUp(message,cb) { g.drawString(message, rDims.x+20, rDims.y+20); buttons.add(btnYes); buttons.add(btnNo); + } function handlePopUpClicks(btn) { const name = btn.name; @@ -512,6 +520,7 @@ function handlePopUpClicks(btn) { buttons.all.pop(); // remove the yes button buttons.all.forEach(b => {b.enable();}); // enable the remaining buttons again debug(() => console.log("Button name =", name)); + buttons.deActivatePopUp(); switch (name) { case 'yes': resetGame(); @@ -568,14 +577,13 @@ function handleclick(e) { // Handle a drag event (moving the stones around) function handledrag(e) { - /*debug(Math.abs(e.dx) > Math.abs(e.dy) ? - (e.dx > 0 ? e => console.log('To the right') : e => console.log('To the left') ) : - (e.dy > 0 ? e => console.log('Move down') : e => console.log('Move up') )); - */ - // [move.right, move.left, move.up, move.down] - runGame((Math.abs(e.dx) > Math.abs(e.dy) ? - (e.dx > 0 ? mover.direction.right : mover.direction.left ) : - (e.dy > 0 ? mover.direction.down : mover.direction.up ))); + // Stop moving things around when the popup message is active + // Bangleapps issue #1609 + if (!(buttons.isPopUpActive)) { + runGame((Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? mover.direction.right : mover.direction.left ) : + (e.dy > 0 ? mover.direction.down : mover.direction.up ))); + } } // Evaluate "drag" events from the UI and call handlers for drags or clicks // The UI sends a drag as a series of events indicating partial movements From a46051a88ec208a12edf48cf4ee7a3a2824a1dfc Mon Sep 17 00:00:00 2001 From: chiefdaft Date: Fri, 25 Mar 2022 00:38:17 +0100 Subject: [PATCH 57/73] v0.06 added to metadata and changelog --- apps/game1024/ChangeLog | 3 ++- apps/game1024/metadata.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/game1024/ChangeLog b/apps/game1024/ChangeLog index ffb1f94bc..8759fb428 100644 --- a/apps/game1024/ChangeLog +++ b/apps/game1024/ChangeLog @@ -2,4 +2,5 @@ 0.02: Temporary intermediate version 0.03: Basic colors 0.04: Bug fix score reset after Game Over, new icon -0.05: Chevron marker on the randomly added square \ No newline at end of file +0.05: Chevron marker on the randomly added square +0.06: Fixed issue 1609 added a message popup state handler to control unwanted screen redraw \ No newline at end of file diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json index 557d77b89..73d7607f3 100644 --- a/apps/game1024/metadata.json +++ b/apps/game1024/metadata.json @@ -1,7 +1,7 @@ { "id": "game1024", "name": "1024 Game", "shortName" : "1024 Game", - "version": "0.05", + "version": "0.06", "icon": "game1024.png", "screenshots": [ {"url":"screenshot.png" } ], "readme":"README.md", From f8d64ab7dd51ce9e391b27001b364226d2021f94 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:19:24 -0500 Subject: [PATCH 58/73] Create app.js --- apps/gsat/app.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 apps/gsat/app.js diff --git a/apps/gsat/app.js b/apps/gsat/app.js new file mode 100644 index 000000000..3a7d443fe --- /dev/null +++ b/apps/gsat/app.js @@ -0,0 +1,38 @@ +// Clear screen +g.clear(); + +const secsinmin = 60; +const quickfixperiod = 900; +var seconds = 1200; + +function countSecs() { + if (seconds != 0) {seconds -=1;} + console.log(seconds); +} +function drawTime() { + g.clear(); + g.setFontAlign(0,0); + g.setFont('Vector', 12); + g.drawString('Geek Squad Appointment Timer', 125, 20); + if (seconds == 0) { + g.setFont('Vector', 35); + g.drawString('Appointment', 125, 100); + g.drawString('finished!', 125, 150); + Bangle.buzz(); + return; + } + min = seconds / secsinmin; + if (seconds < quickfixperiod) { + g.setFont('Vector', 20); + g.drawString('Quick Fix', 125, 50); + g.drawString('Period Passed!', 125, 75); + } + g.setFont('Vector', 50); + g.drawString(Math.ceil(min), 125, 125); + g.setFont('Vector', 25); + g.drawString('minutes', 125, 165); + g.drawString('remaining', 125, 195); +} +drawTime(); +setInterval(countSecs, 1000); +setInterval(drawTime, 60000); From 840cecf976d2444edcae2c7466fc8a3c798594c3 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:22:45 -0500 Subject: [PATCH 59/73] Create metadata.json --- apps/gsat/metadata.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/gsat/metadata.json diff --git a/apps/gsat/metadata.json b/apps/gsat/metadata.json new file mode 100644 index 000000000..cda1f7096 --- /dev/null +++ b/apps/gsat/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "gsat", + "name": "Geek Squad Appointment Timer", + "shortName": "gsat", + "version": "0.01", + "description": "Starts a 20 minute timer for appointments at Geek Squad.", + "icon": "app.png", + "tags": "tool", + "readme": "README.md", + "supports": ["BANGLEJS"], + "storage": [ + {"name":"gsat.app.js","url":"app.js"}, + ] +} From 027048302aa4ba67c68d7bffd1f4960048f2e687 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:23:20 -0500 Subject: [PATCH 60/73] Add app png --- apps/gsat/icons8-clock-48.png | Bin 0 -> 929 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/gsat/icons8-clock-48.png diff --git a/apps/gsat/icons8-clock-48.png b/apps/gsat/icons8-clock-48.png new file mode 100644 index 0000000000000000000000000000000000000000..cf057046be6eca51564a213bc3e7307271464009 GIT binary patch literal 929 zcmV;S177@zP)hL^4Q9~j~`7LX9JB(!Q` zf!g{&{RcFW&Q~mny+EQd5wRkG(GU|*v#i00#krWf=gz{s+-Te{nPTqF`ObG{=bo84 z7wV|v|Hht3JPkk}FaUG|9Y7m!Ao=|p@Bx?yW`SwoCGazne{UQEt^-TJmSbB2t^w^e zR}HiRcY*JgVs~m~;0|!8R^p4mY6RiBtpaENr0^Uv>(vCT0r!Cspcg0r`;t~jo{azx zfb|`CCKF##(F8oL;_xjnl#FrV8%ADg6Xz30z8#UnEWQMc5jI;nU_9Z{{Eq{QD8L7t!&STXpy9ZFm@kj^1Fem7{l-O!eBYr8>b)d^b3W}9W~#A6U*g95Oi>^=c4 z)~Zv`8K;EXvhfK3pp)!7am&FTkR7(HNX8sFd;lS+^%_N}!U1DH{Ly|K9>B#6_hueBM`PvzgXPMrIx>vGBiU)6Z4 zEpn3sb9fkCLB-z-YcKpmwJ#|r9RWl zC5~dFfRx^~zcZo4lLpcS%$dU%U1Vo|3%C)3JcraOHF*iC9^?bK^S~v{rNu|U-T>|j zG$FN0?PUAC7)9=|wrEE3yiS#A@zILA+sGlcN|VDaIFIb+1>`8+h@2|>kwB-& z?yx%JM4NdN29eBAO@MFvNQ4*G7A;7*Pj7ZUZ3CJ0gS8T$MjKKwTeK9b&o&dl(GUSK zKeKitb( Date: Thu, 24 Mar 2022 20:23:55 -0500 Subject: [PATCH 61/73] Delete icons8-clock-48.png --- apps/gsat/icons8-clock-48.png | Bin 929 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/gsat/icons8-clock-48.png diff --git a/apps/gsat/icons8-clock-48.png b/apps/gsat/icons8-clock-48.png deleted file mode 100644 index cf057046be6eca51564a213bc3e7307271464009..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 929 zcmV;S177@zP)hL^4Q9~j~`7LX9JB(!Q` zf!g{&{RcFW&Q~mny+EQd5wRkG(GU|*v#i00#krWf=gz{s+-Te{nPTqF`ObG{=bo84 z7wV|v|Hht3JPkk}FaUG|9Y7m!Ao=|p@Bx?yW`SwoCGazne{UQEt^-TJmSbB2t^w^e zR}HiRcY*JgVs~m~;0|!8R^p4mY6RiBtpaENr0^Uv>(vCT0r!Cspcg0r`;t~jo{azx zfb|`CCKF##(F8oL;_xjnl#FrV8%ADg6Xz30z8#UnEWQMc5jI;nU_9Z{{Eq{QD8L7t!&STXpy9ZFm@kj^1Fem7{l-O!eBYr8>b)d^b3W}9W~#A6U*g95Oi>^=c4 z)~Zv`8K;EXvhfK3pp)!7am&FTkR7(HNX8sFd;lS+^%_N}!U1DH{Ly|K9>B#6_hueBM`PvzgXPMrIx>vGBiU)6Z4 zEpn3sb9fkCLB-z-YcKpmwJ#|r9RWl zC5~dFfRx^~zcZo4lLpcS%$dU%U1Vo|3%C)3JcraOHF*iC9^?bK^S~v{rNu|U-T>|j zG$FN0?PUAC7)9=|wrEE3yiS#A@zILA+sGlcN|VDaIFIb+1>`8+h@2|>kwB-& z?yx%JM4NdN29eBAO@MFvNQ4*G7A;7*Pj7ZUZ3CJ0gS8T$MjKKwTeK9b&o&dl(GUSK zKeKitb( Date: Thu, 24 Mar 2022 20:24:22 -0500 Subject: [PATCH 62/73] Add files via upload --- apps/gsat/app.png | Bin 0 -> 929 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/gsat/app.png diff --git a/apps/gsat/app.png b/apps/gsat/app.png new file mode 100644 index 0000000000000000000000000000000000000000..cf057046be6eca51564a213bc3e7307271464009 GIT binary patch literal 929 zcmV;S177@zP)hL^4Q9~j~`7LX9JB(!Q` zf!g{&{RcFW&Q~mny+EQd5wRkG(GU|*v#i00#krWf=gz{s+-Te{nPTqF`ObG{=bo84 z7wV|v|Hht3JPkk}FaUG|9Y7m!Ao=|p@Bx?yW`SwoCGazne{UQEt^-TJmSbB2t^w^e zR}HiRcY*JgVs~m~;0|!8R^p4mY6RiBtpaENr0^Uv>(vCT0r!Cspcg0r`;t~jo{azx zfb|`CCKF##(F8oL;_xjnl#FrV8%ADg6Xz30z8#UnEWQMc5jI;nU_9Z{{Eq{QD8L7t!&STXpy9ZFm@kj^1Fem7{l-O!eBYr8>b)d^b3W}9W~#A6U*g95Oi>^=c4 z)~Zv`8K;EXvhfK3pp)!7am&FTkR7(HNX8sFd;lS+^%_N}!U1DH{Ly|K9>B#6_hueBM`PvzgXPMrIx>vGBiU)6Z4 zEpn3sb9fkCLB-z-YcKpmwJ#|r9RWl zC5~dFfRx^~zcZo4lLpcS%$dU%U1Vo|3%C)3JcraOHF*iC9^?bK^S~v{rNu|U-T>|j zG$FN0?PUAC7)9=|wrEE3yiS#A@zILA+sGlcN|VDaIFIb+1>`8+h@2|>kwB-& z?yx%JM4NdN29eBAO@MFvNQ4*G7A;7*Pj7ZUZ3CJ0gS8T$MjKKwTeK9b&o&dl(GUSK zKeKitb( Date: Thu, 24 Mar 2022 20:25:34 -0500 Subject: [PATCH 63/73] Create README.md --- apps/gsat/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 apps/gsat/README.md diff --git a/apps/gsat/README.md b/apps/gsat/README.md new file mode 100644 index 000000000..faf986947 --- /dev/null +++ b/apps/gsat/README.md @@ -0,0 +1,3 @@ +# Geek Squad Appointment Timer + +An app dedicated to setting a 20 minute timer for Geek Squad Appointments. From b0f530107d33d849fe0f83b2e7fc0a0e9e7d11bf Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:26:21 -0500 Subject: [PATCH 64/73] Create ChangeLog --- apps/gsat/ChangeLog | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/gsat/ChangeLog diff --git a/apps/gsat/ChangeLog b/apps/gsat/ChangeLog new file mode 100644 index 000000000..48156d0d4 --- /dev/null +++ b/apps/gsat/ChangeLog @@ -0,0 +1 @@ +0.01: Added Source Code From 62b2d94e9363257c5c321ae7aa80da8c7dc62034 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:27:52 -0500 Subject: [PATCH 65/73] Update metadata.json --- apps/gsat/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gsat/metadata.json b/apps/gsat/metadata.json index cda1f7096..678e1eb19 100644 --- a/apps/gsat/metadata.json +++ b/apps/gsat/metadata.json @@ -9,6 +9,6 @@ "readme": "README.md", "supports": ["BANGLEJS"], "storage": [ - {"name":"gsat.app.js","url":"app.js"}, + {"name":"gsat.app.js","url":"app.js"} ] } From 8f3f229c5519812dbc0ccd2c7e0e9b6ab411d006 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:31:26 -0500 Subject: [PATCH 66/73] Create app-icon.js --- apps/gsat/app-icon.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 apps/gsat/app-icon.js diff --git a/apps/gsat/app-icon.js b/apps/gsat/app-icon.js new file mode 100644 index 000000000..06f93e2ef --- /dev/null +++ b/apps/gsat/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV")) From b50f0d7a565994688ced0d52b1ecb64b249e19a9 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:31:55 -0500 Subject: [PATCH 67/73] Update metadata.json --- apps/gsat/metadata.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/gsat/metadata.json b/apps/gsat/metadata.json index 678e1eb19..0e81c0513 100644 --- a/apps/gsat/metadata.json +++ b/apps/gsat/metadata.json @@ -9,6 +9,7 @@ "readme": "README.md", "supports": ["BANGLEJS"], "storage": [ - {"name":"gsat.app.js","url":"app.js"} + {"name":"gsat.app.js","url":"app.js"}, + {"name":"gsat.img","url":"app-icon.js","evaluate":true} ] } From 9af8213b92312c846ce4e521c51af803f280c2f0 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:38:20 -0500 Subject: [PATCH 68/73] Update README.md --- apps/gsat/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/gsat/README.md b/apps/gsat/README.md index faf986947..17880e287 100644 --- a/apps/gsat/README.md +++ b/apps/gsat/README.md @@ -1,3 +1,4 @@ # Geek Squad Appointment Timer An app dedicated to setting a 20 minute timer for Geek Squad Appointments. +![screenshot](https://user-images.githubusercontent.com/37220586/160037582-71b46f82-aa7c-47cf-a42a-5e4bb72705c4.png) From e7ee68d5642d6b8c0a7d06406328f2a8350eab2b Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:42:15 -0500 Subject: [PATCH 69/73] Update README.md --- apps/gsat/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/gsat/README.md b/apps/gsat/README.md index 17880e287..faf986947 100644 --- a/apps/gsat/README.md +++ b/apps/gsat/README.md @@ -1,4 +1,3 @@ # Geek Squad Appointment Timer An app dedicated to setting a 20 minute timer for Geek Squad Appointments. -![screenshot](https://user-images.githubusercontent.com/37220586/160037582-71b46f82-aa7c-47cf-a42a-5e4bb72705c4.png) From 451ecd7226339b676f954588703502a7617f9af2 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:42:36 -0500 Subject: [PATCH 70/73] Add files via upload --- apps/gsat/screenshot.png | Bin 0 -> 4250 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/gsat/screenshot.png diff --git a/apps/gsat/screenshot.png b/apps/gsat/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..032319bf674745ec454e9a16645aff39ba44d93f GIT binary patch literal 4250 zcmcgvc{r478-E)~$w2&EzMl74fA{bH-S>0P0&XZQC?N;{ zfUvRA2`d1Av~u1z8zG#QnA@-6oX;k-(P<0-h`i#y>tcTvkpzG(rp709t*_&z`kJD8 z%69O#xyNl_t>lp(3yAN__-LTUcZ+h}gV;{Iau3jbS2QX0+ea36#((DNnuGa5FgKP+ z2P*0QM-y8|!WgH866_izTAn*eFOcL?;!tee1kW$xT8BZ01q;v=S5-2(}=@ewRv$;=v0KZ2D~A!%RFGL^F6+@|D|6D zem7(SJzeUzJh(nFrTZl^%|`uLACy5Vp;hTl4=Y+aJ#SLc1g>FNF0PJ8nmtO=Jt zWd&R_(&PQ-VlYY(+C3w`nEg5IsJ@tdRkfg1w7bH2qVqgUDx1k#aJbE!@<{jz|9l;A z&R7yS_7L8vV_Kf#87gC0Y&9P~8TDw2x-#AtTcW|DG*qb$kjklhN4oN<OouWH0!;mRvv?SfSej4$9MND)~YZwD#&Y3`y@ELnL zhX58Q%??it&zC`8tm9#r{@H0914*R7Govzr+R3E}DswlW&eWql{%}q2 ztGM~njV1@TTeDStA;60?DV}>5Xqm{AyK*&4C^P?j@ySp&lH&EA z_y#$oLszg0wGA%vRdb0N9Ss4wdfjrQ$S*ZnvodT~g38T(f@z?C!AoQqUx$CB*pB7( zzcBOH^St3@ML~qt%RRJtra*k?UTd{dSlmot1ep2l>;)r$K3A*sV&B++(D};(iaVN` zZiD7NDD5F#JFl3_|#s@?rM#qn%BBBkMVnGOouFlZRkLf%hho^p6JgJdL3 zdD~@OKFHXumoE+i_+z$>PG_E^*Z3ZIf}3`^K7Q;-!xorg09)KCd5_&)5X?Aqfqm!G+x%S34wt)F-Ovw+nasXq-p)7*aZFV23>#r9p_pJ7 z`j#L$?4(*!vjt!0az3?Z2H}2nA!8=1Y{1-N@Jieh-X1Sn8Qq9eF`3Bn>(tT0 z%@g)a=hPf;zE;W*yXE+I`1C#8`~JRdt&I1ZfSdF+t_s_kioNgVGy;jM0aMR0S3C+j zMGq;0T#Py-93$7X`H00DzWilYl$jc6jWtLFf_HCUWaqaW+7BA~7)JLG8u$zaCmBPU zFFn5RG62 zsCVu=p4k;Q#G^)s@mUP<%vSQyc|<-$lMpR;oR5RL0&glv5FmwYK}>IszxqMu$Nkj9CJ zL_w?uF8H`yiRU}hS0@LaK{2*-!q5F)@}#uaC4)}45KoT*86$86X%I(<2w0D-EWBdk zDS1LM~N$;SCO2`rO(TmvMqNsXc`gHPupnIMzJ+ zpBj@(#pMrHgv&56i<6UbxXH|r8R9jygZCs9lrHb|+p*d=9~-U6-YnsM&O4^ew( zlgbr|T@MmU8@)g!g5W3JhSvFYU+}|^^&tqy3a;4D*gdLxZd2~z0A5Dnh9V$@nVZYT zJ`caPuZkCNCj0pZuPIIMOb@O;4Z;NIwWayo7*C%y1UI6KGSY&pmBD2H$d@;p_D^u_ hYr6cOT2SsQkk>x|=EPt9I9VS6jP>Csa*v~K{RJk1|9$`f literal 0 HcmV?d00001 From 9eff57362b1ff350b4bb320ae2f406c8c887ab76 Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:43:08 -0500 Subject: [PATCH 71/73] Update metadata.json --- apps/gsat/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/gsat/metadata.json b/apps/gsat/metadata.json index 0e81c0513..bdc8fee32 100644 --- a/apps/gsat/metadata.json +++ b/apps/gsat/metadata.json @@ -8,6 +8,7 @@ "tags": "tool", "readme": "README.md", "supports": ["BANGLEJS"], + "screenshots": [{"url":"screenshot.png"}] "storage": [ {"name":"gsat.app.js","url":"app.js"}, {"name":"gsat.img","url":"app-icon.js","evaluate":true} From db88e1800d1dec7d707d5a72697bc85d214ec4eb Mon Sep 17 00:00:00 2001 From: Smooklu <37220586+Smooklu@users.noreply.github.com> Date: Thu, 24 Mar 2022 20:44:12 -0500 Subject: [PATCH 72/73] Update metadata.json --- apps/gsat/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gsat/metadata.json b/apps/gsat/metadata.json index bdc8fee32..878d213e4 100644 --- a/apps/gsat/metadata.json +++ b/apps/gsat/metadata.json @@ -8,7 +8,7 @@ "tags": "tool", "readme": "README.md", "supports": ["BANGLEJS"], - "screenshots": [{"url":"screenshot.png"}] + "screenshots": [{"url":"screenshot.png"}], "storage": [ {"name":"gsat.app.js","url":"app.js"}, {"name":"gsat.img","url":"app-icon.js","evaluate":true} From 9a0c31c56d41912074ba41ca034baeb7cdf17b12 Mon Sep 17 00:00:00 2001 From: Salim Blume Date: Thu, 24 Mar 2022 22:19:11 -0500 Subject: [PATCH 73/73] Fix run notifications bugs and improve accuracy --- apps/recorder/ChangeLog | 1 + apps/recorder/metadata.json | 2 +- apps/recorder/widget.js | 3 +-- apps/run/ChangeLog | 1 + apps/run/app.js | 4 ++-- apps/run/metadata.json | 2 +- apps/run/settings.js | 12 ++++++------ modules/exstats.js | 12 +++++++----- 8 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 90937e160..e9877808c 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -17,3 +17,4 @@ 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 +0.14: Remove unneeded variable assignment \ No newline at end of file diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index e2400603d..d715af38d 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.13", + "version": "0.14", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 221bc6c1a..4a105754b 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -248,7 +248,7 @@ } 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=>{ + return 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(); @@ -259,7 +259,6 @@ } return WIDGETS["recorder"].setRecording(1); }); - return prompt; } settings.recording = isOn; updateSettings(settings); diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index 401a68de9..46fdb7e7e 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -9,3 +9,4 @@ 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) +0.11: Notifications fixes \ No newline at end of file diff --git a/apps/run/app.js b/apps/run/app.js index d066c8b1f..fb8158e58 100644 --- a/apps/run/app.js +++ b/apps/run/app.js @@ -59,7 +59,7 @@ function onStartStop() { layout.render(); }) ); - } else { + } else if (!settings.record && WIDGETS["recorder"]) { prepPromises.push( WIDGETS["recorder"].setRecording(false) ); @@ -124,7 +124,7 @@ function configureNotification(stat) { } Object.keys(settings.notify).forEach((statType) => { - if (settings.notify[statType].increment > 0) { + if (settings.notify[statType].increment > 0 && exs.stats[statType]) { configureNotification(exs.stats[statType]); } }); diff --git a/apps/run/metadata.json b/apps/run/metadata.json index 51239d297..09e5a3bed 100644 --- a/apps/run/metadata.json +++ b/apps/run/metadata.json @@ -1,6 +1,6 @@ { "id": "run", "name": "Run", - "version":"0.10", + "version":"0.11", "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 949f7a235..6a7d169c4 100644 --- a/apps/run/settings.js +++ b/apps/run/settings.js @@ -90,8 +90,8 @@ [[300, 1],[300, 0],[300, 1],[300, 0],[300, 1]], ]; 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, + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), + min: 0, max: vibTimes.length, format: v => vibPatterns[v]||/*LANG*/"Off", onchange: v => { settings.notify.dist.notifications = vibTimes[v]; @@ -100,8 +100,8 @@ } } 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, + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), + min: 0, max: vibTimes.length, format: v => vibPatterns[v]||/*LANG*/"Off", onchange: v => { settings.notify.step.notifications = vibTimes[v]; @@ -110,8 +110,8 @@ } } 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, + value: Math.max(0,vibTimes.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), + min: 0, max: vibTimes.length, format: v => vibPatterns[v]||/*LANG*/"Off", onchange: v => { settings.notify.time.notifications = vibTimes[v]; diff --git a/modules/exstats.js b/modules/exstats.js index b106622d0..ec0a838a7 100644 --- a/modules/exstats.js +++ b/modules/exstats.js @@ -139,9 +139,9 @@ Bangle.on("GPS", function(fix) { if (stats["pacea"]) stats["pacea"].emit("changed",stats["pacea"]); if (stats["pacec"]) stats["pacec"].emit("changed",stats["pacec"]); if (stats["speed"]) stats["speed"].emit("changed",stats["speed"]); - if (state.notify.dist.increment > 0 && state.notify.dist.next <= stats["dist"]) { + if (state.notify.dist.increment > 0 && state.notify.dist.next <= state.distance) { stats["dist"].emit("notify",stats["dist"]); - state.notify.dist.next = stats["dist"] + state.notify.dist.increment; + state.notify.dist.next = state.notify.dist.next + state.notify.dist.increment; } }); @@ -152,7 +152,7 @@ Bangle.on("step", function(steps) { state.lastStepCount = steps; if (state.notify.step.increment > 0 && state.notify.step.next <= steps) { stats["step"].emit("notify",stats["step"]); - state.notify.step.next = steps + state.notify.step.increment; + state.notify.step.next = state.notify.step.next + state.notify.step.increment; } }); Bangle.on("HRM", function(h) { @@ -285,7 +285,7 @@ exports.getStats = function(statIDs, options) { } if (state.notify.time.increment > 0 && state.notify.time.next <= now) { stats["time"].emit("notify",stats["time"]); - state.notify.time.next = now + state.notify.time.increment; + state.notify.time.next = state.notify.time.next + state.notify.time.increment; } }, 1000); function reset() { @@ -299,6 +299,8 @@ exports.getStats = function(statIDs, options) { state.curSpeed = 0; state.BPM = 0; state.BPMage = 0; + state.thisGPS = {}; + state.lastGPS = {}; state.notify = options.notify; if (options.notify.dist.increment > 0) { state.notify.dist.next = state.distance + options.notify.dist.increment; @@ -338,7 +340,7 @@ exports.appendMenuItems = function(menu, settings, saveSettings) { } exports.appendNotifyMenuItems = function(menu, settings, saveSettings) { var distNames = ['Off', "1000m","1 mile","1/2 Mthn", "Marathon",]; - var distAmts = [0, 1000,1609,21098,42195]; + var distAmts = [0, 1000, 1609, 21098, 42195]; menu['Ntfy Dist'] = { min: 0, max: distNames.length-1, value: Math.max(distAmts.indexOf(settings.notify.dist.increment),0),