From b7d94f32441e384137763e455ae96a359396d3d8 Mon Sep 17 00:00:00 2001 From: Paul Cockrell Date: Wed, 8 Apr 2020 17:02:16 +0100 Subject: [PATCH 1/2] Integrate GadgetBridge --- apps.json | 2 +- apps/marioclock/ChangeLog | 3 +- apps/marioclock/README.md | 3 +- apps/marioclock/marioclock-app.js | 140 +++++++++++++++++++++++++++++- 4 files changed, 142 insertions(+), 6 deletions(-) diff --git a/apps.json b/apps.json index 92df7fe93..0271c244d 100644 --- a/apps.json +++ b/apps.json @@ -914,7 +914,7 @@ { "id": "marioclock", "name": "Mario Clock", "icon": "marioclock.png", - "version":"0.08", + "version":"0.09", "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.", "tags": "clock,mario,retro", "type": "clock", diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog index c95ce3d0d..acce6a7ed 100644 --- a/apps/marioclock/ChangeLog +++ b/apps/marioclock/ChangeLog @@ -5,4 +5,5 @@ 0.05: use 12/24 hour clock from settings 0.06: Performance refactor, and enhanced graphics! 0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode -0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy. +0.08: Update date panel to be info panel toggling between Date, Battery and Temperature. Add Princes Daisy +0.09: Add GadgetBridge functionality. Mario shows message type in speach bubble, while message scrolls in info panel \ No newline at end of file diff --git a/apps/marioclock/README.md b/apps/marioclock/README.md index 8b859e031..e6aeaa1bb 100644 --- a/apps/marioclock/README.md +++ b/apps/marioclock/README.md @@ -13,7 +13,8 @@ Enjoy watching Mario, or one of the other game characters run through a level wh * Awesome 8-bit style grey-scale graphics * Mario jumps to change the time, every minute * You can make Mario jump by pressing the bottom button (Button 3) on the watch -* Toggle the info pannel bettween `Date`, `Battery level`, and `Temperature` by pressing the top button (Button 1). +* Toggle the info pannel bettween `Date`, `Battery level`, and `Temperature` by pressing the top button (Button 1) +* If you have [GadgetBridge](https://f-droid.org/packages/nodomain.freeyourgadget.gadgetbridge/) installed on your phone, Mario will let you know when you get a new call or notification. You can clear a message by pressing either Button 1 or Button 3 ## Requests diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js index 81f5616b9..529f1c95b 100644 --- a/apps/marioclock/marioclock-app.js +++ b/apps/marioclock/marioclock-app.js @@ -58,6 +58,7 @@ const ONE_SECOND = 1000; const DATE_MODE = "date"; const BATT_MODE = "batt"; const TEMP_MODE = "temp"; +const PHON_MODE = "gbri"; let timer = 0; let backgroundArr = []; @@ -68,6 +69,77 @@ let infoMode = DATE_MODE; let lastBatt = 0; let lastTemp = 0; +const phone = { + get status() { + return NRF.getSecurityStatus().connected ? "Yes" : "No"; + }, + message: null, + messageTimeout: null, + messageScrollX: null, + messageType: null, +}; + +function phoneOutbound(msg) { + Bluetooth.println(JSON.stringify(msg)); +} + +function phoneClearMessage() { + if (phone.message === null) return; + + if (phone.messageTimeout) { + clearTimeout(phone.messageTimeout); + phone.messageTimeout = null; + } + phone.message = null; + phone.messageScrollX = null; + phone.messageType = null; +} + +function phoneNewMessage(type, msg) { + Bangle.buzz(); + + phoneClearMessage(); + phone.messageTimeout = setTimeout(() => phone.message = null, ONE_SECOND * 30); + phone.message = msg; + phone.messageType = type; + + // Notify user and active screen + if (!Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDPower(true); + } +} + +function truncStr(str, max) { + if (str.length > max) { + return str.substr(0, max) + '...'; + } + return str; +} + +function phoneInbound(evt) { + switch (evt.t) { + case 'notify': + const sender = truncStr(evt.sender, 10); + const subject = truncStr(evt.subject, 15); + phoneNewMessage("notify", `${sender} - '${subject}'`); + break; + case 'call': + if (evt.cmd === "accept") { + let nameOrNumber = "Unknown"; + if (evt.name !== null || evt.name !== "") { + nameOrNumber = evt.name; + } else if (evt.number !== null || evt.number !== "") { + nameOrNumber = evt.number; + } + phoneNewMessage("call", nameOrNumber); + } + break; + default: + return null; + } +} + function genRanNum(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); } @@ -248,6 +320,22 @@ function drawToadFrame(idx, x, y) { } } +// Mario speach bubble +function drawNotice(x, y) { + if (phone.message === null) return; + + switch (phone.messageType) { + case "call": + const callImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INEw9cAAIPFBxAPEBw/WBxYACDrQ7QLI53OSpApDBoQAHB4INLByANNAwo=")); + g.drawImage(callImg, characterSprite.x, characterSprite.y - 16); + break; + case "notify": + const msgImg = require("heatshrink").decompress(atob("h8PxH+AAMHABIND6wAJB4INCrgAHB4QOEDQgOIAIQFGBwovDA4gOGFooOVLJR3OSpApDBoQAHB4INLByANNAwoA=")); + g.drawImage(msgImg, characterSprite.x, characterSprite.y - 16); + break; + } +} + function drawCharacter(date, character) { // calculate jumping const seconds = date.getSeconds(), @@ -352,9 +440,33 @@ function buildTempStr() { return tempStr; } +function buildPhonStr() { + return `Phone: ${phone.status}`; +} + function drawInfo(date) { + let xPos; let str = ""; - switch(infoMode) { + + if (phone.message !== null) { + str = phone.message; + const strLen = g.stringWidth(str); + if (strLen > W) { + if (phone.messageScrollX === null || (phone.messageScrollX <= (strLen * -1))) { + phone.messageScrollX = W; + resetDisplayTimeout(); + } else { + phone.messageScrollX -= 2; + } + xPos = phone.messageScrollX; + } else { + xPos = (W - g.stringWidth(str)) / 2; + } + } else { + switch(infoMode) { + case PHON_MODE: + str = buildPhonStr(); + break; case TEMP_MODE: str = buildTempStr(); break; @@ -364,19 +476,26 @@ function drawInfo(date) { case DATE_MODE: default: str = buildDateStr(date); + } + xPos = (W - g.stringWidth(str)) / 2; } g.setFont("6x8"); g.setColor(LIGHTEST); - g.drawString(str, (W - g.stringWidth(str))/2, 1); + g.drawString(str, xPos, 1); } function changeInfoMode() { + phoneClearMessage(); + switch(infoMode) { case BATT_MODE: infoMode = TEMP_MODE; break; case TEMP_MODE: + infoMode = PHON_MODE; + break; + case PHON_MODE: infoMode = DATE_MODE; break; case DATE_MODE: @@ -399,6 +518,7 @@ function redraw() { drawTime(date); drawInfo(date); drawCharacter(date); + drawNotice(); drawCoin(); // Render new frame @@ -450,6 +570,7 @@ function init() { setWatch(() => { if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true; resetDisplayTimeout(); + phoneClearMessage(); // Clear any phone messages and message timers }, BTN3, {repeat: true}); // Close watch and load launcher app @@ -487,8 +608,21 @@ function init() { } }); + // Phone connectivity + try { NRF.wake(); } catch (e) {} + + NRF.on('disconnect', () => Bangle.buzz()); + NRF.on('connect', () => { + setTimeout(() => { + phoneOutbound({ t: "status", bat: E.getBattery() }); + }, ONE_SECOND * 2); + Bangle.buzz(); + }); + + GB = (evt) => phoneInbound(evt); + startTimers(); } // Initialise! -init(); \ No newline at end of file +init() \ No newline at end of file From 8b2ea9f0c3148ac6b8ddf40a4b57205edcc628ae Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Wed, 8 Apr 2020 22:30:28 +0200 Subject: [PATCH 2/2] widpedom: Add goal setting Draws progress as part of a circle (default goal: 10.000 seems to be common-ish?) --- apps.json | 5 +-- apps/widpedom/ChangeLog | 1 + apps/widpedom/settings.js | 44 ++++++++++++++++++++++++ apps/widpedom/widget.js | 71 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 apps/widpedom/settings.js diff --git a/apps.json b/apps.json index e250da0f6..2a4cd7750 100644 --- a/apps.json +++ b/apps.json @@ -786,12 +786,13 @@ { "id": "widpedom", "name": "Pedometer widget", "icon": "widget.png", - "version":"0.08", + "version":"0.09", "description": "Daily pedometer widget", "tags": "widget", "type":"widget", "storage": [ - {"name":"widpedom.wid.js","url":"widget.js"} + {"name":"widpedom.wid.js","url":"widget.js"}, + {"name":"widpedom.settings.js","url":"settings.js"} ] }, { "id": "berlinc", diff --git a/apps/widpedom/ChangeLog b/apps/widpedom/ChangeLog index 980494acb..d6fe28940 100644 --- a/apps/widpedom/ChangeLog +++ b/apps/widpedom/ChangeLog @@ -5,3 +5,4 @@ 0.06: Fix widget position increment 0.07: Tweaks for variable size widget system 0.08: Ensure redrawing works with variable size widget system +0.09: Add daily goal diff --git a/apps/widpedom/settings.js b/apps/widpedom/settings.js new file mode 100644 index 000000000..f6d30b830 --- /dev/null +++ b/apps/widpedom/settings.js @@ -0,0 +1,44 @@ +(function(back) { + const SETTINGS_FILE = 'widpedom.settings.json' + + // initialize with default settings... + let s = { + 'goal': 10000, + 'progress': false, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + s[key] = saved[key] + } + + function save() { + storage.write(SETTINGS_FILE, s) + WIDGETS['wpedom'].reload() + } + + E.showMenu({ + '': { 'title': 'Pedometer widget' }, + 'Daily Goal': { + value: s.goal, + min: 0, step: 1000, + format: s => (s ? s / 1000 + ',000' : '0'), + onchange: (g) => { + s.goal = g + s.progress = !!g + save() + }, + }, + 'Show Progress': { + value: s.progress, + format: () => (s.progress ? 'Yes' : 'No'), + onchange: () => { + s.progress = !s.progress + save() + }, + }, + '< Back': back, + }) +}) diff --git a/apps/widpedom/widget.js b/apps/widpedom/widget.js index 1cc14fc2c..a9e5bf121 100644 --- a/apps/widpedom/widget.js +++ b/apps/widpedom/widget.js @@ -1,7 +1,57 @@ (() => { - const PEDOMFILE = "wpedom.json"; + const PEDOMFILE = "wpedom.json" + const SETTINGS_FILE = "widpedom.settings.json" + const DEFAULTS = { + 'goal': 10000, + 'progress': false, + } + const COLORS = { + 'white': -1, + 'progress': 0x001F, // Blue + 'done': 0x03E0, // DarkGreen + } + const TAU = Math.PI*2; let lastUpdate = new Date(); let stp_today = 0; + let settings; + + function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; + } + + function setting(key) { + if (!settings) { loadSettings() } + return (key in settings) ? settings[key] : DEFAULTS[key]; + } + + function drawProgress(stps) { + if (setting('progress')) { + const width = 24, half = width/2; + const goal = setting('goal'), left = Math.max(goal-stps,0); + const c = left ? COLORS.progress : COLORS.done; + g.setColor(c).fillCircle(this.x + half, this.y + half, half); + if (left) { + const f = left/goal; // fraction to blank out + let p = []; + p.push(half,half); + p.push(half,0); + if(f>1/8) p.push(0,0); + if(f>2/8) p.push(0,half); + if(f>3/8) p.push(0,width); + if(f>4/8) p.push(half,width); + if(f>5/8) p.push(width,width); + if(f>6/8) p.push(width,half); + if(f>7/8) p.push(width,0); + p.push(half - Math.sin(f * TAU) * half); + p.push(half - Math.cos(f * TAU) * half); + for (let i = p.length; i; i -= 2) { + p[i - 2] += this.x; + p[i - 1] += this.y; + } + g.setColor(0).fillPoly(p); + } + } + } // draw your widget function draw() { @@ -11,6 +61,9 @@ } let stps = stp_today.toString(); g.reset(); + g.clearRect(this.x, this.y, this.x + width, this.y + 23); // erase background + drawProgress(stps); + g.setColor(COLORS.white); if (stps.length > 3){ stps = stps.slice(0,-3) + "," + stps.slice(-3); g.setFont("4x6", 1); // if big, shrink text to fix @@ -18,11 +71,15 @@ g.setFont("6x8", 1); } g.setFontAlign(0, 0); // align to x: center, y: center - g.clearRect(this.x,this.y+15,this.x+width,this.y+23); // erase background g.drawString(stps, this.x+width/2, this.y+19); g.drawImage(atob("CgoCLguH9f2/7+v6/79f56CtAAAD9fw/n8Hx9A=="),this.x+(width-10)/2,this.y+2); } + function reload() { + loadSettings() + draw() + } + Bangle.on('step', (up) => { let date = new Date(); if (lastUpdate.getDate() == date.getDate()){ @@ -31,7 +88,13 @@ // TODO: could save this to PEDOMFILE for lastUpdate's day? stp_today = 1; } - lastUpdate = date; + if (stp_today === setting('goal')) { + let b = 3, buzz = () => { + if (b--) Bangle.buzz().then(() => setTimeout(buzz, 100)) + } + buzz() + } + lastUpdate = date //console.log("up: " + up + " stp: " + stp_today + " " + date.toString()); if (Bangle.isLCDOn()) WIDGETS["wpedom"].draw(); }); @@ -49,7 +112,7 @@ }); // add your widget - WIDGETS["wpedom"]={area:"tl",width:26,draw:draw}; + WIDGETS["wpedom"]={area:"tl",width:26,draw:draw,reload:Reload}; // Load data at startup let pedomData = require("Storage").readJSON(PEDOMFILE,1); if (pedomData) {