From 3adb3f1c2b28bab8a42e9cf5c92c179e9e153439 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Sun, 5 Dec 2021 19:27:50 +0800 Subject: [PATCH 001/250] Make main digits height configurable --- apps/authentiwatch/app.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index c0cb608c0..392ed8940 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -1,4 +1,5 @@ -const tokenentryheight = 46; +const tokenextraheight = 16; +var tokendigitsheight = 30; // Hash functions const crypto = require("crypto"); const algos = { @@ -124,14 +125,14 @@ function drawToken(id, r) { // current token g.setColor(g.theme.fgH); g.setBgColor(g.theme.bgH); - g.setFont("Vector", 16); + g.setFont("Vector", tokenextraheight); // center just below top line g.setFontAlign(0, -1, 0); adj = y1; } else { g.setColor(g.theme.fg); g.setBgColor(g.theme.bg); - g.setFont("Vector", 30); + g.setFont("Vector", tokendigitsheight); // center in box g.setFontAlign(0, 0, 0); adj = (y1 + y2) / 2; @@ -148,14 +149,14 @@ function drawToken(id, r) { // counter - draw triangle as swipe hint let yc = (y1 + y2) / 2; g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]); - adj = 10; + adj = 12; } // digits just below label - sz = 30; + sz = tokendigitsheight; do { g.setFont("Vector", sz--); } while (g.stringWidth(state.otp) > (r.w - adj)); - g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false); + g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false); } // shaded lines top and bottom g.setColor(0.5, 0.5, 0.5); @@ -196,15 +197,15 @@ function draw() { } if (tokens.length > 0) { var drewcur = false; - var id = Math.floor(state.listy / tokenentryheight); - var y = id * tokenentryheight + Bangle.appRect.y - state.listy; + var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight)); + var y = id * (tokendigitsheight + tokenextraheight) + 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:tokenentryheight}); + drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)}); if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) { drewcur = true; } id += 1; - y += tokenentryheight; + y += (tokendigitsheight + tokenextraheight); } if (drewcur) { // the current token has been drawn - schedule a redraw @@ -226,7 +227,7 @@ function draw() { state.nexttime = 0; } } else { - g.setFont("Vector", 30); + g.setFont("Vector", tokendigitsheight); g.setFontAlign(0, 0, 0); g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } @@ -238,18 +239,18 @@ function draw() { function onTouch(zone, e) { if (e) { - var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight); + var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight)); if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) { id = -1; } if (state.curtoken != id) { if (id != -1) { - var y = id * tokenentryheight - state.listy; + var y = id * (tokendigitsheight + tokenextraheight) - state.listy; if (y < 0) { state.listy += y; y = 0; } - y += tokenentryheight; + y += (tokendigitsheight + tokenextraheight); if (y > Bangle.appRect.h) { state.listy += (y - Bangle.appRect.h); } @@ -266,7 +267,7 @@ 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 * tokenentryheight - Bangle.appRect.h); + var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h); state.listy = Math.max(0, newy); draw(); } @@ -296,7 +297,7 @@ function bangle1Btn(e) { state.curtoken = Math.max(state.curtoken, 0); state.curtoken = Math.min(state.curtoken, tokens.length - 1); var fakee = {}; - fakee.y = state.curtoken * tokenentryheight - state.listy + Bangle.appRect.y; + fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y; state.curtoken = -1; state.nextTime = 0; onTouch(0, fakee); From d9cb12bdf15f203052e3dd8eac5bdde9a44608c0 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Sun, 5 Dec 2021 19:42:44 +0800 Subject: [PATCH 002/250] Shrink label to fit --- apps/authentiwatch/app.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 392ed8940..bb797edc4 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -118,9 +118,10 @@ function drawToken(id, r) { var y1 = r.y; var x2 = r.x + r.w - 1; var y2 = r.y + r.h - 1; - var adj, sz; + var adj, lbl, sz; 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); @@ -132,13 +133,16 @@ function drawToken(id, r) { } else { g.setColor(g.theme.fg); g.setBgColor(g.theme.bg); - g.setFont("Vector", tokendigitsheight); + sz = tokendigitsheight; + do { + g.setFont("Vector", sz--); + } while (g.stringWidth(lbl) > r.w); // center in box g.setFontAlign(0, 0, 0); adj = (y1 + y2) / 2; } g.clearRect(x1, y1, x2, y2); - g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false); + g.drawString(lbl, (x1 + x2) / 2, adj, false); if (id == state.curtoken) { if (tokens[id].period > 0) { // timed - draw progress bar From 26079db909677e565a3718daae6ea7a181190048 Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Fri, 31 Dec 2021 11:39:22 +0800 Subject: [PATCH 003/250] Update app.js Restore swipe to exit. --- apps/authentiwatch/app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index bb797edc4..de3d02163 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -277,6 +277,9 @@ function onDrag(e) { } function onSwipe(e) { + if (e == 1) { + Bangle.showLauncher(); + } if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { tokens[state.curtoken].period--; let newsettings={tokens:tokens,misc:settings.misc}; From d1e7a7a220626070f7e9f1a29211dec9bd9bec6c Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Fri, 31 Dec 2021 12:00:11 +0800 Subject: [PATCH 004/250] Update app.js Fix extra zeros out of b32decode() --- apps/authentiwatch/app.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index de3d02163..d2a72c8a8 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -45,9 +45,6 @@ function b32decode(seedstr) { } } } - if (bitcount > 0) { - retstr += String.fromCharCode(buf << (8 - bitcount)); - } var retbuf = new Uint8Array(retstr.length); for (i in retstr) { retbuf[i] = retstr.charCodeAt(i); From 7b6386ce5d0860ed81deed91a1c2a4304cb6e00b Mon Sep 17 00:00:00 2001 From: Andrew Gregory Date: Fri, 31 Dec 2021 15:56:39 +0800 Subject: [PATCH 005/250] Update app.js Use exitApp() --- apps/authentiwatch/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index d2a72c8a8..640183230 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -275,7 +275,7 @@ function onDrag(e) { function onSwipe(e) { if (e == 1) { - Bangle.showLauncher(); + exitApp(); } if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { tokens[state.curtoken].period--; From 97696f7928f9820eff469260c6560ec6957ffd85 Mon Sep 17 00:00:00 2001 From: Joseph Moroney Date: Thu, 6 Jan 2022 17:29:22 +1000 Subject: [PATCH 006/250] sonicclk minor - add settings to control twist/lcd --- apps.json | 2 +- apps/sonicclk/Changelog | 3 +- apps/sonicclk/README.md | 9 +++++- apps/sonicclk/app.js | 71 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/apps.json b/apps.json index ebafa9c97..ffc76f746 100644 --- a/apps.json +++ b/apps.json @@ -5381,7 +5381,7 @@ { "id": "sonicclk", "name": "Sonic Clock", - "version": "1.01", + "version": "1.10", "description": "A classic sonic clock featuring run, stop and wait animations.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog index 7c83f6988..4f597b627 100644 --- a/apps/sonicclk/Changelog +++ b/apps/sonicclk/Changelog @@ -1,2 +1,3 @@ 1.00 Added sonic clock app -1.01 Fixed text alignment issue; Increased acceleration required to activate twist; \ No newline at end of file +1.01 Fixed text alignment issue; Increased acceleration required to activate twist; +1.10 Added settings menu to control twist threshold and LCD Activity \ No newline at end of file diff --git a/apps/sonicclk/README.md b/apps/sonicclk/README.md index a381e0a07..adfbf47b4 100644 --- a/apps/sonicclk/README.md +++ b/apps/sonicclk/README.md @@ -8,6 +8,13 @@ A classic sonic clock featuring run, stop and wait animations. - Sonic will run when the screen is unlocked - Sonic will stop when the screen is locked -- Sonic will wait when looking at your watch face (when `Bangle.on("twist", fn)` is fired). +- Sonic will wait when looking at your watch face (when `Bangle.on("twist", fn)` is fired). This option is configurable (see below). + +## Configuration + +To access the settings menu, you can **double tap** on the **right** side of the watch. The following options are configurable: + +- `Active Mode` - catering for 'active' behaviour where the `twist` method can be fired undesirably. When `on` this will prevent the LCD from turning on when a `twist` event is fired. +- `Twist Thresh` - customise the acceleration needed to activate the twist method (see the [`Bangle.setOptions`](https://www.espruino.com/Reference#:~:text=twisted%3F%20default%20%3D%20true-,twistThreshold,-How%20much%20acceleration) method for more info). ### Made with love by [Joseph](https://github.com/Johoseph) 🤗 diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js index 296677281..c92818b01 100644 --- a/apps/sonicclk/app.js +++ b/apps/sonicclk/app.js @@ -109,10 +109,14 @@ let currentSonic = -1; let drawTimeout, drawInterval, waitTimeout; let bgScroll = [0, null]; -const start = () => { +const fullReset = () => { if (drawTimeout) clearTimeout(drawTimeout); if (waitTimeout) clearTimeout(waitTimeout); if (drawInterval) clearInterval(drawInterval); +} + +const start = () => { + fullReset(); drawInterval = setInterval(() => { draw("start"); @@ -252,22 +256,75 @@ const draw = (action) => { if (action === "reset") queueDraw(); }; +// Settings +const settings = require("Storage").readJSON("sonicclk-settings") || { + activeMode: false, + twistThreshold: 1600, +}; +let isSettings = false; + +const settingsMenu = { + "": { "title": "Settings" }, + "Active Mode": { + value: settings.activeMode, + format: v => v ? "On" : "Off", + onchange: v => settings.activeMode = v, + }, + "Twist Thresh" : { + value: settings.twistThreshold, + min: 800, max: 4000, step: 200, + onchange: v => settings.twistThreshold = v, + }, + "Exit" : () => { + isSettings = false; + + require("Storage").writeJSON("sonicclk-settings", settings); + Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 12000, + twistThreshold: settings.twistThreshold, + }); + + E.showMenu(); + draw("reset"); + start(); + } +}; + g.setTheme({ bg: "#0099ff", fg: "#fff", dark: true }).clear(); Bangle.on("lock", (locked) => { - if (locked) { - stop(); - } else { - start(); + if (!isSettings) { + if (locked) { + stop(); + } else { + start(); + } } }); -Bangle.on("twist", () => wait()); +Bangle.on("twist", () => { + if (settings.activeMode) { + fullReset(); + draw("reset"); + } else { + wait(); + } +}); + +Bangle.on("tap", (d) => { + if (d.double && d.dir === "right") { + fullReset(); + isSettings = true; + Bangle.setLocked(false); + E.showMenu(settingsMenu); + } +}); Bangle.setOptions({ lockTimeout: 10000, backlightTimeout: 12000, - twistThreshold: 1600, + twistThreshold: settings.twistThreshold, }); Bangle.setUI("clock"); From d9cd94226e84a07e4cd4219dbd6f40b07195f798 Mon Sep 17 00:00:00 2001 From: Joseph Moroney Date: Thu, 6 Jan 2022 17:38:41 +1000 Subject: [PATCH 007/250] Missing semi-colon --- apps/sonicclk/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js index c92818b01..b12af48fd 100644 --- a/apps/sonicclk/app.js +++ b/apps/sonicclk/app.js @@ -113,7 +113,7 @@ const fullReset = () => { if (drawTimeout) clearTimeout(drawTimeout); if (waitTimeout) clearTimeout(waitTimeout); if (drawInterval) clearInterval(drawInterval); -} +}; const start = () => { fullReset(); From 73f9511128b709b5a465c280498d29d39c687124 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Thu, 27 Jan 2022 20:18:57 +0100 Subject: [PATCH 008/250] Fix sunprogress calculation during night --- apps/circlesclock/app.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 5b7569d63..7c9645159 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -504,12 +504,12 @@ function getSunProgress() { } } else { // during night - if (sunSet < sunRise) { - const upcomingSunRise = sunRise + 60 * 60 * 24; - return 1 - (upcomingSunRise - now) / (upcomingSunRise - sunSet); + if (now < sunRise) { + const prevSunSet = sunSet - 60 * 60 * 24; + return 1- (sunRise - now) / (sunRise - prevSunSet); } else { - const lastSunSet = sunSet - 60 * 60 * 24; - return (now - lastSunSet) / (sunRise - lastSunSet); + const upcomingSunRise = sunRise + 60 * 60 * 24; + return (upcomingSunRise - now) / (upcomingSunRise - sunSet); } } } From d7ecbc865bcd9ad683362629621a17ada6259dcb Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Thu, 27 Jan 2022 20:20:01 +0100 Subject: [PATCH 009/250] Fix icon position if four circles are used --- apps/circlesclock/app.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 7c9645159..50a4c0095 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -100,6 +100,7 @@ const radiusOuter = circleCount == 3 ? 25 : 20; const radiusInner = circleCount == 3 ? 20 : 15; const circleFont = circleCount == 3 ? "Vector:15" : "Vector:12"; const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:13"; +const iconOffset = circleCount == 3 ? 6 : 8; const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; @@ -226,7 +227,7 @@ function drawSteps(w) { writeCircleText(w, shortValue(steps)); - g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6); + g.drawImage(shoesIcon, w - iconOffset, h3 + radiusOuter - iconOffset); } function drawStepsDistance(w) { @@ -248,7 +249,7 @@ function drawStepsDistance(w) { writeCircleText(w, shortValue(stepsDistance)); - g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6); + g.drawImage(shoesIconGreen, w - iconOffset, h3 + radiusOuter - iconOffset); } function drawHeartRate(w) { @@ -267,7 +268,7 @@ function drawHeartRate(w) { writeCircleText(w, hrtValue != undefined ? hrtValue : "-"); - g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6); + g.drawImage(heartIcon, w - iconOffset, h3 + radiusOuter - iconOffset); } function drawBattery(w) { @@ -296,7 +297,7 @@ function drawBattery(w) { } writeCircleText(w, battery + '%'); - g.drawImage(icon, w - 6, h3 + radiusOuter - 6); + g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); } function drawWeather(w) { @@ -337,7 +338,7 @@ function drawWeather(w) { if (code > 0) { const icon = getWeatherIconByCode(code); - if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10); + if (icon) g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); } else { g.drawString("?", w, h3 + radiusOuter); } @@ -389,7 +390,7 @@ function drawSunProgress(w) { writeCircleText(w, text); - g.drawImage(icon, w - 6, h3 + radiusOuter - 6); + g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); } From 546c8efa58a16e41a73561c36581da1bae0d7072 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Thu, 27 Jan 2022 21:36:25 +0100 Subject: [PATCH 010/250] Inital support for temperature and pressure from internal watch sensor --- apps/circlesclock/README.md | 5 +++ apps/circlesclock/app.js | 77 ++++++++++++++++++++++++++++++++++- apps/circlesclock/settings.js | 12 +++--- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 242adcf4b..869bc238a 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -14,6 +14,8 @@ It can show the following information (this can be configured): * Temperature inside circle * Condition as icon below circle * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) + * Temperature from internal watch sensor + * Air pressure from internal watch sensor ## Screenshots ![Screenshot dark theme](screenshot-dark.png) @@ -21,6 +23,9 @@ It can show the following information (this can be configured): ![Screenshot dark theme with four circles](screenshot-dark-4.png) ![Screenshot light theme with four circles](screenshot-light-4.png) +## Ideas +* Make colors configurable + ## Creator Marco ([myxor](https://github.com/myxor)) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 50a4c0095..14efd4539 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -149,6 +149,7 @@ function drawCircle(index) { let type = settings['circle' + index]; if (!type) type = defaultCircleTypes[index - 1]; const w = getCirclePosition(type); + switch (type) { case "steps": drawSteps(w); @@ -169,6 +170,12 @@ function drawCircle(index) { case "sunProgress": drawSunProgress(w); break; + case "temperature": + drawTemperature(w); + break; + case "pressure": + drawPressure(w); + break; case "empty": // we draw nothing here return; @@ -391,7 +398,55 @@ function drawSunProgress(w) { writeCircleText(w, text); g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); +} +function drawTemperature(w) { + if (!w) w = getCirclePosition("temperature"); + getPressureValue("temperature").then((temperature) => { + + drawCircleBackground(w); + + if (temperature && temperature > 0) { + const percent = temperature / 100; // TODO: find good max for temperature + drawGauge(w, h3, percent, colorGreen); + } + + drawInnerCircleAndTriangle(w); + + let icon = powerIcon; + let color = colorFg; + if (temperature && temperature > 0) + writeCircleText(w, locale.temp(temperature)); + + g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + + }); +} + +function drawPressure(w) { + if (!w) w = getCirclePosition("pressure"); + getPressureValue("pressure").then((pressure) => { + + drawCircleBackground(w); + + if (pressure && pressure > 0) { + + const minPressure = 870; + const maxPressure = 1080; + const percent = (pressure - minPressure) / (maxPressure - minPressure); + drawGauge(w, h3, percent, colorGreen); + } + + drawInnerCircleAndTriangle(w); + + let icon = powerIcon; + let color = colorFg; + if (pressure && pressure > 0) + writeCircleText(w, pressure); + + g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + + }); } /* @@ -479,7 +534,7 @@ function formatSeconds(s) { function getSunData() { if (location != undefined && location.lat != undefined) { // get today's sunlight times for lat/lon - return SunCalc.getTimes(new Date(), location.lat, location.lon); + return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined; } return undefined; } @@ -507,7 +562,7 @@ function getSunProgress() { // during night if (now < sunRise) { const prevSunSet = sunSet - 60 * 60 * 24; - return 1- (sunRise - now) / (sunRise - prevSunSet); + return 1 - (sunRise - now) / (sunRise - prevSunSet); } else { const upcomingSunRise = sunRise + 60 * 60 * 24; return (upcomingSunRise - now) / (upcomingSunRise - sunSet); @@ -608,6 +663,24 @@ function enableHRMSensor() { } } +function getPressureValue(type) { + return new Promise((resolve) => { + if (Bangle.getPressure) { + Bangle.getPressure().then(function(d) { + if (d) { + resolve(d[type]); + } + }).catch(function(e) {}); + } else { + switch (type) { + case "temperature": + resolve(E.getTemperature()); + break; + } + } + }); +} + Bangle.on('lock', function(isLocked) { if (!isLocked) { if (isCircleEnabled("hr")) { diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 1c072fc90..5cb2e9bfc 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -7,8 +7,8 @@ storage.write(SETTINGS_FILE, settings); } - const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty"]; - const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty"]; + const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty", "temperature", "pressure"]; + const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty", "temperature", "pressure"]; const weatherData = ["humidity", "wind", "empty"]; @@ -105,25 +105,25 @@ }, 'circle1': { value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0, - min: 0, max: 6, + min: 0, max: valuesCircleTypes.length, format: v => namesCircleTypes[v], onchange: x => save('circle1', valuesCircleTypes[x]), }, 'circle2': { value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2, - min: 0, max: 6, + min: 0, max: valuesCircleTypes.length, format: v => namesCircleTypes[v], onchange: x => save('circle2', valuesCircleTypes[x]), }, 'circle3': { value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3, - min: 0, max: 6, + min: 0, max: valuesCircleTypes.length, format: v => namesCircleTypes[v], onchange: x => save('circle3', valuesCircleTypes[x]), }, 'circle4': { value: settings.circle4 ? valuesCircleTypes.indexOf(settings.circle4) : 4, - min: 0, max: 6, + min: 0, max: valuesCircleTypes.length, format: v => namesCircleTypes[v], onchange: x => save('circle4', valuesCircleTypes[x]), } From b930a8d2afbbff8f549e68291e7015d3b09280ab Mon Sep 17 00:00:00 2001 From: Hilmar Strauch <56518493+HilmarSt@users.noreply.github.com> Date: Thu, 27 Jan 2022 21:38:57 +0100 Subject: [PATCH 011/250] Update ChangeLog --- apps/vectorclock/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/vectorclock/ChangeLog b/apps/vectorclock/ChangeLog index 6ba096f88..f1fd5b134 100644 --- a/apps/vectorclock/ChangeLog +++ b/apps/vectorclock/ChangeLog @@ -2,3 +2,4 @@ 0.02: Use Bangle.setUI for button/launcher handling 0.03: Bangle.js 2 support 0.04: Adds costumizable colours and the respective settings menu +0.05: "Chime the time" (buzz or beep) with up/down swipe added From 22c30219a138863484486ca7c0a90f89f59245fc Mon Sep 17 00:00:00 2001 From: Hilmar Strauch <56518493+HilmarSt@users.noreply.github.com> Date: Thu, 27 Jan 2022 21:51:06 +0100 Subject: [PATCH 012/250] Update metadata.json --- apps/vectorclock/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vectorclock/metadata.json b/apps/vectorclock/metadata.json index 3da93ccad..63d2ca678 100644 --- a/apps/vectorclock/metadata.json +++ b/apps/vectorclock/metadata.json @@ -1,7 +1,7 @@ { "id": "vectorclock", "name": "Vector Clock", - "version": "0.04", + "version": "0.05", "description": "A digital clock that uses the built-in vector font.", "icon": "app.png", "type": "clock", From 6718b4317f65c5c28468b978e62819f46efe45bb Mon Sep 17 00:00:00 2001 From: Hilmar Strauch <56518493+HilmarSt@users.noreply.github.com> Date: Thu, 27 Jan 2022 21:55:01 +0100 Subject: [PATCH 013/250] Update settings.js --- apps/vectorclock/settings.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/vectorclock/settings.js b/apps/vectorclock/settings.js index a6f4dd807..86cbad1e5 100644 --- a/apps/vectorclock/settings.js +++ b/apps/vectorclock/settings.js @@ -10,10 +10,10 @@ var colnames = ["white", "yellow", "green", "cyan", "red", "orange", "magenta", "black"]; var colvalues = [0xFFFF, 0xFFE0, 0x07E0, 0x07FF, 0xF800, 0xFD20, 0xF81F, 0x0000]; + var chimenames = ["off", "buzz", "beep"]; // Show the menu E.showMenu({ - "" : { "title" : "VectorClock colours" }, - "< Back" : () => back(), + "" : { "title" : "VectorClock settings" }, 'Time': { value: Math.max(0 | colvalues.indexOf(settings.timecol),0), min: 0, max: colvalues.length-1, @@ -41,5 +41,15 @@ writeSettings(); } }, + 'Chimetype': { + value: Math.max(0 | chimenames.indexOf(settings.chimetype),0), + min: 0, max: chimenames.length-1, + format: v => chimenames[v], + onchange: v => { + settings.chimetype = chimenames[v]; + writeSettings(); + } + }, + "< Back" : () => back(), }); }) From 38ea17592a5d3ee4d761e61877156631c408fbcb Mon Sep 17 00:00:00 2001 From: Hilmar Strauch <56518493+HilmarSt@users.noreply.github.com> Date: Thu, 27 Jan 2022 21:58:24 +0100 Subject: [PATCH 014/250] Update app.js --- apps/vectorclock/app.js | 73 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/apps/vectorclock/app.js b/apps/vectorclock/app.js index e2e4c1e64..e99a13ed1 100644 --- a/apps/vectorclock/app.js +++ b/apps/vectorclock/app.js @@ -4,7 +4,7 @@ var settings = require('Storage').readJSON("vectorclock.json", true) || {}; var dowcol = settings.dowcol || g.theme.fg; var timecol = settings.timecol || g.theme.fg; var datecol = settings.datecol || g.theme.fg; - +var chimetype = settings.chimetype; function padNum(n, l) { return ("0".repeat(l)+n).substr(-l); @@ -44,7 +44,7 @@ function drawVectorText(text, size, x, y, alignX, alignY, color) { function draw() { g.reset(); - + let d = new Date(); let hours = is12Hour ? ((d.getHours() + 11) % 12) + 1 : d.getHours(); let timeText = `${hours}:${padNum(d.getMinutes(), 2)}`; @@ -58,27 +58,27 @@ function draw() { let timeFontSize = width / ((g.stringWidth(timeText) / 256) + (Math.max(g.stringWidth(meridian), g.stringWidth(secondsText)) / 512 * 9 / 10)); let dowFontSize = width / (g.stringWidth(dowText) / 256); let dateFontSize = width / (g.stringWidth(dateText) / 256); - + let timeHeight = g.setFont("Vector", timeFontSize).getFontHeight() * 9 / 10; let dowHeight = g.setFont("Vector", dowFontSize).getFontHeight(); let dateHeight = g.setFont("Vector", dateFontSize).getFontHeight(); - + let remainingHeight = g.getHeight() - 24 - timeHeight - dowHeight - dateHeight; let spacer = remainingHeight / 4; - + let x = 2; let y = 24 + spacer; - + pushCommand(drawVectorText, timeText, timeFontSize, x, y, -1, -1, timecol); pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1, timecol); if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1, timecol); y += timeHeight + spacer; - + pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1, dowcol); y += dowHeight + spacer; - + pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1, datecol); - + executeCommands(); } @@ -90,6 +90,57 @@ function tick() { timeout = setTimeout(tick, period - getTime() * 1000 % period); } +// ====================================== Vibration (taken from "Vibrate Clock") +// vibrate 0..9 +function vibrateDigitBuzz(num) { + if (num==0) return Bangle.buzz(500); + return new Promise(function f(resolve){ + if (num--<=0) return resolve(); + Bangle.buzz(100).then(()=>{ + setTimeout(()=>f(resolve), 200); + }); + }); +} +function vibrateDigitBeep(num) { + if (num==0) return Bangle.beep(500); + return new Promise(function f(resolve){ + if (num--<=0) return resolve(); + Bangle.beep(100).then(()=>{ + setTimeout(()=>f(resolve), 200); + }); + }); +} +// vibrate multiple digits (num must be a string) +function vibrateNumber(num) { + return new Promise(function f(resolve){ + if (!num.length) return resolve(); + var digit = num[0]; + num = num.substr(1); + if ("buzz"==chimetype) + vibrateDigitBuzz(digit).then(()=>{ + setTimeout(()=>f(resolve),500);}); + if ("beep"==chimetype) + vibrateDigitBeep(digit).then(()=>{ + setTimeout(()=>f(resolve),500);}); + }); +} +var vibrateBusy; +function vibrateTime() { + if (vibrateBusy) return; + vibrateBusy = true; + var d = new Date(); + var hours = d.getHours(), minutes = d.getMinutes(); + if (is12Hour) { + if (hours == 0) hours = 12; + else if (hours>12) hours -= 12; + } + vibrateNumber(hours.toString()). + then(() => new Promise(resolve=>setTimeout(resolve,500))). + then(() => vibrateNumber(minutes.toString())). + then(() => vibrateBusy=false); +} + + Bangle.on('lcdPower', function(on) { if (timeout) clearTimeout(timeout); timeout = null; @@ -103,6 +154,10 @@ Bangle.on('lock', function(locked) { tick(); }); +if ("buzz"==chimetype || "beep"==chimetype) + Bangle.on("swipe",function(direction){ + if (0==direction) vibrateTime();}); // 0 = swipe up/down + g.clear(); tick(); Bangle.loadWidgets(); From 9896b85e16485ea433a23ab72ee8e9ce428c0342 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Thu, 27 Jan 2022 22:20:32 +0100 Subject: [PATCH 015/250] Further support for temperature, air pressure or altitude from internal pressure sensor --- apps/circlesclock/ChangeLog | 1 + apps/circlesclock/README.md | 3 +- apps/circlesclock/app.js | 139 ++++++++++++++++++++++++---------- apps/circlesclock/settings.js | 4 +- 4 files changed, 104 insertions(+), 43 deletions(-) diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index 4a23c944f..fe3fd23fd 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -13,3 +13,4 @@ Load daily steps from Bangle health if available 0.07: Allow configuration of minimal heart rate confidence 0.08: Allow configuration of up to 4 circles in a row +0.09: Support to show temperature, air pressure or altitude from internal pressure sensor diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 869bc238a..a3b713d76 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -14,8 +14,7 @@ It can show the following information (this can be configured): * Temperature inside circle * Condition as icon below circle * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) - * Temperature from internal watch sensor - * Air pressure from internal watch sensor + * Temperature, air pressure or altitude from internal pressure sensor ## Screenshots ![Screenshot dark theme](screenshot-dark.png) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 14efd4539..8f223978f 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -9,6 +9,7 @@ const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIF const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI")); const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI")); const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA")); +const themperatureIcon = heatshrink.decompress(atob("h0OwIFChkAvEBkEHwEeAwXgh+An8A/gGBgYWCA==")); const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA")); const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA=")); @@ -176,6 +177,9 @@ function drawCircle(index) { case "pressure": drawPressure(w); break; + case "altitude": + drawAltitude(w); + break; case "empty": // we draw nothing here return; @@ -402,51 +406,103 @@ function drawSunProgress(w) { function drawTemperature(w) { if (!w) w = getCirclePosition("temperature"); - getPressureValue("temperature").then((temperature) => { - drawCircleBackground(w); + drawCircleBackground(w); + g.setColor(colorFg); - if (temperature && temperature > 0) { - const percent = temperature / 100; // TODO: find good max for temperature - drawGauge(w, h3, percent, colorGreen); - } + const delay = pressureLocked ? 1000 : 0; + setTimeout(() => { + getPressureValue("temperature").then((temperature) => { - drawInnerCircleAndTriangle(w); + pressureLocked = false; - let icon = powerIcon; - let color = colorFg; - if (temperature && temperature > 0) - writeCircleText(w, locale.temp(temperature)); + if (temperature) { + const percent = temperature / 100; // TODO: find good max for temperature + drawGauge(w, h3, percent, colorGreen); + } - g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + drawInnerCircleAndTriangle(w); - }); + if (temperature) + writeCircleText(w, locale.temp(temperature)); + + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + + }).catch(() => { + pressureLocked = false; + drawInnerCircleAndTriangle(w); + writeCircleText(w, "?"); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + }); + }, delay); } function drawPressure(w) { if (!w) w = getCirclePosition("pressure"); - getPressureValue("pressure").then((pressure) => { - drawCircleBackground(w); + drawCircleBackground(w); + g.setColor(colorFg); - if (pressure && pressure > 0) { + const delay = pressureLocked ? 1000 : 0; + setTimeout(() => { + getPressureValue("pressure").then((pressure) => { + pressureLocked = false; - const minPressure = 870; - const maxPressure = 1080; - const percent = (pressure - minPressure) / (maxPressure - minPressure); - drawGauge(w, h3, percent, colorGreen); - } + if (pressure && pressure > 0) { + const minPressure = 900; + const maxPressure = 1050; + const percent = (pressure - minPressure) / (maxPressure - minPressure); + drawGauge(w, h3, percent, colorGreen); + } - drawInnerCircleAndTriangle(w); + drawInnerCircleAndTriangle(w); - let icon = powerIcon; - let color = colorFg; - if (pressure && pressure > 0) - writeCircleText(w, pressure); + if (pressure) + writeCircleText(w, pressure); - g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - }); + }).catch(() => { + pressureLocked = false; + drawInnerCircleAndTriangle(w); + writeCircleText(w, "?"); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + }); + }, delay); +} + +function drawAltitude(w) { + if (!w) w = getCirclePosition("altitude"); + + drawCircleBackground(w); + g.setColor(colorFg); + + const delay = pressureLocked ? 1000 : 0; + setTimeout(() => { + getPressureValue("altitude").then((altitude) => { + pressureLocked = false; + + if (altitude) { + const min = -1000; + const max = 10000; + const percent = (pressure - min) / (max - min); + drawGauge(w, h3, percent, colorGreen); + } + + drawInnerCircleAndTriangle(w); + + if (altitude) + writeCircleText(w, altitude); + + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + + }).catch(() => { + pressureLocked = false; + drawInnerCircleAndTriangle(w); + writeCircleText(w, "?"); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + }); + }, delay); } /* @@ -663,20 +719,25 @@ function enableHRMSensor() { } } +let pressureLocked = false; + function getPressureValue(type) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { if (Bangle.getPressure) { - Bangle.getPressure().then(function(d) { - if (d) { - resolve(d[type]); - } - }).catch(function(e) {}); - } else { - switch (type) { - case "temperature": - resolve(E.getTemperature()); - break; + if (!pressureLocked) { + pressureLocked = true; + Bangle.getPressure().then(function(d) { + if (d && d[type]) { + resolve(d[type]); + } else { + reject(); + } + }).catch(reject); + } else { + reject(); } + } else { + reject(); } }); } diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index 5cb2e9bfc..eb70203a3 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -7,8 +7,8 @@ storage.write(SETTINGS_FILE, settings); } - const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty", "temperature", "pressure"]; - const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty", "temperature", "pressure"]; + const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty", "temperature", "pressure", "altitude"]; + const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty", "temperature", "pressure", "altitude"]; const weatherData = ["humidity", "wind", "empty"]; From 075889f4d3ad4ba7a475c0356ab4c9c3ea90f65c Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Fri, 28 Jan 2022 10:19:00 +0100 Subject: [PATCH 016/250] Improvements for pressure sensor values --- apps/circlesclock/app.js | 140 ++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 74 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 8f223978f..b17345d42 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -407,102 +407,85 @@ function drawSunProgress(w) { function drawTemperature(w) { if (!w) w = getCirclePosition("temperature"); - drawCircleBackground(w); - g.setColor(colorFg); + getPressureValue("temperature").then((temperature) => { + drawCircleBackground(w); + g.setColor(colorFg); - const delay = pressureLocked ? 1000 : 0; - setTimeout(() => { - getPressureValue("temperature").then((temperature) => { + drawInnerCircleAndTriangle(w); - pressureLocked = false; + if (temperature) { + const min = -40; // TODO: find good min, max for temperature + const max = 85; + const percent = (temperature - min) / (max - min); + drawGauge(w, h3, percent, colorGreen); + } - if (temperature) { - const percent = temperature / 100; // TODO: find good max for temperature - drawGauge(w, h3, percent, colorGreen); - } + if (temperature) + writeCircleText(w, locale.temp(temperature)); - drawInnerCircleAndTriangle(w); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - if (temperature) - writeCircleText(w, locale.temp(temperature)); - - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - - }).catch(() => { - pressureLocked = false; - drawInnerCircleAndTriangle(w); - writeCircleText(w, "?"); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - }); - }, delay); + }).catch(() => { + setTimeout(() => { + drawTemperature(); + }, 1000); + }); } function drawPressure(w) { if (!w) w = getCirclePosition("pressure"); - drawCircleBackground(w); - g.setColor(colorFg); + getPressureValue("pressure").then((pressure) => { + drawCircleBackground(w); + g.setColor(colorFg); - const delay = pressureLocked ? 1000 : 0; - setTimeout(() => { - getPressureValue("pressure").then((pressure) => { - pressureLocked = false; + drawInnerCircleAndTriangle(w); - if (pressure && pressure > 0) { - const minPressure = 900; - const maxPressure = 1050; - const percent = (pressure - minPressure) / (maxPressure - minPressure); - drawGauge(w, h3, percent, colorGreen); - } + if (pressure && pressure > 0) { + const minPressure = 900; + const maxPressure = 1050; + const percent = (pressure - minPressure) / (maxPressure - minPressure); + drawGauge(w, h3, percent, colorGreen); + } - drawInnerCircleAndTriangle(w); + if (pressure) + writeCircleText(w, Math.round(pressure)); - if (pressure) - writeCircleText(w, pressure); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - - }).catch(() => { - pressureLocked = false; - drawInnerCircleAndTriangle(w); - writeCircleText(w, "?"); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - }); - }, delay); + }).catch(() => { + setTimeout(() => { + drawPressure(w); + }, 1000); + }); } function drawAltitude(w) { if (!w) w = getCirclePosition("altitude"); - drawCircleBackground(w); - g.setColor(colorFg); + getPressureValue("altitude").then((altitude) => { + drawCircleBackground(w); + g.setColor(colorFg); - const delay = pressureLocked ? 1000 : 0; - setTimeout(() => { - getPressureValue("altitude").then((altitude) => { - pressureLocked = false; + drawInnerCircleAndTriangle(w); - if (altitude) { - const min = -1000; - const max = 10000; - const percent = (pressure - min) / (max - min); - drawGauge(w, h3, percent, colorGreen); - } + if (altitude) { + const min = 0; + const max = 10000; + const percent = (altitude - min) / (max - min); + drawGauge(w, h3, percent, colorGreen); + } - drawInnerCircleAndTriangle(w); + if (altitude) + writeCircleText(w, locale.distance(Math.round(altitude))); - if (altitude) - writeCircleText(w, altitude); + g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - - }).catch(() => { - pressureLocked = false; - drawInnerCircleAndTriangle(w); - writeCircleText(w, "?"); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); - }); - }, delay); + }).catch(() => { + setTimeout(() => { + drawAltitude(w); + }, 1000); + }); } /* @@ -720,6 +703,7 @@ function enableHRMSensor() { } let pressureLocked = false; +let lastPressureValue; function getPressureValue(type) { return new Promise((resolve, reject) => { @@ -727,14 +711,22 @@ function getPressureValue(type) { if (!pressureLocked) { pressureLocked = true; Bangle.getPressure().then(function(d) { - if (d && d[type]) { - resolve(d[type]); + pressureLocked = false; + if (d) { + lastPressureValue = d; + if (d[type]) { + resolve(d[type]); + } } else { reject(); } }).catch(reject); } else { - reject(); + if (lastPressureValue && lastPressureValue[type]) { + resolve(lastPressureValue[type]); + } else { + reject(); + } } } else { reject(); From efbd6be868be608f3fccb1a59950f91c9cff4dd3 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Fri, 28 Jan 2022 10:19:29 +0100 Subject: [PATCH 017/250] Improve font size choosing for circle inner text --- apps/circlesclock/app.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index b17345d42..c279cd830 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -97,8 +97,9 @@ const circlePosX = [ Math.round(7 * w / parts), // circle4 ]; -const radiusOuter = circleCount == 3 ? 25 : 20; +const radiusOuter = circleCount == 3 ? 25 : 19; const radiusInner = circleCount == 3 ? 20 : 15; +const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:11"; const circleFont = circleCount == 3 ? "Vector:15" : "Vector:12"; const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:13"; const iconOffset = circleCount == 3 ? 6 : 8; @@ -638,8 +639,8 @@ function radians(a) { */ function drawGauge(cx, cy, percent, color) { const offset = 15; - const end = 345; - const radius = radiusInner + 3; + const end = 360 - offset; + const radius = radiusInner + (circleCount == 3 ? 3 : 2); const size = radiusOuter - radiusInner - 2; if (percent <= 0) return; @@ -659,7 +660,8 @@ function drawGauge(cx, cy, percent, color) { function writeCircleText(w, content) { if (content == undefined) return; - g.setFont(content.length < 4 ? circleFontBig : circleFont); + const font = String(content).length > 3 ? circleFontSmall : String(content).length > 2 ? circleFont : circleFontBig; + g.setFont(font); g.setFontAlign(0, 0); g.setColor(colorFg); From 4e7833b808b8c6e846ac18564f4a05dce0cdc807 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Fri, 28 Jan 2022 12:02:09 +0100 Subject: [PATCH 018/250] Update readme --- apps/circlesclock/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index a3b713d76..51429cb4b 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -24,6 +24,7 @@ It can show the following information (this can be configured): ## Ideas * Make colors configurable +* Show compass heading ## Creator Marco ([myxor](https://github.com/myxor)) From f9d2d59e60969ea63f75ad71c13079d8cf48a2c6 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Fri, 28 Jan 2022 13:19:15 +0100 Subject: [PATCH 019/250] Cache for pressure values to get faster drawing + some layout improvements --- apps/circlesclock/app.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index c279cd830..be12342c1 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -70,6 +70,7 @@ const colorGreen = '#008000'; const colorBlue = '#0000ff'; const colorYellow = '#ffff00'; const widgetOffset = showWidgets ? 24 : 0; +const dowOffset = circleCount == 3 ? 22 : 24; // dow offset relative to date const h = g.getHeight() - widgetOffset; const w = g.getWidth(); const hOffset = 30 - widgetOffset; @@ -107,8 +108,6 @@ const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; function draw() { - g.clear(true); - if (!showWidgets) { /* * we are not drawing the widgets as we are taking over the whole screen @@ -125,8 +124,9 @@ function draw() { Bangle.drawWidgets(); } + g.clearRect(0, widgetOffset, w, h2 + 22); g.setColor(colorBg); - g.fillRect(0, widgetOffset, w, h); + g.fillRect(0, widgetOffset, w, h2 + 22); // time g.setFont("Vector:50"); @@ -139,7 +139,7 @@ function draw() { g.setFont("Vector:21"); g.setFontAlign(-1, 0); g.drawString(locale.date(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2); - g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + 22); + g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + dowOffset); drawCircle(1); drawCircle(2); @@ -410,7 +410,6 @@ function drawTemperature(w) { getPressureValue("temperature").then((temperature) => { drawCircleBackground(w); - g.setColor(colorFg); drawInnerCircleAndTriangle(w); @@ -438,12 +437,11 @@ function drawPressure(w) { getPressureValue("pressure").then((pressure) => { drawCircleBackground(w); - g.setColor(colorFg); drawInnerCircleAndTriangle(w); if (pressure && pressure > 0) { - const minPressure = 900; + const minPressure = 950; const maxPressure = 1050; const percent = (pressure - minPressure) / (maxPressure - minPressure); drawGauge(w, h3, percent, colorGreen); @@ -466,7 +464,6 @@ function drawAltitude(w) { getPressureValue("altitude").then((altitude) => { drawCircleBackground(w); - g.setColor(colorFg); drawInnerCircleAndTriangle(w); @@ -614,6 +611,7 @@ function getSunProgress() { * Draws the background and the grey circle */ function drawCircleBackground(w) { + g.clearRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); // Draw rectangle background: g.setColor(colorBg); g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); @@ -705,17 +703,22 @@ function enableHRMSensor() { } let pressureLocked = false; -let lastPressureValue; +let pressureCache; function getPressureValue(type) { return new Promise((resolve, reject) => { if (Bangle.getPressure) { if (!pressureLocked) { pressureLocked = true; + if (pressureCache && pressureCache[type]) { + resolve(pressureCache[type]); + } else { + reject(); + } Bangle.getPressure().then(function(d) { pressureLocked = false; if (d) { - lastPressureValue = d; + pressureCache = d; if (d[type]) { resolve(d[type]); } @@ -724,8 +727,8 @@ function getPressureValue(type) { } }).catch(reject); } else { - if (lastPressureValue && lastPressureValue[type]) { - resolve(lastPressureValue[type]); + if (pressureCache && pressureCache[type]) { + resolve(pressureCache[type]); } else { reject(); } @@ -762,6 +765,8 @@ Bangle.on('HRM', function(hrm) { Bangle.setUI("clock"); Bangle.loadWidgets(); +g.clear(true); + draw(); setInterval(draw, 60000); From 2fe7872fd8cf3ea62386744eddee95ccf694fbe7 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Fri, 28 Jan 2022 21:48:07 +0100 Subject: [PATCH 020/250] Fix drawing --- apps/circlesclock/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index be12342c1..79b2f7319 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -108,6 +108,7 @@ const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; function draw() { + g.reset(true); if (!showWidgets) { /* * we are not drawing the widgets as we are taking over the whole screen From ddbaf97b7dd0510a9aa5fdcac1cbeea6b5968064 Mon Sep 17 00:00:00 2001 From: Joseph Moroney Date: Sat, 29 Jan 2022 08:37:46 +1000 Subject: [PATCH 021/250] patch - call `Bangle.setUI` when exiting settings --- apps/sonicclk/Changelog | 7 ++++--- apps/sonicclk/README.md | 2 +- apps/sonicclk/app.js | 21 ++++++++++++--------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog index 4f597b627..050e18b5a 100644 --- a/apps/sonicclk/Changelog +++ b/apps/sonicclk/Changelog @@ -1,3 +1,4 @@ -1.00 Added sonic clock app -1.01 Fixed text alignment issue; Increased acceleration required to activate twist; -1.10 Added settings menu to control twist threshold and LCD Activity \ No newline at end of file +1.00 [MAJOR] Added sonic clock app +1.01 [PATCH] Fixed text alignment issue; Increased acceleration required to activate twist; +1.10 [MINOR] Added settings menu to control twist threshold and LCD Activity +1.11 [PATCH] Call `Bangle.setUI` when exiting settings menu, settings tap moved to top \ No newline at end of file diff --git a/apps/sonicclk/README.md b/apps/sonicclk/README.md index adfbf47b4..54de16115 100644 --- a/apps/sonicclk/README.md +++ b/apps/sonicclk/README.md @@ -12,7 +12,7 @@ A classic sonic clock featuring run, stop and wait animations. ## Configuration -To access the settings menu, you can **double tap** on the **right** side of the watch. The following options are configurable: +To access the settings menu, you can **double tap** on the **top** side of the watch. The following options are configurable: - `Active Mode` - catering for 'active' behaviour where the `twist` method can be fired undesirably. When `on` this will prevent the LCD from turning on when a `twist` event is fired. - `Twist Thresh` - customise the acceleration needed to activate the twist method (see the [`Bangle.setOptions`](https://www.espruino.com/Reference#:~:text=twisted%3F%20default%20%3D%20true-,twistThreshold,-How%20much%20acceleration) method for more info). diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js index b12af48fd..1fae353ca 100644 --- a/apps/sonicclk/app.js +++ b/apps/sonicclk/app.js @@ -264,18 +264,20 @@ const settings = require("Storage").readJSON("sonicclk-settings") || { let isSettings = false; const settingsMenu = { - "": { "title": "Settings" }, + "": { title: "Settings" }, "Active Mode": { value: settings.activeMode, - format: v => v ? "On" : "Off", - onchange: v => settings.activeMode = v, + format: (v) => (v ? "On" : "Off"), + onchange: (v) => (settings.activeMode = v), }, - "Twist Thresh" : { + "Twist Thresh": { value: settings.twistThreshold, - min: 800, max: 4000, step: 200, - onchange: v => settings.twistThreshold = v, + min: 800, + max: 4000, + step: 200, + onchange: (v) => (settings.twistThreshold = v), }, - "Exit" : () => { + Exit: () => { isSettings = false; require("Storage").writeJSON("sonicclk-settings", settings); @@ -286,9 +288,10 @@ const settingsMenu = { }); E.showMenu(); + Bangle.setUI("clock"); draw("reset"); start(); - } + }, }; g.setTheme({ bg: "#0099ff", fg: "#fff", dark: true }).clear(); @@ -313,7 +316,7 @@ Bangle.on("twist", () => { }); Bangle.on("tap", (d) => { - if (d.double && d.dir === "right") { + if (d.double && d.dir === "top") { fullReset(); isSettings = true; Bangle.setLocked(false); From 651f3a5f8fbaa645bdbc32fd3d030a74e375909c Mon Sep 17 00:00:00 2001 From: Joseph Moroney Date: Sat, 29 Jan 2022 08:39:05 +1000 Subject: [PATCH 022/250] Increment sonicclk version --- apps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.json b/apps.json index ffc76f746..8551481aa 100644 --- a/apps.json +++ b/apps.json @@ -5381,7 +5381,7 @@ { "id": "sonicclk", "name": "Sonic Clock", - "version": "1.10", + "version": "1.11", "description": "A classic sonic clock featuring run, stop and wait animations.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], From 1babca18baae02947a0d7b32423a2521702e2d7a Mon Sep 17 00:00:00 2001 From: Benjamin-6848 <83084481+Benjamin-6848@users.noreply.github.com> Date: Sat, 29 Jan 2022 00:11:30 +0100 Subject: [PATCH 023/250] fixed Font-Name in Morse App fixed Font-Name in Morse App --- apps/morse/morse-code.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/morse/morse-code.js b/apps/morse/morse-code.js index 227aeed81..7fd22d32d 100644 --- a/apps/morse/morse-code.js +++ b/apps/morse/morse-code.js @@ -4,7 +4,7 @@ /** * Constants */ -const FONT_NAME = 'Vector12'; +const FONT_NAME = 'Vector'; const FONT_SIZE = 80; const SCREEN_PIXELS = 240; const UNIT = 100; @@ -144,4 +144,4 @@ setWatch(step(true), BTN1, { repeat: true }); setWatch(step(false), BTN3, { repeat: true }); // Toggle buzzing/beeping with the touchscreen setWatch(toggleBuzzing, BTN4, { repeat: true }); -setWatch(toggleBuzzing, BTN5, { repeat: true }); \ No newline at end of file +setWatch(toggleBuzzing, BTN5, { repeat: true }); From 55e4866efdf71760e49658f08e9fac1c534ef200 Mon Sep 17 00:00:00 2001 From: Joseph Moroney Date: Sat, 29 Jan 2022 11:05:26 +1000 Subject: [PATCH 024/250] patch - 2v11 changes + `fullReset` update --- apps/sonicclk/Changelog | 1 + apps/sonicclk/app.js | 5 ++++- apps/sonicclk/metadata.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog index d0b0db581..d78fe291f 100644 --- a/apps/sonicclk/Changelog +++ b/apps/sonicclk/Changelog @@ -2,3 +2,4 @@ 0.02 [PATCH] Fixed text alignment issue; Increased acceleration required to activate twist; 0.03 [MINOR] Added settings menu to control twist threshold and LCD Activity 0.04 [PATCH] Call `Bangle.setUI` when exiting settings menu, settings tap moved to top +0.05 [PATCH] Firmware 2v11 - use `wakeOnTwist` rather than manual `setLCDPower`; Reset sonic on `fullReset` diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js index 1fae353ca..eddb971f8 100644 --- a/apps/sonicclk/app.js +++ b/apps/sonicclk/app.js @@ -113,6 +113,8 @@ const fullReset = () => { if (drawTimeout) clearTimeout(drawTimeout); if (waitTimeout) clearTimeout(waitTimeout); if (drawInterval) clearInterval(drawInterval); + currentSonic = -1; + currentSpeed = 0; }; const start = () => { @@ -148,7 +150,6 @@ const wait = () => { currentSpeed = 0; if (drawTimeout) clearTimeout(drawTimeout); if (drawInterval) clearInterval(drawInterval); - Bangle.setLCDPower(1); drawInterval = setInterval(() => draw("wait"), timeout); @@ -285,6 +286,7 @@ const settingsMenu = { lockTimeout: 10000, backlightTimeout: 12000, twistThreshold: settings.twistThreshold, + wakeOnTwist: !settings.activeMode, }); E.showMenu(); @@ -328,6 +330,7 @@ Bangle.setOptions({ lockTimeout: 10000, backlightTimeout: 12000, twistThreshold: settings.twistThreshold, + wakeOnTwist: !settings.activeMode, }); Bangle.setUI("clock"); diff --git a/apps/sonicclk/metadata.json b/apps/sonicclk/metadata.json index 569518d84..5a2d64db1 100644 --- a/apps/sonicclk/metadata.json +++ b/apps/sonicclk/metadata.json @@ -1,7 +1,7 @@ { "id": "sonicclk", "name": "Sonic Clock", - "version": "0.04", + "version": "0.05", "description": "A classic sonic clock featuring run, stop and wait animations.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], From eacd618eae1591d9ef14bdbee83c2e2c80b1938a Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 13:40:34 +0100 Subject: [PATCH 025/250] Fix drawing --- apps/circlesclock/app.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 79b2f7319..29b8b2cbb 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -108,7 +108,7 @@ const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; function draw() { - g.reset(true); + g.clear(true); if (!showWidgets) { /* * we are not drawing the widgets as we are taking over the whole screen @@ -125,7 +125,6 @@ function draw() { Bangle.drawWidgets(); } - g.clearRect(0, widgetOffset, w, h2 + 22); g.setColor(colorBg); g.fillRect(0, widgetOffset, w, h2 + 22); @@ -742,10 +741,10 @@ function getPressureValue(type) { Bangle.on('lock', function(isLocked) { if (!isLocked) { + draw(); if (isCircleEnabled("hr")) { enableHRMSensor(); } - draw(); } else { Bangle.setHRMPower(0, "circleclock"); } @@ -762,6 +761,15 @@ Bangle.on('HRM', function(hrm) { } }); +Bangle.on('charging', function(charging) { + if (isCircleEnabled("battery")) drawBattery(); +}); + +if (isCircleEnabled("hr")) { + enableHRMSensor(); +} + + Bangle.setUI("clock"); Bangle.loadWidgets(); @@ -770,11 +778,3 @@ g.clear(true); draw(); setInterval(draw, 60000); - -Bangle.on('charging', function(charging) { - if (isCircleEnabled("battery")) drawBattery(); -}); - -if (isCircleEnabled("hr")) { - enableHRMSensor(); -} From 1207fc41303c1baa1700b5d049b588147c3e2d32 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 13:43:35 +0100 Subject: [PATCH 026/250] Update changelog --- apps/circlesclock/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index fe3fd23fd..4e3af24b5 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -14,3 +14,4 @@ 0.07: Allow configuration of minimal heart rate confidence 0.08: Allow configuration of up to 4 circles in a row 0.09: Support to show temperature, air pressure or altitude from internal pressure sensor + Fix sunprogress calculation during night From 40ee16c8730c8c741441cd907af1a08c96d5b544 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 13:47:42 +0100 Subject: [PATCH 027/250] Clean up --- apps/circlesclock/app.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 29b8b2cbb..e4e42f665 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -770,11 +770,8 @@ if (isCircleEnabled("hr")) { } - Bangle.setUI("clock"); Bangle.loadWidgets(); -g.clear(true); - draw(); setInterval(draw, 60000); From 7de40d9c8cdae94e260ed35306e34dd283151ed2 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 21:51:44 +0100 Subject: [PATCH 028/250] Refactor settings menu & make colors of circles configurable --- apps/circlesclock/ChangeLog | 2 + apps/circlesclock/README.md | 1 - apps/circlesclock/app.js | 155 ++++++++++++--------- apps/circlesclock/settings.js | 247 +++++++++++++++++++--------------- 4 files changed, 233 insertions(+), 172 deletions(-) diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index 4e3af24b5..0393613e0 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -15,3 +15,5 @@ 0.08: Allow configuration of up to 4 circles in a row 0.09: Support to show temperature, air pressure or altitude from internal pressure sensor Fix sunprogress calculation during night + Refactor settings menu + Colors of circles can be configured diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 51429cb4b..325d6f838 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -23,7 +23,6 @@ It can show the following information (this can be configured): ![Screenshot light theme with four circles](screenshot-light-4.png) ## Ideas -* Make colors configurable * Show compass heading ## Creator diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index e4e42f665..520f7f748 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,28 +1,24 @@ const locale = require("locale"); -const heatshrink = require("heatshrink"); const storage = require("Storage"); const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); -const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA=")); -const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD")); -const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM")); -const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI")); -const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI")); -const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA")); -const themperatureIcon = heatshrink.decompress(atob("h0OwIFChkAvEBkEHwEeAwXgh+An8A/gGBgYWCA==")); +const shoesIcon = atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"); +const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); +const powerIcon = atob("FBSBAAAAAAAAAfgAP8AH/gB/4Af+AH/gB/4Af+AH/gB/4Af+AH/gB/4Af+AH/gB/4AAAAAAA"); +const temperatureIcon = atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"); -const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA")); -const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA=")); -const weatherMoon = heatshrink.decompress(atob("iEQwIFCgOAh/wj/4n/8AId//wBBBIoRBCoIZBDoI")); -const weatherPartlyCloudy = heatshrink.decompress(atob("iEQwYQNv0AjgGDn4EDh///gFChwREC4MfxwIBv0//+AC4X4j4FCv/AgfwgED/wIBuAaBBwgFDgP4gf/AAXABwIEBDQQAEA==")); -const weatherRainy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AocAnAFBFIU4EAM//gRBEAIOBhw1C/AmDAosAC4JNIAAg")); -const weatherPartlyRainy = heatshrink.decompress(atob("h0OwYJGjkAnAFCj+AAgU//4FCuEA8EAg8ch/4gEB4////AAoIIBCIMD/wgCg4bBg/8BwMD+AgBh4ZBDQf/FIIABh4IBgAA==")); -const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN/4EDx/4j/x4EAgIIBwAXBAogRFDoopFGoxBGABIA=")); -const weatherFoggy = heatshrink.decompress(atob("iEQwYROn/8AgUB/EfwAFBh/AgfwgED/wIBuEABwd/4EcDQgFDgE4Fosf///8f//A/Lj/xCQIRNA=")); -const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AoX8gE4AoQpBnAdBF4IRBDQMH/kOHgY7DAo4AOA==")); +const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); +const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); +const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); +const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); +const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); +const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); +const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); +const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); +const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); -const sunSetDown = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wLDg1ggfACoo")); -const sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY")); +const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); +const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); let settings = storage.readJSON("circlesclock.json", 1) || { 'minHR': 40, @@ -42,7 +38,7 @@ let settings = storage.readJSON("circlesclock.json", 1) || { }; // Load step goal from pedometer widget as fallback if (settings.stepGoal == undefined) { - const d = require('Storage').readJSON("wpedom.json", 1) || {}; + const d = storage.readJSON("wpedom.json", 1) || {}; settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; } @@ -150,7 +146,7 @@ function draw() { function drawCircle(index) { let type = settings['circle' + index]; if (!type) type = defaultCircleTypes[index - 1]; - const w = getCirclePosition(type); + const w = getCircleXPosition(type); switch (type) { case "steps": @@ -199,133 +195,164 @@ let circlePositionsCache = []; */ function getCirclePosition(type) { if (circlePositionsCache[type] >= 0) { - return circlePosX[circlePositionsCache[type]]; + return circlePositionsCache[type]; } for (let i = 1; i <= circleCount; i++) { const setting = settings['circle' + i]; if (setting == type) { circlePositionsCache[type] = i - 1; - return circlePosX[i - 1]; + return i - 1; } } for (let i = 0; i < defaultCircleTypes.length; i++) { if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) { circlePositionsCache[type] = i; - return circlePosX[i]; + return i; } } return undefined; } +function getCircleXPosition(type) { + const circlePos = getCirclePosition(type); + if (circlePos != undefined) { + return circlePosX[circlePos]; + } + return undefined; +} + function isCircleEnabled(type) { return getCirclePosition(type) != undefined; } +function getCircleColor(type) { + const pos = getCirclePosition(type); + const color = settings["circle" + (pos + 1) + "color"]; + if (color && color != "") return color; +} + +function getImage(graphic, color) { + return { + width: 16, + height: 16, + bpp: 1, + transparent: 0, + buffer: E.toArrayBuffer(graphic), + palette: new Uint16Array([colorBg, g.toColor(color)]) + }; +} + function drawSteps(w) { - if (!w) w = getCirclePosition("steps"); + if (!w) w = getCircleXPosition("steps"); const steps = getSteps(); drawCircleBackground(w); + const color = getCircleColor("steps") || colorBlue; + const stepGoal = settings.stepGoal || 10000; if (stepGoal > 0) { let percent = steps / stepGoal; if (stepGoal < steps) percent = 1; - drawGauge(w, h3, percent, colorBlue); + drawGauge(w, h3, percent, color); } drawInnerCircleAndTriangle(w); writeCircleText(w, shortValue(steps)); - g.drawImage(shoesIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(shoesIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawStepsDistance(w) { - if (!w) w = getCirclePosition("steps"); + if (!w) w = getCircleXPosition("stepsDistance"); const steps = getSteps(); const stepDistance = settings.stepLength || 0.8; const stepsDistance = Math.round(steps * stepDistance); drawCircleBackground(w); + const color = getCircleColor("stepsDistance") || colorGreen; + const stepDistanceGoal = settings.stepDistanceGoal || 8000; if (stepDistanceGoal > 0) { let percent = stepsDistance / stepDistanceGoal; if (stepDistanceGoal < stepsDistance) percent = 1; - drawGauge(w, h3, percent, colorGreen); + drawGauge(w, h3, percent, color); } drawInnerCircleAndTriangle(w); writeCircleText(w, shortValue(stepsDistance)); - g.drawImage(shoesIconGreen, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(shoesIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawHeartRate(w) { - if (!w) w = getCirclePosition("hr"); + if (!w) w = getCircleXPosition("hr"); drawCircleBackground(w); + const color = getCircleColor("hr") || colorRed; + if (hrtValue != undefined) { const minHR = settings.minHR || 40; const maxHR = settings.maxHR || 200; const percent = (hrtValue - minHR) / (maxHR - minHR); - drawGauge(w, h3, percent, colorRed); + drawGauge(w, h3, percent, color); } drawInnerCircleAndTriangle(w); writeCircleText(w, hrtValue != undefined ? hrtValue : "-"); - g.drawImage(heartIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(heartIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawBattery(w) { - if (!w) w = getCirclePosition("battery"); + if (!w) w = getCircleXPosition("battery"); const battery = E.getBattery(); drawCircleBackground(w); + let color = getCircleColor("battery") || colorYellow; + if (battery > 0) { const percent = battery / 100; - drawGauge(w, h3, percent, colorYellow); + drawGauge(w, h3, percent, color); } drawInnerCircleAndTriangle(w); - let icon = powerIcon; - let color = colorFg; if (Bangle.isCharging()) { color = colorGreen; - icon = powerIconGreen; } else { if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) { color = colorRed; - icon = powerIconRed; } } writeCircleText(w, battery + '%'); - g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(powerIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawWeather(w) { - if (!w) w = getCirclePosition("weather"); + if (!w) w = getCircleXPosition("weather"); const weather = getWeather(); const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; const code = weather ? weather.code : -1; drawCircleBackground(w); + const color = getCircleColor("weather") || colorYellow; + const data = settings.weatherCircleData || "humidity"; switch (data) { case "humidity": const humidity = weather ? weather.hum : undefined; if (humidity >= 0) { - drawGauge(w, h3, humidity / 100, colorYellow); + drawGauge(w, h3, humidity / 100, color); } break; case "wind": @@ -336,7 +363,7 @@ function drawWeather(w) { wind[1] = windAsBeaufort(wind[1]); } // wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) - drawGauge(w, h3, wind[1] / 12, colorYellow); + drawGauge(w, h3, wind[1] / 12, color); } } break; @@ -350,7 +377,7 @@ function drawWeather(w) { if (code > 0) { const icon = getWeatherIconByCode(code); - if (icon) g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + if (icon) g.drawImage(getImage(icon, color), w - iconOffset, h3 + radiusOuter - iconOffset); } else { g.drawString("?", w, h3 + radiusOuter); } @@ -358,27 +385,25 @@ function drawWeather(w) { function drawSunProgress(w) { - if (!w) w = getCirclePosition("sunprogress"); + if (!w) w = getCircleXPosition("sunprogress"); const percent = getSunProgress(); drawCircleBackground(w); - drawGauge(w, h3, percent, colorYellow); + const color = getCircleColor("sunpgrogress") || colorYellow; + + drawGauge(w, h3, percent, color); drawInnerCircleAndTriangle(w); - let icon = powerIcon; - let color = colorFg; + let icon = sunSetDown; if (isDay()) { // day - color = colorFg; icon = sunSetDown; } else { // night - color = colorGrey; icon = sunSetUp; } - g.setColor(color); let text = "?"; const times = getSunData(); @@ -402,28 +427,30 @@ function drawSunProgress(w) { writeCircleText(w, text); - g.drawImage(icon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(icon, color), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawTemperature(w) { - if (!w) w = getCirclePosition("temperature"); + if (!w) w = getCircleXPosition("temperature"); getPressureValue("temperature").then((temperature) => { drawCircleBackground(w); + const color = getCircleColor("temperature") || colorGreen; + drawInnerCircleAndTriangle(w); if (temperature) { - const min = -40; // TODO: find good min, max for temperature + const min = -40; const max = 85; const percent = (temperature - min) / (max - min); - drawGauge(w, h3, percent, colorGreen); + drawGauge(w, h3, percent, color); } if (temperature) writeCircleText(w, locale.temp(temperature)); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -433,24 +460,26 @@ function drawTemperature(w) { } function drawPressure(w) { - if (!w) w = getCirclePosition("pressure"); + if (!w) w = getCircleXPosition("pressure"); getPressureValue("pressure").then((pressure) => { drawCircleBackground(w); + const color = getCircleColor("pressure") || colorGreen; + drawInnerCircleAndTriangle(w); if (pressure && pressure > 0) { const minPressure = 950; const maxPressure = 1050; const percent = (pressure - minPressure) / (maxPressure - minPressure); - drawGauge(w, h3, percent, colorGreen); + drawGauge(w, h3, percent, color); } if (pressure) writeCircleText(w, Math.round(pressure)); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -460,24 +489,26 @@ function drawPressure(w) { } function drawAltitude(w) { - if (!w) w = getCirclePosition("altitude"); + if (!w) w = getCircleXPosition("altitude"); getPressureValue("altitude").then((altitude) => { drawCircleBackground(w); + const color = getCircleColor("altitude") || colorGreen; + drawInnerCircleAndTriangle(w); if (altitude) { const min = 0; const max = 10000; const percent = (altitude - min) / (max - min); - drawGauge(w, h3, percent, colorGreen); + drawGauge(w, h3, percent, color); } if (altitude) writeCircleText(w, locale.distance(Math.round(altitude))); - g.drawImage(themperatureIcon, w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index eb70203a3..cc2ba7a1f 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -10,122 +10,151 @@ const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty", "temperature", "pressure", "altitude"]; const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty", "temperature", "pressure", "altitude"]; + const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000"]; + const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black"]; + const weatherData = ["humidity", "wind", "empty"]; - E.showMenu({ - '': { 'title': 'circlesclock' }, - '< Back': back, - 'min heartrate': { - value: "minHR" in settings ? settings.minHR : 40, - min: 0, - max : 250, - step: 5, - format: x => { - return x; + function showMainMenu() { + let menu ={ + '': { 'title': 'Circles clock' }, + /*LANG*/'< Back': back, + /*LANG*/'circle count': { + value: "circleCount" in settings ? settings.circleCount : 3, + min: 3, + max : 4, + step: 1, + onchange: x => save('circleCount', x), }, - onchange: x => save('minHR', x), - }, - 'max heartrate': { - value: "maxHR" in settings ? settings.maxHR : 200, - min: 20, - max : 250, - step: 5, - format: x => { - return x; + /*LANG*/'circle 1': ()=>showCircleMenu(1), + /*LANG*/'circle 2': ()=>showCircleMenu(2), + /*LANG*/'circle 3': ()=>showCircleMenu(3), + /*LANG*/'circle 4': ()=>showCircleMenu(4), + /*LANG*/'heartrate': ()=>showHRMenu(), + /*LANG*/'steps': ()=>showStepMenu(), + /*LANG*/'battery warn': { + value: "batteryWarn" in settings ? settings.batteryWarn : 30, + min: 10, + max : 100, + step: 10, + format: x => { + return x + '%'; + }, + onchange: x => save('batteryWarn', x), }, - onchange: x => save('maxHR', x), - }, - 'hr confidence': { - value: "confidence" in settings ? settings.confidence : 0, - min: 0, - max : 100, - step: 10, - format: x => { - return x; + /*LANG*/'show widgets': { + value: "showWidgets" in settings ? settings.showWidgets : false, + format: () => (settings.showWidgets ? 'Yes' : 'No'), + onchange: x => save('showWidgets', x), }, - onchange: x => save('confidence', x), - }, - 'step goal': { - value: "stepGoal" in settings ? settings.stepGoal : 10000, - min: 2000, - max : 50000, - step: 2000, - format: x => { - return x; + /*LANG*/'weather circle': { + value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 0, + min: 0, max: 2, + format: v => weatherData[v], + onchange: x => save('weatherCircleData', weatherData[x]), + } + }; + E.showMenu(menu); + } + + function showHRMenu() { + let menu = { + '': { 'title': /*LANG*/'Heartrate' }, + /*LANG*/'< Back': ()=>showMainMenu(), + /*LANG*/'minimum bpm': { + value: "minHR" in settings ? settings.minHR : 40, + min: 0, + max : 250, + step: 5, + format: x => { + return x; + }, + onchange: x => save('minHR', x), }, - onchange: x => save('stepGoal', x), - }, - 'step length': { - value: "stepLength" in settings ? settings.stepLength : 0.8, - min: 0.1, - max : 1.5, - step: 0.01, - format: x => { - return x; + /*LANG*/'maximum bpm': { + value: "maxHR" in settings ? settings.maxHR : 200, + min: 20, + max : 250, + step: 5, + format: x => { + return x; + }, + onchange: x => save('maxHR', x), }, - onchange: x => save('stepLength', x), - }, - 'step dist goal': { - value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000, - min: 2000, - max : 30000, - step: 1000, - format: x => { - return x; + /*LANG*/'min. confidence': { + value: "confidence" in settings ? settings.confidence : 0, + min: 0, + max : 100, + step: 10, + format: x => { + return x; + }, + onchange: x => save('confidence', x), }, - onchange: x => save('stepDistanceGoal', x), - }, - 'battery warn': { - value: "batteryWarn" in settings ? settings.batteryWarn : 30, - min: 10, - max : 100, - step: 10, - format: x => { - return x + '%'; + }; + E.showMenu(menu); + } + + function showStepMenu() { + let menu = { + '': { 'title': /*LANG*/'Steps' }, + /*LANG*/'< Back': ()=>showMainMenu(), + /*LANG*/'goal': { + value: "stepGoal" in settings ? settings.stepGoal : 10000, + min: 2000, + max : 50000, + step: 2000, + format: x => { + return x; + }, + onchange: x => save('stepGoal', x), }, - onchange: x => save('batteryWarn', x), - }, - 'show widgets': { - value: "showWidgets" in settings ? settings.showWidgets : false, - format: () => (settings.showWidgets ? 'Yes' : 'No'), - onchange: x => save('showWidgets', x), - }, - 'weather circle': { - value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 0, - min: 0, max: 2, - format: v => weatherData[v], - onchange: x => save('weatherCircleData', weatherData[x]), - }, - 'circle count': { - value: "circleCount" in settings ? settings.circleCount : 3, - min: 3, - max : 4, - step: 1, - onchange: x => save('circleCount', x), - }, - 'circle1': { - value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0, - min: 0, max: valuesCircleTypes.length, - format: v => namesCircleTypes[v], - onchange: x => save('circle1', valuesCircleTypes[x]), - }, - 'circle2': { - value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2, - min: 0, max: valuesCircleTypes.length, - format: v => namesCircleTypes[v], - onchange: x => save('circle2', valuesCircleTypes[x]), - }, - 'circle3': { - value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3, - min: 0, max: valuesCircleTypes.length, - format: v => namesCircleTypes[v], - onchange: x => save('circle3', valuesCircleTypes[x]), - }, - 'circle4': { - value: settings.circle4 ? valuesCircleTypes.indexOf(settings.circle4) : 4, - min: 0, max: valuesCircleTypes.length, - format: v => namesCircleTypes[v], - onchange: x => save('circle4', valuesCircleTypes[x]), - } - }); + /*LANG*/'distance goal': { + value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000, + min: 2000, + max : 30000, + step: 1000, + format: x => { + return x; + }, + onchange: x => save('stepDistanceGoal', x), + }, + /*LANG*/'step length': { + value: "stepLength" in settings ? settings.stepLength : 0.8, + min: 0.1, + max : 1.5, + step: 0.01, + format: x => { + return x; + }, + onchange: x => save('stepLength', x), + } + }; + E.showMenu(menu); + } + + function showCircleMenu(circleId) { + String circleName = "circle" + circleId; + String colorKey = circleName + "color"; + let menu = { + '': { 'title': /*LANG*/'Circle ' + circleId }, + /*LANG*/'< Back': ()=>showMainMenu(), + /*LANG*/'data': { + value: settings[circleName] ? valuesCircleTypes.indexOf(settings[circleName]) : 0, + min: 0, max: valuesCircleTypes.length, + format: v => namesCircleTypes[v], + onchange: x => save(circleName, valuesCircleTypes[x]), + }, + /*LANG*/'color': { + value: settings[colorKey] ? valuesColors.indexOf(settings[colorKey]) : 0, + min: 0, max: valuesColors.length, + format: v => namesColors[v], + onchange: x => save(colorKey, valuesColors[x]), + }, + }; + E.showMenu(menu); + } + + + showMainMenu(); }); From fdd68f2de2caefa1cd09da44200d97a8777a0246 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 22:33:43 +0100 Subject: [PATCH 029/250] Colorization of icon can be toggled --- apps/circlesclock/app.js | 57 +++++++++++++++++++++-------------- apps/circlesclock/settings.js | 6 ++++ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 520f7f748..9e921ff5f 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -96,9 +96,9 @@ const circlePosX = [ const radiusOuter = circleCount == 3 ? 25 : 19; const radiusInner = circleCount == 3 ? 20 : 15; -const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:11"; -const circleFont = circleCount == 3 ? "Vector:15" : "Vector:12"; -const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:13"; +const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; +const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; +const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; const iconOffset = circleCount == 3 ? 6 : 8; const defaultCircleTypes = ["steps", "hr", "battery", "weather"]; @@ -231,17 +231,30 @@ function getCircleColor(type) { if (color && color != "") return color; } -function getImage(graphic, color) { - return { - width: 16, - height: 16, - bpp: 1, - transparent: 0, - buffer: E.toArrayBuffer(graphic), - palette: new Uint16Array([colorBg, g.toColor(color)]) - }; +function getCircleIconColor(type, color) { + const pos = getCirclePosition(type); + const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] != undefined; + if (colorizeIcon) { + return color; + } else { + return ""; + } } +function getImage(graphic, color) { + if (!color || color == "") { + return graphic; + } else { + return { + width: 16, + height: 16, + bpp: 1, + transparent: 0, + buffer: E.toArrayBuffer(graphic), + palette: new Uint16Array([colorBg, g.toColor(color)]) + }; + } +} function drawSteps(w) { if (!w) w = getCircleXPosition("steps"); @@ -262,7 +275,7 @@ function drawSteps(w) { writeCircleText(w, shortValue(steps)); - g.drawImage(getImage(shoesIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(shoesIcon, getCircleIconColor("steps", color)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawStepsDistance(w) { @@ -286,7 +299,7 @@ function drawStepsDistance(w) { writeCircleText(w, shortValue(stepsDistance)); - g.drawImage(getImage(shoesIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(shoesIcon, getCircleIconColor("stepsDistance", color)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawHeartRate(w) { @@ -307,7 +320,7 @@ function drawHeartRate(w) { writeCircleText(w, hrtValue != undefined ? hrtValue : "-"); - g.drawImage(getImage(heartIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawBattery(w) { @@ -334,7 +347,7 @@ function drawBattery(w) { } writeCircleText(w, battery + '%'); - g.drawImage(getImage(powerIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawWeather(w) { @@ -377,7 +390,7 @@ function drawWeather(w) { if (code > 0) { const icon = getWeatherIconByCode(code); - if (icon) g.drawImage(getImage(icon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color)), w - iconOffset, h3 + radiusOuter - iconOffset); } else { g.drawString("?", w, h3 + radiusOuter); } @@ -427,7 +440,7 @@ function drawSunProgress(w) { writeCircleText(w, text); - g.drawImage(getImage(icon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawTemperature(w) { @@ -450,7 +463,7 @@ function drawTemperature(w) { if (temperature) writeCircleText(w, locale.temp(temperature)); - g.drawImage(getImage(temperatureIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color)), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -479,7 +492,7 @@ function drawPressure(w) { if (pressure) writeCircleText(w, Math.round(pressure)); - g.drawImage(getImage(temperatureIcon), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color)), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -508,7 +521,7 @@ function drawAltitude(w) { if (altitude) writeCircleText(w, locale.distance(Math.round(altitude))); - g.drawImage(getImage(temperatureIcon, color), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color)), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -689,7 +702,7 @@ function drawGauge(cx, cy, percent, color) { function writeCircleText(w, content) { if (content == undefined) return; - const font = String(content).length > 3 ? circleFontSmall : String(content).length > 2 ? circleFont : circleFontBig; + const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig; g.setFont(font); g.setFontAlign(0, 0); diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index cc2ba7a1f..fb94ffd52 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -136,6 +136,7 @@ function showCircleMenu(circleId) { String circleName = "circle" + circleId; String colorKey = circleName + "color"; + String colorizeIconKey = circleName + "colorizeIcon"; let menu = { '': { 'title': /*LANG*/'Circle ' + circleId }, /*LANG*/'< Back': ()=>showMainMenu(), @@ -151,6 +152,11 @@ format: v => namesColors[v], onchange: x => save(colorKey, valuesColors[x]), }, + /*LANG*/'colorize icon': { + value: colorizeIconKey in settings ? settings[colorizeIconKey] : false, + format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'), + onchange: x => save(colorizeIconKey, x), + }, }; E.showMenu(menu); } From 686cfe249fb12389aeacf906cab5f6a4484f3a93 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 22:34:56 +0100 Subject: [PATCH 030/250] Update README --- apps/circlesclock/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 325d6f838..31afd98eb 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -16,6 +16,9 @@ It can show the following information (this can be configured): * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) * Temperature, air pressure or altitude from internal pressure sensor + +The color of each circle can be configured. + ## Screenshots ![Screenshot dark theme](screenshot-dark.png) ![Screenshot light theme](screenshot-light.png) From 37ecd01c5efcb6aba1803be681eb97eaecd51a38 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 22:43:01 +0100 Subject: [PATCH 031/250] Fix colorize setting reading --- apps/circlesclock/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 9e921ff5f..064072d05 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -233,7 +233,7 @@ function getCircleColor(type) { function getCircleIconColor(type, color) { const pos = getCirclePosition(type); - const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] != undefined; + const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true; if (colorizeIcon) { return color; } else { From 41a9cf851df9a0f0047fdd9300e184b7a96e739a Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sat, 29 Jan 2022 22:44:33 +0100 Subject: [PATCH 032/250] Bump version --- apps/circlesclock/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/circlesclock/metadata.json b/apps/circlesclock/metadata.json index bd2ce751b..f426a1681 100644 --- a/apps/circlesclock/metadata.json +++ b/apps/circlesclock/metadata.json @@ -1,7 +1,7 @@ { "id": "circlesclock", "name": "Circles clock", "shortName":"Circles clock", - "version":"0.08", + "version":"0.09", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style", "icon": "app.png", "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}], From faec5bd02112e4fabb7b3edd1ee014382ea7c888 Mon Sep 17 00:00:00 2001 From: The Dod Date: Sun, 30 Jan 2022 15:17:39 +0200 Subject: [PATCH 033/250] `acmaze` and `ftclock` are `bangle.js 1` compat Both apps were tested on a `bangle.js 1` by forum user Mi: https://forum.espruino.com/comments/16376253/ (thanks for testing). --- apps/acmaze/metadata.json | 2 +- apps/ftclock/metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/acmaze/metadata.json b/apps/acmaze/metadata.json index d8ab8fa62..48635ad2f 100644 --- a/apps/acmaze/metadata.json +++ b/apps/acmaze/metadata.json @@ -5,7 +5,7 @@ "description": "Tilt the watch to roll a ball through a maze.", "icon": "app.png", "tags": "game", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "screenshots": [{"url":"screenshot.png"}], "storage": [ diff --git a/apps/ftclock/metadata.json b/apps/ftclock/metadata.json index ec45e9ce3..3cbf5ce66 100644 --- a/apps/ftclock/metadata.json +++ b/apps/ftclock/metadata.json @@ -7,7 +7,7 @@ "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot1.png"}], "type": "clock", "tags": "clock", - "supports": ["BANGLEJS2"], + "supports" : ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"ftclock.app.js","url":"app.js"}, From b02c2f4de889e10f34a8f6b56d58e44b232562bc Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sun, 30 Jan 2022 19:04:15 +0100 Subject: [PATCH 034/250] Fix circle filling --- apps/circlesclock/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 064072d05..3ab1f93a7 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -94,7 +94,7 @@ const circlePosX = [ Math.round(7 * w / parts), // circle4 ]; -const radiusOuter = circleCount == 3 ? 25 : 19; +const radiusOuter = circleCount == 3 ? 25 : 20; const radiusInner = circleCount == 3 ? 20 : 15; const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; From 93fb41dfe0f88a7a84c1f6f999c1f21af2b9e585 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sun, 30 Jan 2022 20:28:31 +0100 Subject: [PATCH 035/250] Color depending on value (green -> red, red -> green) --- apps/circlesclock/ChangeLog | 1 + apps/circlesclock/README.md | 5 ++- apps/circlesclock/app.js | 83 ++++++++++++++++++++++------------- apps/circlesclock/settings.js | 4 +- 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index 0393613e0..4fcdbd653 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -17,3 +17,4 @@ Fix sunprogress calculation during night Refactor settings menu Colors of circles can be configured + Color depending on value (green -> red, red -> green) option diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 31afd98eb..aa429d5ec 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -17,7 +17,10 @@ It can show the following information (this can be configured): * Temperature, air pressure or altitude from internal pressure sensor -The color of each circle can be configured. +The color of each circle can be configured. The following colors are available: + * Basic colors (red, green, blue, yellow, magenta, cyan, black, white) + * Color depending on value (green -> red, red -> green) + ## Screenshots ![Screenshot dark theme](screenshot-dark.png) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 3ab1f93a7..107e8d450 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -231,16 +231,33 @@ function getCircleColor(type) { if (color && color != "") return color; } -function getCircleIconColor(type, color) { +function getCircleIconColor(type, color, percent) { const pos = getCirclePosition(type); const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true; if (colorizeIcon) { - return color; + return getGradientColor(color, percent); } else { return ""; } } +function getGradientColor(color, percent) { + if (isNaN(percent)) percent = 0; + if (percent > 1) percent = 1; + const colorList = [ + '#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000' + ]; + if (color == "green-red") { + const colorIndex = Math.round(colorList.length * percent); + return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00"; + } + if (color == "red-green") { + const colorIndex = colorList.length - Math.round(colorList.length * percent); + return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000"; + } + return color; +} + function getImage(graphic, color) { if (!color || color == "") { return graphic; @@ -264,9 +281,10 @@ function drawSteps(w) { const color = getCircleColor("steps") || colorBlue; + let percent; const stepGoal = settings.stepGoal || 10000; if (stepGoal > 0) { - let percent = steps / stepGoal; + percent = steps / stepGoal; if (stepGoal < steps) percent = 1; drawGauge(w, h3, percent, color); } @@ -275,7 +293,7 @@ function drawSteps(w) { writeCircleText(w, shortValue(steps)); - g.drawImage(getImage(shoesIcon, getCircleIconColor("steps", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(shoesIcon, getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawStepsDistance(w) { @@ -288,9 +306,10 @@ function drawStepsDistance(w) { const color = getCircleColor("stepsDistance") || colorGreen; + let percent; const stepDistanceGoal = settings.stepDistanceGoal || 8000; if (stepDistanceGoal > 0) { - let percent = stepsDistance / stepDistanceGoal; + percent = stepsDistance / stepDistanceGoal; if (stepDistanceGoal < stepsDistance) percent = 1; drawGauge(w, h3, percent, color); } @@ -299,7 +318,7 @@ function drawStepsDistance(w) { writeCircleText(w, shortValue(stepsDistance)); - g.drawImage(getImage(shoesIcon, getCircleIconColor("stepsDistance", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(shoesIcon, getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawHeartRate(w) { @@ -309,10 +328,12 @@ function drawHeartRate(w) { const color = getCircleColor("hr") || colorRed; + let percent; if (hrtValue != undefined) { const minHR = settings.minHR || 40; const maxHR = settings.maxHR || 200; - const percent = (hrtValue - minHR) / (maxHR - minHR); + percent = (hrtValue - minHR) / (maxHR - minHR); + if (isNaN(percent)) percent = 0; drawGauge(w, h3, percent, color); } @@ -320,7 +341,7 @@ function drawHeartRate(w) { writeCircleText(w, hrtValue != undefined ? hrtValue : "-"); - g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawBattery(w) { @@ -331,8 +352,9 @@ function drawBattery(w) { let color = getCircleColor("battery") || colorYellow; + let percent; if (battery > 0) { - const percent = battery / 100; + percent = battery / 100; drawGauge(w, h3, percent, color); } @@ -347,7 +369,7 @@ function drawBattery(w) { } writeCircleText(w, battery + '%'); - g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawWeather(w) { @@ -359,13 +381,14 @@ function drawWeather(w) { drawCircleBackground(w); const color = getCircleColor("weather") || colorYellow; - + let percent; const data = settings.weatherCircleData || "humidity"; switch (data) { case "humidity": const humidity = weather ? weather.hum : undefined; if (humidity >= 0) { - drawGauge(w, h3, humidity / 100, color); + percent = humidity / 100; + drawGauge(w, h3, percent, color); } break; case "wind": @@ -376,7 +399,8 @@ function drawWeather(w) { wind[1] = windAsBeaufort(wind[1]); } // wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) - drawGauge(w, h3, wind[1] / 12, color); + percent = wind[1] / 12; + drawGauge(w, h3, percent, color); } } break; @@ -390,7 +414,7 @@ function drawWeather(w) { if (code > 0) { const icon = getWeatherIconByCode(code); - if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } else { g.drawString("?", w, h3 + radiusOuter); } @@ -403,21 +427,13 @@ function drawSunProgress(w) { drawCircleBackground(w); - const color = getCircleColor("sunpgrogress") || colorYellow; + const color = getCircleColor("sunprogress") || colorYellow; drawGauge(w, h3, percent, color); drawInnerCircleAndTriangle(w); let icon = sunSetDown; - if (isDay()) { - // day - icon = sunSetDown; - } else { - // night - icon = sunSetUp; - } - let text = "?"; const times = getSunData(); if (times != undefined) { @@ -432,15 +448,17 @@ function drawSunProgress(w) { } else { text = formatSeconds(sunRise - now); } + icon = sunSetUp; } else { // day, approx sunrise tomorrow: text = formatSeconds(sunSet - now); + icon = sunSetDown; } } writeCircleText(w, text); - g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); } function drawTemperature(w) { @@ -452,18 +470,18 @@ function drawTemperature(w) { const color = getCircleColor("temperature") || colorGreen; drawInnerCircleAndTriangle(w); - + let percent; if (temperature) { const min = -40; const max = 85; - const percent = (temperature - min) / (max - min); + percent = (temperature - min) / (max - min); drawGauge(w, h3, percent, color); } if (temperature) writeCircleText(w, locale.temp(temperature)); - g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -482,17 +500,18 @@ function drawPressure(w) { drawInnerCircleAndTriangle(w); + let percent; if (pressure && pressure > 0) { const minPressure = 950; const maxPressure = 1050; - const percent = (pressure - minPressure) / (maxPressure - minPressure); + percent = (pressure - minPressure) / (maxPressure - minPressure); drawGauge(w, h3, percent, color); } if (pressure) writeCircleText(w, Math.round(pressure)); - g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -511,17 +530,18 @@ function drawAltitude(w) { drawInnerCircleAndTriangle(w); + let percent; if (altitude) { const min = 0; const max = 10000; - const percent = (altitude - min) / (max - min); + percent = (altitude - min) / (max - min); drawGauge(w, h3, percent, color); } if (altitude) writeCircleText(w, locale.distance(Math.round(altitude))); - g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color)), w - iconOffset, h3 + radiusOuter - iconOffset); + g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); }).catch(() => { setTimeout(() => { @@ -691,6 +711,7 @@ function drawGauge(cx, cy, percent, color) { const startRotation = -offset; const endRotation = startRotation - ((end - offset) * percent); + color = getGradientColor(color, percent); g.setColor(color); for (let i = startRotation; i > endRotation - size; i -= size) { diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index fb94ffd52..ac0a4f696 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -10,8 +10,8 @@ const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty", "temperature", "pressure", "altitude"]; const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty", "temperature", "pressure", "altitude"]; - const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000"]; - const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black"]; + const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"]; + const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"]; const weatherData = ["humidity", "wind", "empty"]; From 12bf75d92404cd8ac61a445e6f2414a75a039af1 Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sun, 30 Jan 2022 20:46:51 +0100 Subject: [PATCH 036/250] Fix battery icon and gauge of barometer values --- apps/circlesclock/app.js | 45 ++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index 107e8d450..475b4e86f 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -4,7 +4,7 @@ const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/maste const shoesIcon = atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"); const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); -const powerIcon = atob("FBSBAAAAAAAAAfgAP8AH/gB/4Af+AH/gB/4Af+AH/gB/4Af+AH/gB/4Af+AH/gB/4AAAAAAA"); +const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); const temperatureIcon = atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"); const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); @@ -469,7 +469,6 @@ function drawTemperature(w) { const color = getCircleColor("temperature") || colorGreen; - drawInnerCircleAndTriangle(w); let percent; if (temperature) { const min = -40; @@ -478,15 +477,15 @@ function drawTemperature(w) { drawGauge(w, h3, percent, color); } + + drawInnerCircleAndTriangle(w); + + if (temperature) writeCircleText(w, locale.temp(temperature)); g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - }).catch(() => { - setTimeout(() => { - drawTemperature(); - }, 1000); }); } @@ -498,8 +497,6 @@ function drawPressure(w) { const color = getCircleColor("pressure") || colorGreen; - drawInnerCircleAndTriangle(w); - let percent; if (pressure && pressure > 0) { const minPressure = 950; @@ -508,15 +505,15 @@ function drawPressure(w) { drawGauge(w, h3, percent, color); } + + drawInnerCircleAndTriangle(w); + + if (pressure) writeCircleText(w, Math.round(pressure)); g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - }).catch(() => { - setTimeout(() => { - drawPressure(w); - }, 1000); }); } @@ -528,8 +525,6 @@ function drawAltitude(w) { const color = getCircleColor("altitude") || colorGreen; - drawInnerCircleAndTriangle(w); - let percent; if (altitude) { const min = 0; @@ -538,15 +533,15 @@ function drawAltitude(w) { drawGauge(w, h3, percent, color); } + + drawInnerCircleAndTriangle(w); + + if (altitude) writeCircleText(w, locale.distance(Math.round(altitude))); g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - }).catch(() => { - setTimeout(() => { - drawAltitude(w); - }, 1000); }); } @@ -705,7 +700,7 @@ function drawGauge(cx, cy, percent, color) { const radius = radiusInner + (circleCount == 3 ? 3 : 2); const size = radiusOuter - radiusInner - 2; - if (percent <= 0) return; + if (percent <= 0) return; // no gauge needed if (percent > 1) percent = 1; const startRotation = -offset; @@ -771,14 +766,12 @@ let pressureLocked = false; let pressureCache; function getPressureValue(type) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { if (Bangle.getPressure) { if (!pressureLocked) { pressureLocked = true; if (pressureCache && pressureCache[type]) { resolve(pressureCache[type]); - } else { - reject(); } Bangle.getPressure().then(function(d) { pressureLocked = false; @@ -787,19 +780,13 @@ function getPressureValue(type) { if (d[type]) { resolve(d[type]); } - } else { - reject(); } - }).catch(reject); + }).catch(() => {}); } else { if (pressureCache && pressureCache[type]) { resolve(pressureCache[type]); - } else { - reject(); } } - } else { - reject(); } }); } From c96a52238f8c81786facedc75a1fcbacd471665e Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Sun, 30 Jan 2022 20:55:22 +0100 Subject: [PATCH 037/250] Improve settings sorting --- apps/circlesclock/settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/circlesclock/settings.js b/apps/circlesclock/settings.js index ac0a4f696..7aaad6824 100644 --- a/apps/circlesclock/settings.js +++ b/apps/circlesclock/settings.js @@ -7,13 +7,13 @@ storage.write(SETTINGS_FILE, settings); } - const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty", "temperature", "pressure", "altitude"]; - const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty", "temperature", "pressure", "altitude"]; + const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"]; + const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"]; const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"]; const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"]; - const weatherData = ["humidity", "wind", "empty"]; + const weatherData = ["empty", "humidity", "wind"]; function showMainMenu() { let menu ={ @@ -48,7 +48,7 @@ onchange: x => save('showWidgets', x), }, /*LANG*/'weather circle': { - value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 0, + value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 1, min: 0, max: 2, format: v => weatherData[v], onchange: x => save('weatherCircleData', weatherData[x]), @@ -142,13 +142,13 @@ /*LANG*/'< Back': ()=>showMainMenu(), /*LANG*/'data': { value: settings[circleName] ? valuesCircleTypes.indexOf(settings[circleName]) : 0, - min: 0, max: valuesCircleTypes.length, + min: 0, max: valuesCircleTypes.length - 1, format: v => namesCircleTypes[v], onchange: x => save(circleName, valuesCircleTypes[x]), }, /*LANG*/'color': { value: settings[colorKey] ? valuesColors.indexOf(settings[colorKey]) : 0, - min: 0, max: valuesColors.length, + min: 0, max: valuesColors.length - 1, format: v => namesColors[v], onchange: x => save(colorKey, valuesColors[x]), }, From bcfb9167bd6cad6660ba9163fd82e4da32fffae5 Mon Sep 17 00:00:00 2001 From: Marc Englund Date: Tue, 25 Jan 2022 22:19:34 +0200 Subject: [PATCH 038/250] New version, redesigned and refactored A bit of re-design and refactor before making things configurable. Removed flicker. --- apps/ruuviwatch/ChangeLog | 1 + apps/ruuviwatch/README.md | 37 +-- apps/ruuviwatch/metadata.json | 4 +- apps/ruuviwatch/ruuviwatch-in-action.jpg | Bin 0 -> 80687 bytes apps/ruuviwatch/ruuviwatch.app.js | 371 ++++++++++++++--------- 5 files changed, 258 insertions(+), 155 deletions(-) create mode 100644 apps/ruuviwatch/ruuviwatch-in-action.jpg diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog index 7953548cb..15ff601f0 100644 --- a/apps/ruuviwatch/ChangeLog +++ b/apps/ruuviwatch/ChangeLog @@ -1,2 +1,3 @@ 0.01: Hello Ruuvi Watch! 0.02: Clear gfx on startup. +0.03: Improve design and code, reduce flicker. diff --git a/apps/ruuviwatch/README.md b/apps/ruuviwatch/README.md index bf4358267..c96e032d7 100644 --- a/apps/ruuviwatch/README.md +++ b/apps/ruuviwatch/README.md @@ -1,25 +1,26 @@ -# Ruuvi Watch +# Ruuvi Watch -Watch the status of [RuuviTags](https://ruuvi.com) in range. +Watch the status of [RuuviTags](https://ruuvi.com) in range. - - Id - - Temperature (°C) - - Humidity (%) - - Pressure (hPa) - - Battery voltage +![Ruuvi Watch in action](/BangleApps/apps/ruuviwatch/ruuviwatch-in-action.jpg) - Also shows how "fresh" the data is (age of reading). +- Id +- Temperature (°C) +- Humidity (%) +- Pressure (hPa) +- Battery voltage - ## Usage - - - Scans for devices when launched and every N seconds. - - Page trough devices with BTN1/BTN3. - - Trigger scan with BTN2. +Also shows how "fresh" the data is (age of reading). + +## Usage + +- Scans for devices when launched and every N seconds. +- Page trough devices with BTN1/BTN3. +- Trigger scan with BTN2. ## Todo / ideas - - Allow to "name" known devices - - Prevent flicker when updating - - Include more data - - Support older Ruuvi protocols - +- Settings for scan frequency, units +- Allow to "name" known devices +- Include more data +- Support older Ruuvi protocols diff --git a/apps/ruuviwatch/metadata.json b/apps/ruuviwatch/metadata.json index 12f9ff4a0..413d96153 100644 --- a/apps/ruuviwatch/metadata.json +++ b/apps/ruuviwatch/metadata.json @@ -2,8 +2,8 @@ "name": "Ruuvi Watch", "shortName":"Ruuvi Watch", "icon": "ruuviwatch.png", - "version":"0.02", - "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", + "version":"0.03", + "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.", "readme":"README.md", "tags": "bluetooth", "supports": ["BANGLEJS"], diff --git a/apps/ruuviwatch/ruuviwatch-in-action.jpg b/apps/ruuviwatch/ruuviwatch-in-action.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08b391e8413dc1fea01a685ee60f4b93ab092fd5 GIT binary patch literal 80687 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