From 2d60ce4a5358cfa2367bc60f44d3a1efbc7740fe Mon Sep 17 00:00:00 2001 From: Lubomir Date: Tue, 24 May 2022 22:08:43 +1000 Subject: [PATCH 01/23] android: Fix SMS bug --- apps/android/ChangeLog | 1 + apps/android/boot.js | 13 ++++++++++++- apps/android/metadata.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 55f456795..f13ccd95c 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -8,3 +8,4 @@ 0.07: Include charging state in battery updates to phone 0.08: Handling of alarms 0.09: Alarm vibration, repeat, and auto-snooze now handled by sched +0.10: Fix SMS bug diff --git a/apps/android/boot.js b/apps/android/boot.js index fb2101aa4..efd7e7e46 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -3,6 +3,7 @@ Bluetooth.println(""); Bluetooth.println(JSON.stringify(message)); } + var lastMsg; var settings = require("Storage").readJSON("android.settings.json",1)||{}; //default alarm settings @@ -18,7 +19,17 @@ /* TODO: Call handling, fitness */ var HANDLERS = { // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add - "notify" : function() { Object.assign(event,{t:"add",positive:true, negative:true});require("messages").pushMessage(event); }, + "notify" : function() { + Object.assign(event,{t:"add",positive:true, negative:true}); + // Detect a weird GadgetBridge bug and fix it + // For some reason SMS messages send two GB notifications, with different sets of info + if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") { + // Mutate the other message + event.id = lastMsg.id; + } + lastMsg = event; + require("messages").pushMessage(event); + }, // {t:"notify~",id:int, title:string} // modified "notify~" : function() { event.t="modify";require("messages").pushMessage(event); }, // {t:"notify-",id:int} // remove diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 4051a344b..bf37b8407 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.09", + "version": "0.10", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", From 1fb256b9def3cf47df62d79b1f8f1ae563848e5f Mon Sep 17 00:00:00 2001 From: Lubomir Date: Tue, 24 May 2022 22:24:38 +1000 Subject: [PATCH 02/23] messages: Replace mail icon with color notif icons --- apps/messages/ChangeLog | 1 + apps/messages/app.js | 94 ++----------------------------------- apps/messages/lib.js | 93 +++++++++++++++++++++++++++++++++++- apps/messages/metadata.json | 2 +- apps/messages/widget.js | 53 +++++++++++++++------ 5 files changed, 135 insertions(+), 108 deletions(-) diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index d6ad393d6..1a484b3e5 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -50,4 +50,5 @@ 0.35: Reset graphics colors before rendering a message (possibly fix #1752) 0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362) 0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items +0.38: Add notification icons in the widget diff --git a/apps/messages/app.js b/apps/messages/app.js index aac59e246..4919fcd94 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -67,99 +67,13 @@ function saveMessages() { require("Storage").writeJSON("messages.json",MESSAGES) } -function getNotificationImage() { - return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); -} -function getFBIcon() { - return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); -} function getPosImage() { return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); } function getNegImage() { return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); } -/* -* icons should be 24x24px with 1bpp colors and 'Transparency to Color' -* http://www.espruino.com/Image+Converter -*/ -function getMessageImage(msg) { - if (msg.img) return atob(msg.img); - var s = (msg.src||"").toLowerCase(); - if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); - if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA"); - if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); - if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA"); - if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA"); - if (s=="facebook") return getFBIcon(); - if (s=="gmail") return getNotificationImage(); - if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); - if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); - if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA="); - if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); - if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA"); - if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44"); - if (s=="mail") return getNotificationImage(); - if (s=="messenger") return getFBIcon(); - if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA"); - if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA="); - if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); - if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw"); - if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA"); - if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); - if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); - if (s=="sms message") return getNotificationImage(); - if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA"); - if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA"); - if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); - if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); - if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA"); - if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); - if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); - if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); - if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); - if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); - return getNotificationImage(); -} -function getMessageImageCol(msg,def) { - return { - // generic colors, using B2-safe colors - "alarm": "#fff", - "mail": "#ff0", - "music": "#f0f", - "phone": "#0f0", - "sms message": "#0ff", - // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) - // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) - "bibel": "#54342c", - "discord": "#738adb", - "facebook": "#4267b2", - "gmail": "#ea4335", - "google home": "#fbbc05", - "hangouts": "#1ba261", - "home assistant": "#fff", // ha-blue is #41bdf5, but that's the background - "instagram": "#dd2a7b", - "liferando": "#ee5c00", - "messenger": "#0078ff", - "nina": "#e57004", - "outlook mail": "#0072c6", - "post & dhl": "#f2c101", - "signal": "#00f", - "skype": "#00aff0", - "slack": "#e51670", - "snapchat": "#ff0", - "teams": "#464eb8", - "telegram": "#0088cc", - "threema": "#000", - "to do": "#3999e5", - "twitch": "#6441A4", - "twitter": "#1da1f2", - "whatsapp": "#4fce5d", - "wordfeud": "#e7d3c7", - "youtube": "#f00", - }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); -} + function showMapMessage(msg) { active = "map"; @@ -411,7 +325,7 @@ function showMessage(msgid) { {type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 }, title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{}, ]}, - { type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{ + { type:"btn", src:require("messages").getMessageImage(msg), col:require("messages").getMessageImageCol(msg), pad: 3, cb:()=>{ cancelReloadTimeout(); // don't auto-reload to clock now showMessageSettings(msg); }}, @@ -467,14 +381,14 @@ function checkMessages(options) { g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); if (!msg) return; var x = r.x+2, title = msg.title, body = msg.body; - var img = getMessageImage(msg); + var img = require("messages").getMessageImage(msg); if (msg.id=="music") { title = msg.artist || /*LANG*/"Music"; body = msg.track; } if (img) { var fg = g.getColor(); - g.setColor(getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering + g.setColor(require("messages").getMessageImageCol(msg,fg)).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering .setColor(fg); // only color the icon x += 50; } diff --git a/apps/messages/lib.js b/apps/messages/lib.js index c39c8886c..f3c461e57 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -85,7 +85,7 @@ exports.pushMessage = function(event) { return load("messages.app.js"); } if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz to let someone know - WIDGETS.messages.show(); + WIDGETS.messages.update(); }, 500); } /// Remove all messages @@ -102,5 +102,94 @@ exports.clearAll = function(event) { if (inApp) return onMessagesModified(); // if we have a widget, update it if (global.WIDGETS && WIDGETS.messages) - WIDGETS.messages.hide(); + WIDGETS.messages.update(); +} + +function getNotificationImage() { + return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); +} +function getFBIcon() { + return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); +} +/* +* icons should be 24x24px with 1bpp colors and 'Transparency to Color' +* http://www.espruino.com/Image+Converter +*/ +exports.getMessageImage = function (msg) { + if (msg.img) return atob(msg.img); + var s = (msg.src||"").toLowerCase(); + if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); + if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA"); + if (s=="calendar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); + if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA"); + if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA"); + if (s=="facebook") return getFBIcon(); + if (s=="gmail") return getNotificationImage(); + if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); + if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); + if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA="); + if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); + if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA"); + if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44"); + if (s=="mail") return getNotificationImage(); + if (s=="messenger") return getFBIcon(); + if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA"); + if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA="); + if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); + if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw"); + if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA"); + if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); + if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); + if (s=="sms message") return getNotificationImage(); + if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA"); + if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA"); + if (s=="telegram") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); + if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); + if (s=="to do") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); + if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA"); + if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); + if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); + if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); + if (s=="youtube") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); + if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); + return getNotificationImage(); +} + +exports.getMessageImageCol = function (msg,def) { + return { + // generic colors, using B2-safe colors + "alarm": "#fff", + "mail": "#ff0", + "music": "#f0f", + "phone": "#0f0", + "sms message": "#0ff", + // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) + // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) + "bibel": "#54342c", + "discord": "#738adb", + "facebook": "#4267b2", + "gmail": "#ea4335", + "google home": "#fbbc05", + "hangouts": "#1ba261", + "home assistant": "#fff", // ha-blue is #41bdf5, but that's the background + "instagram": "#dd2a7b", + "liferando": "#ee5c00", + "messenger": "#0078ff", + "nina": "#e57004", + "outlook mail": "#0072c6", + "post & dhl": "#f2c101", + "signal": "#00f", + "skype": "#00aff0", + "slack": "#e51670", + "snapchat": "#ff0", + "teams": "#464eb8", + "telegram": "#0088cc", + "threema": "#000", + "to do": "#3999e5", + "twitch": "#6441A4", + "twitter": "#1da1f2", + "whatsapp": "#4fce5d", + "wordfeud": "#e7d3c7", + "youtube": "#f00", + }[(msg.src||"").toLowerCase()]||(def !== undefined?def:g.theme.fg); } diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index ab9b03273..fd09fdfe4 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,7 +1,7 @@ { "id": "messages", "name": "Messages", - "version": "0.37", + "version": "0.38", "description": "App to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", diff --git a/apps/messages/widget.js b/apps/messages/widget.js index 4b368ffd6..bb887b04e 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,3 +1,15 @@ +(() => { + +function getBaseMessages() { + if ("undefined"!=typeof MESSAGES) return MESSAGES; + return require("Storage").readJSON("messages.json",1)||[]; +} + +function getMessages() { + return getBaseMessages().filter(msg => msg.new && msg.id != "music") + .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); +} + WIDGETS["messages"]={area:"tl", width:0, iconwidth:24, draw:function(recall) { // If we had a setTimeout queued from the last time we were called, remove it @@ -11,8 +23,18 @@ draw:function(recall) { let settings = require('Storage').readJSON("messages.settings.json", true) || {}; if (settings.flash===undefined) settings.flash = true; if (recall !== true || settings.flash) { + var msgs = getMessages(); + var msgsShown = E.clip(msgs.length, 0, 3); g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); - g.drawImage(settings.flash && (c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y-1); + for(let i = 0;i < msgsShown;i++) { + const msg = msgs[i]; + const colors = [g.theme.bg, g.setColor(require("messages").getMessageImageCol(msg)).getColor()]; + if (settings.flash && (c&1)) { + colors[1] = g.theme.fg; + } + g.setColor(colors[1]).setBgColor(colors[0]); + g.drawImage(i == 2 ? atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4H4H4H4H4H4H4H4H4H4H4H4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") : require("messages").getMessageImage(msg), this.x + i * this.iconwidth, this.y - 1); + } } if (settings.repeat===undefined) settings.repeat = 4; if (c<120 && (Date.now()-this.l)>settings.repeat*1000) { @@ -21,18 +43,19 @@ draw:function(recall) { } WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000); if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); -},show:function(quiet) { - WIDGETS["messages"].t=Date.now(); // first time - WIDGETS["messages"].l=Date.now()-10000; // last buzz - if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing - WIDGETS["messages"].width=this.iconwidth; - Bangle.drawWidgets(); - Bangle.setLCDPower(1);// turns screen on -},hide:function() { - delete WIDGETS["messages"].t; - delete WIDGETS["messages"].l; - WIDGETS["messages"].width=0; +},update:function(quiet) { + const msgs = getMessages(); + if (msgs.length === 0) { + delete WIDGETS["messages"].t; + delete WIDGETS["messages"].l; + } else { + WIDGETS["messages"].t=Date.now(); // first time + WIDGETS["messages"].l=Date.now()-10000; // last buzz + if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing + } + WIDGETS["messages"].width=this.iconwidth * E.clip(msgs.length, 0, 3); Bangle.drawWidgets(); + if (msgs.length !== 0) Bangle.setLCDPower(1);// turns screen on },buzz:function() { if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return; // never buzz during Quiet Mode require("buzz").pattern((require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || "."); @@ -41,10 +64,10 @@ draw:function(recall) { if (!w||!w.width||c.xw.x+w.width||c.yw.y+w.iconwidth) return; load("messages.app.js"); }}; + /* We might have returned here if we were in the Messages app for a message but then the watch was never viewed. In that case we don't want to buzz but should still show that there are unread messages. */ -if (global.MESSAGES===undefined) (function() { - var messages = require("Storage").readJSON("messages.json",1)||[]; - if (messages.some(m=>m.new&&m.id!="music")) WIDGETS["messages"].show(true); +WIDGETS["messages"].update(true); + })(); From 9740a36796e617489133e4d42901aca3371012e4 Mon Sep 17 00:00:00 2001 From: "Lubomir (Mac)" Date: Sun, 29 May 2022 14:19:38 +1000 Subject: [PATCH 03/23] messages: Use message array from lib Also, a max widget messages setting --- apps/messages/lib.js | 4 ++-- apps/messages/settings.js | 6 ++++++ apps/messages/widget.js | 24 +++++++++++++----------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/messages/lib.js b/apps/messages/lib.js index dc1da6662..a098f2c81 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -85,7 +85,7 @@ exports.pushMessage = function(event) { return load("messages.app.js"); } if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz to let someone know - WIDGETS.messages.update(); + if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages); }, 500); } /// Remove all messages @@ -102,7 +102,7 @@ exports.clearAll = function(event) { if (inApp) return onMessagesModified(); // if we have a widget, update it if (global.WIDGETS && WIDGETS.messages) - WIDGETS.messages.update(); + WIDGETS.messages.update(messages); } function getNotificationImage() { diff --git a/apps/messages/settings.js b/apps/messages/settings.js index adea36f12..44301fff6 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -4,6 +4,7 @@ if (settings.vibrate===undefined) settings.vibrate="."; if (settings.repeat===undefined) settings.repeat=4; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; + if (settings.maxMessages===undefined) settings.maxMessages=3; settings.unlockWatch=!!settings.unlockWatch; settings.openMusic=!!settings.openMusic; settings.maxUnreadTimeout=240; @@ -58,6 +59,11 @@ format: v => v?/*LANG*/'Yes':/*LANG*/'No', onchange: v => updateSetting("quietNoAutOpn", v) }, + /*LANG*/'Widget messages': { + value:0|settings().maxMessages, + min: 1, max: 5, + onchange: v => updateSetting("maxMessages", v) + } }; E.showMenu(mainmenu); }) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index bb887b04e..a8ba33ae5 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,12 +1,12 @@ (() => { -function getBaseMessages() { +function getMessages() { if ("undefined"!=typeof MESSAGES) return MESSAGES; return require("Storage").readJSON("messages.json",1)||[]; } -function getMessages() { - return getBaseMessages().filter(msg => msg.new && msg.id != "music") +function filterMessages(msgs) { + return msgs.filter(msg => msg.new && msg.id != "music") .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); } @@ -23,17 +23,16 @@ draw:function(recall) { let settings = require('Storage').readJSON("messages.settings.json", true) || {}; if (settings.flash===undefined) settings.flash = true; if (recall !== true || settings.flash) { - var msgs = getMessages(); - var msgsShown = E.clip(msgs.length, 0, 3); + var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages); g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); for(let i = 0;i < msgsShown;i++) { - const msg = msgs[i]; + const msg = this.msgs[i]; const colors = [g.theme.bg, g.setColor(require("messages").getMessageImageCol(msg)).getColor()]; if (settings.flash && (c&1)) { colors[1] = g.theme.fg; } g.setColor(colors[1]).setBgColor(colors[0]); - g.drawImage(i == 2 ? atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4H4H4H4H4H4H4H4H4H4H4H4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") : require("messages").getMessageImage(msg), this.x + i * this.iconwidth, this.y - 1); + g.drawImage(i == (settings.maxMessages - 1) && msgs.length > settings.maxMessages ? atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4H4H4H4H4H4H4H4H4H4H4H4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") : require("messages").getMessageImage(msg), this.x + i * this.iconwidth, this.y - 1); } } if (settings.repeat===undefined) settings.repeat = 4; @@ -43,8 +42,9 @@ draw:function(recall) { } WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000); if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); -},update:function(quiet) { - const msgs = getMessages(); +},update:function(rawMsgs, quiet) { + const settings = require('Storage').readJSON("messages.settings.json", true) || {}; + msgs = filterMessages(rawMsgs); if (msgs.length === 0) { delete WIDGETS["messages"].t; delete WIDGETS["messages"].l; @@ -53,7 +53,8 @@ draw:function(recall) { WIDGETS["messages"].l=Date.now()-10000; // last buzz if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing } - WIDGETS["messages"].width=this.iconwidth * E.clip(msgs.length, 0, 3); + WIDGETS["messages"].width=this.iconwidth * E.clip(msgs.length, 0, settings.maxMessages); + WIDGETS["messages"].msgs = msgs; Bangle.drawWidgets(); if (msgs.length !== 0) Bangle.setLCDPower(1);// turns screen on },buzz:function() { @@ -68,6 +69,7 @@ draw:function(recall) { /* We might have returned here if we were in the Messages app for a message but then the watch was never viewed. In that case we don't want to buzz but should still show that there are unread messages. */ -WIDGETS["messages"].update(true); +if (global.MESSAGES===undefined) + WIDGETS["messages"].update(getMessages(), true); })(); From 6c6e9b141d3bed52cc189fbc160433f7031182c0 Mon Sep 17 00:00:00 2001 From: CarlR9 <108166078+CarlR9@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:01:54 +1200 Subject: [PATCH 04/23] Add files via upload --- apps/widscrlock/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/widscrlock/metadata.json b/apps/widscrlock/metadata.json index 9ff76b910..5110d76c1 100644 --- a/apps/widscrlock/metadata.json +++ b/apps/widscrlock/metadata.json @@ -1,7 +1,7 @@ { "id": "widscrlock", "name": "Screenlock Widget", "shortName":"Screenlock", - "version":"0.02", + "version":"0.03", "description": "Lock a Bangle 2 screen by tapping a widget.", "icon": "widget.png", "type": "widget", From fe707c335e27a101057ccb8be3dee4b8c825ea68 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 28 Jun 2022 11:51:25 +0100 Subject: [PATCH 05/23] Add (hopefully!) github actions compatible error messages so we get files marked --- bin/sanitycheck.js | 161 ++++++++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 68 deletions(-) diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 81c0f75ac..569f5fa4b 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -17,13 +17,21 @@ try { } var BASEDIR = __dirname+"/../"; -var APPSDIR = BASEDIR+"apps/"; -function ERROR(s) { - console.error("ERROR: "+s); - process.exit(1); +var APPSDIR_RELATIVE = "apps/"; +var APPSDIR = BASEDIR + APPSDIR_RELATIVE; +var warningCount = 0; +var errorCount = 0; +function ERROR(msg, opt) { + // file=app.js,line=1,col=5,endColumn=7 + opt = opt||{}; + console.log(`::error${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + errorCount++; } -function WARN(s) { - console.log("Warning: "+s); +function WARN(msg, opt) { + // file=app.js,line=1,col=5,endColumn=7 + opt = opt||{}; + console.log(`::warning${Object.keys(opt).length?" ":""}${Object.keys(opt).map(k=>k+"="+opt[k]).join(",")}::${msg}`); + warningCount++; } var apps = []; @@ -43,16 +51,20 @@ dirs.forEach(dir => { } catch (e) { console.log(e); var m = e.toString().match(/in JSON at position (\d+)/); + var messageInfo = { + file : "apps/"+dir.name+"/metadata.json", + }; if (m) { var char = parseInt(m[1]); + messageInfo.line = appsFile.substr(0,char).split("\n").length; console.log("==============================================="); - console.log("LINE "+appsFile.substr(0,char).split("\n").length); + console.log("LINE "+messageInfo.line); console.log("==============================================="); console.log(appsFile.substr(char-10, 20)); console.log("==============================================="); } console.log(m); - ERROR(dir.name+"/metadata.json not valid JSON"); + ERROR(messageInfo.file+" not valid JSON", messageInfo); } }); @@ -87,87 +99,90 @@ let allFiles = []; let existingApps = []; apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); - if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`); + var appDirRelative = APPSDIR_RELATIVE+app.id+"/"; + var appDir = APPSDIR+app.id+"/"; + var metadataFile = appDirRelative+"metadata.json"; + if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`, {file:metadataFile}); existingApps.push(app.id); //console.log(`Checking ${app.id}...`); - var appDir = APPSDIR+app.id+"/"; + if (!fs.existsSync(APPSDIR+app.id)) ERROR(`App ${app.id} has no directory`); - if (!app.name) ERROR(`App ${app.id} has no name`); + if (!app.name) ERROR(`App ${app.id} has no name`, {file:metadataFile}); var isApp = !app.type || app.type=="app"; - if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`); + if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`, {file:metadataFile}); if (app.type && !METADATA_TYPES.includes(app.type)) - ERROR(`App ${app.id} 'type' is one one of `+METADATA_TYPES); - if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`); + ERROR(`App ${app.id} 'type' is one one of `+METADATA_TYPES, {file:metadataFile}); + if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`, {file:metadataFile}); else { app.supports.forEach(dev => { if (!SUPPORTS_DEVICES.includes(dev)) - ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`); + ERROR(`App ${app.id} has unknown device in 'supports' field - ${dev}`, {file:metadataFile}); }); } - if (!app.version) WARN(`App ${app.id} has no version`); + if (!app.version) ERROR(`App ${app.id} has no version`, {file:metadataFile}); else { if (!fs.existsSync(appDir+"ChangeLog")) { if (app.version != "0.01") - WARN(`App ${app.id} has no ChangeLog`); + WARN(`App ${app.id} has no ChangeLog`, {file:metadataFile}); } else { var changeLog = fs.readFileSync(appDir+"ChangeLog").toString(); var versions = changeLog.match(/\d+\.\d+:/g); - if (!versions) ERROR(`No versions found in ${app.id} ChangeLog (${appDir}ChangeLog)`); + if (!versions) ERROR(`No versions found in ${app.id} ChangeLog (${appDir}ChangeLog)`, {file:metadataFile}); var lastChangeLog = versions.pop().slice(0,-1); if (lastChangeLog != app.version) - WARN(`App ${app.id} app version (${app.version}) and ChangeLog (${lastChangeLog}) don't agree`); + ERROR(`App ${app.id} app version (${app.version}) and ChangeLog (${lastChangeLog}) don't agree`, {file:appDirRelative+"ChangeLog", line:changeLog.split("\n").length-1}); } } - if (!app.description) ERROR(`App ${app.id} has no description`); - if (!app.icon) ERROR(`App ${app.id} has no icon`); - if (!fs.existsSync(appDir+app.icon)) ERROR(`App ${app.id} icon doesn't exist`); + if (!app.description) ERROR(`App ${app.id} has no description`, {file:metadataFile}); + if (!app.icon) ERROR(`App ${app.id} has no icon`, {file:metadataFile}); + if (!fs.existsSync(appDir+app.icon)) ERROR(`App ${app.id} icon doesn't exist`, {file:metadataFile}); if (app.screenshots) { - if (!Array.isArray(app.screenshots)) ERROR(`App ${app.id} screenshots is not an array`); + if (!Array.isArray(app.screenshots)) ERROR(`App ${app.id} screenshots is not an array`, {file:metadataFile}); app.screenshots.forEach(screenshot => { if (!fs.existsSync(appDir+screenshot.url)) - ERROR(`App ${app.id} screenshot file ${screenshot.url} not found`); + ERROR(`App ${app.id} screenshot file ${screenshot.url} not found`, {file:metadataFile}); }); } - if (app.readme && !fs.existsSync(appDir+app.readme)) ERROR(`App ${app.id} README file doesn't exist`); - if (app.custom && !fs.existsSync(appDir+app.custom)) ERROR(`App ${app.id} custom HTML doesn't exist`); - if (app.customConnect && !app.custom) ERROR(`App ${app.id} has customConnect but no customn HTML`); - if (app.interface && !fs.existsSync(appDir+app.interface)) ERROR(`App ${app.id} interface HTML doesn't exist`); + if (app.readme && !fs.existsSync(appDir+app.readme)) ERROR(`App ${app.id} README file doesn't exist`, {file:metadataFile}); + if (app.custom && !fs.existsSync(appDir+app.custom)) ERROR(`App ${app.id} custom HTML doesn't exist`, {file:metadataFile}); + if (app.customConnect && !app.custom) ERROR(`App ${app.id} has customConnect but no customn HTML`, {file:metadataFile}); + if (app.interface && !fs.existsSync(appDir+app.interface)) ERROR(`App ${app.id} interface HTML doesn't exist`, {file:metadataFile}); if (app.dependencies) { if (("object"==typeof app.dependencies) && !Array.isArray(app.dependencies)) { Object.keys(app.dependencies).forEach(dependency => { if (!["type","app"].includes(app.dependencies[dependency])) - ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`); + ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`, {file:metadataFile}); if (app.dependencies[dependency]=="type" && !METADATA_TYPES.includes(dependency)) - ERROR(`App ${app.id} 'type' dependency must be one of `+METADATA_TYPES); + ERROR(`App ${app.id} 'type' dependency must be one of `+METADATA_TYPES, {file:metadataFile}); }); } else - ERROR(`App ${app.id} 'dependencies' must be an object`); + ERROR(`App ${app.id} 'dependencies' must be an object`, {file:metadataFile}); } var fileNames = []; app.storage.forEach((file) => { - if (!file.name) ERROR(`App ${app.id} has a file with no name`); - if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`); + if (!file.name) ERROR(`App ${app.id} has a file with no name`, {file:metadataFile}); + if (isGlob(file.name)) ERROR(`App ${app.id} storage file ${file.name} contains wildcards`, {file:metadataFile}); let char = file.name.match(FORBIDDEN_FILE_NAME_CHARS) - if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`) + if (char) ERROR(`App ${app.id} storage file ${file.name} contains invalid character "${char[0]}"`, {file:metadataFile}) if (fileNames.includes(file.name) && !file.supports) // assume that there aren't duplicates if 'supports' is set - ERROR(`App ${app.id} file ${file.name} is a duplicate`); + ERROR(`App ${app.id} file ${file.name} is a duplicate`, {file:metadataFile}); if (file.supports && !Array.isArray(file.supports)) - ERROR(`App ${app.id} file ${file.name} supports field must be an array`); + ERROR(`App ${app.id} file ${file.name} supports field must be an array`, {file:metadataFile}); if (file.supports) file.supports.forEach(dev => { if (!SUPPORTS_DEVICES.includes(dev)) - ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`); + ERROR(`App ${app.id} file ${file.name} has unknown device in 'supports' field - ${dev}`, {file:metadataFile}); }); fileNames.push(file.name); allFiles.push({app: app.id, file: file.name}); - if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`); - if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`); + if (file.url) if (!fs.existsSync(appDir+file.url)) ERROR(`App ${app.id} file ${file.url} doesn't exist`, {file:metadataFile}); + if (!file.url && !file.content && !app.custom) ERROR(`App ${app.id} file ${file.name} has no contents`, {file:metadataFile}); var fileContents = ""; if (file.content) fileContents = file.content; if (file.url) fileContents = fs.readFileSync(appDir+file.url).toString(); - if (file.supports && !Array.isArray(file.supports)) ERROR(`App ${app.id} file ${file.name} supports field is not an array`); + if (file.supports && !Array.isArray(file.supports)) ERROR(`App ${app.id} file ${file.name} supports field is not an array`, {file:metadataFile}); if (file.evaluate) { try { acorn.parse("("+fileContents+")"); @@ -179,7 +194,7 @@ apps.forEach((app,appIdx) => { console.log("====================================================="); console.log(fileContents); console.log("====================================================="); - ERROR(`App ${app.id}'s ${file.name} has evaluate:true but is not valid JS expression`); + ERROR(`App ${app.id}'s ${file.name} has evaluate:true but is not valid JS expression`, {file:appDirRelative+file.url}); } } if (file.name.endsWith(".js")) { @@ -194,11 +209,11 @@ apps.forEach((app,appIdx) => { console.log("====================================================="); console.log(fileContents); console.log("====================================================="); - ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`); + ERROR(`App ${app.id}'s ${file.name} is a JS file but isn't valid JS`, {file:appDirRelative+file.url}); } } for (const key in file) { - if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`); + if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`, {file:appDirRelative+file.url}); } // warn if JS icon is the wrong size if (file.name == app.id+".img") { @@ -209,44 +224,44 @@ apps.forEach((app,appIdx) => { else { match = fileContents.match(/^\s*require\(\"heatshrink\"\)\.decompress\(\s*atob\(\s*\"([^"]*)\"\s*\)\s*\)\s*$/); if (match) icon = heatshrink.decompress(Buffer.from(match[1], 'base64')); - else ERROR(`JS icon ${file.name} does not match the pattern 'require("heatshrink").decompress(atob("..."))'`); + else ERROR(`JS icon ${file.name} does not match the pattern 'require("heatshrink").decompress(atob("..."))'`, {file:appDirRelative+file.url}); } if (match) { if (icon[0] > 48 || icon[0] < 24 || icon[1] > 48 || icon[1] < 24) { - if (GRANDFATHERED_ICONS.includes(app.id)) WARN(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`); - else ERROR(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`); + if (GRANDFATHERED_ICONS.includes(app.id)) WARN(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`, {file:appDirRelative+file.url}); + else ERROR(`JS icon ${file.name} should be 48x48px (or slightly under) but is instead ${icon[0]}x${icon[1]}px`, {file:appDirRelative+file.url}); } } } }); let dataNames = []; (app.data||[]).forEach((data)=>{ - if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`); + if (!data.name && !data.wildcard) ERROR(`App ${app.id} has a data file with no name`, {file:metadataFile}); if (dataNames.includes(data.name||data.wildcard)) - ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`); + ERROR(`App ${app.id} data file ${data.name||data.wildcard} is a duplicate`, {file:metadataFile}); dataNames.push(data.name||data.wildcard) allFiles.push({app: app.id, data: (data.name||data.wildcard)}); if ('name' in data && 'wildcard' in data) - ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`); + ERROR(`App ${app.id} data file ${data.name} has both name and wildcard`, {file:metadataFile}); if (isGlob(data.name)) - ERROR(`App ${app.id} data file name ${data.name} contains wildcards`); + ERROR(`App ${app.id} data file name ${data.name} contains wildcards`, {file:metadataFile}); if (data.wildcard) { if (!isGlob(data.wildcard)) - ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`); + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not actually contains wildcard`, {file:metadataFile}); if (data.wildcard.replace(/\?|\*/g,'') === '') - ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`); + ERROR(`App ${app.id} data file wildcard ${data.wildcard} does not contain regular characters`, {file:metadataFile}); else if (data.wildcard.replace(/\?|\*/g,'').length < 3) - WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`); + WARN(`App ${app.id} data file wildcard ${data.wildcard} is very broad`, {file:metadataFile}); else if (!data.wildcard.includes(app.id)) - WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`); + WARN(`App ${app.id} data file wildcard ${data.wildcard} does not include app ID`, {file:metadataFile}); } let char = (data.name||data.wildcard).match(FORBIDDEN_FILE_NAME_CHARS) - if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`) + if (char) ERROR(`App ${app.id} data file ${data.name||data.wildcard} contains invalid character "${char[0]}"`, {file:metadataFile}) if ('storageFile' in data && typeof data.storageFile !== 'boolean') - ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`); + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has non-boolean value for "storageFile"`, {file:metadataFile}); for (const key in data) { if (!DATA_KEYS.includes(key)) - ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`); + ERROR(`App ${app.id} data file ${data.name||data.wildcard} has unknown property "${key}"`, {file:metadataFile}); } }); // prefer "appid.json" over "appid.settings.json" (TODO: change to ERROR once all apps comply?) @@ -256,32 +271,35 @@ apps.forEach((app,appIdx) => { WARN(`App ${app.id} uses data file ${app.id+'.settings.json'}`)*/ // settings files should be listed under data, not storage (TODO: change to ERROR once all apps comply?) if (fileNames.includes(app.id+".settings.json")) - WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`) + WARN(`App ${app.id} uses storage file ${app.id+'.settings.json'} instead of data file`, {file:metadataFile}) if (fileNames.includes(app.id+".json")) - WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`) + WARN(`App ${app.id} uses storage file ${app.id+'.json'} instead of data file`, {file:metadataFile}) // warn if storage file matches data file of same app dataNames.forEach(dataName=>{ const glob = globToRegex(dataName) fileNames.forEach(fileName=>{ if (glob.test(fileName)) { - if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`) - else WARN(`App ${app.id} storage file ${fileName} is also listed in data`) + if (isGlob(dataName)) WARN(`App ${app.id} storage file ${fileName} matches data wildcard ${dataName}`, {file:metadataFile}) + else WARN(`App ${app.id} storage file ${fileName} is also listed in data`, {file:metadataFile}) } }) }) //console.log(fileNames); - if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`); - if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`); - if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`); + if (isApp && !fileNames.includes(app.id+".app.js")) ERROR(`App ${app.id} has no entrypoint`, {file:metadataFile}); + if (isApp && !fileNames.includes(app.id+".img")) ERROR(`App ${app.id} has no JS icon`, {file:metadataFile}); + if (app.type=="widget" && !fileNames.includes(app.id+".wid.js")) ERROR(`Widget ${app.id} has no entrypoint`, {file:metadataFile}); for (const key in app) { - if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`); + if (!APP_KEYS.includes(key)) ERROR(`App ${app.id} has unknown key ${key}`, {file:metadataFile}); } }); + + // Do not allow files from different apps to collide let fileA + while(fileA=allFiles.pop()) { if (VALID_DUPLICATES.includes(fileA.file)) - return; + break; const nameA = (fileA.file||fileA.data), globA = globToRegex(nameA), typeA = fileA.file?'storage':'data' @@ -291,9 +309,16 @@ while(fileA=allFiles.pop()) { typeB = fileB.file?'storage':'data' if (globA.test(nameB)||globB.test(nameA)) { if (isGlob(nameA)||isGlob(nameB)) - ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`) + ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`); else if (fileA.app != fileB.app) - WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`) + WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`); } }) } + +console.log("=================================="); +console.log(`${errorCount} errors, ${warningCount} warnings`); +console.log("=================================="); +if (errorCount) { + process.exit(1); +} From 233b0dc8fc9d1247bea69e3223af4980cfbd954d Mon Sep 17 00:00:00 2001 From: Marco Heiming Date: Tue, 28 Jun 2022 13:00:26 +0200 Subject: [PATCH 06/23] Fix exception --- apps/widbaroalarm/ChangeLog | 1 + apps/widbaroalarm/metadata.json | 2 +- apps/widbaroalarm/widget.js | 16 ++++++++-------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog index f0ab89928..2dfe8336d 100644 --- a/apps/widbaroalarm/ChangeLog +++ b/apps/widbaroalarm/ChangeLog @@ -6,3 +6,4 @@ 0.05: Fix warning calculation Show difference of last measurement to pressure average of the the last three hours in the widget Only use valid pressure values +0.06: Fix exception diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json index 17630eaa8..ba6c47b37 100644 --- a/apps/widbaroalarm/metadata.json +++ b/apps/widbaroalarm/metadata.json @@ -2,7 +2,7 @@ "id": "widbaroalarm", "name": "Barometer Alarm Widget", "shortName": "Barometer Alarm", - "version": "0.05", + "version": "0.06", "description": "A widget that can alarm on when the pressure reaches defined thresholds.", "icon": "widget.png", "type": "widget", diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js index 2febd1eb2..d877c4384 100644 --- a/apps/widbaroalarm/widget.js +++ b/apps/widbaroalarm/widget.js @@ -231,15 +231,15 @@ function getPressureValue() { if (isValidPressureValue(pressure)) { currentPressures.unshift(pressure); median = currentPressures.slice().sort(); - } - if (median.length > 10) { - var mid = median.length >> 1; - medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); - if (medianPressure > 0) { - turnOff(); - draw(); - handlePressureValue(medianPressure); + if (median.length > 10) { + var mid = median.length >> 1; + medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); + if (medianPressure > 0) { + turnOff(); + draw(); + handlePressureValue(medianPressure); + } } } }); From f3d309170675929ec667f0d44dba88e3eea83028 Mon Sep 17 00:00:00 2001 From: Lubomir Date: Tue, 28 Jun 2022 22:27:22 +1000 Subject: [PATCH 07/23] Remove pointless functions --- apps/messages/lib.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 90847ccb0..3c6fbc880 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -105,13 +105,6 @@ exports.clearAll = function(event) { WIDGETS.messages.update(messages); } -function getNotificationImage() { - return atob("GBIBP//8H//4n//5z//z5//n8//H+P+f/H4//n5//Dw/+Jkf8YGP48PH5//nz//zn//5H//4P//8"); -} -function getFBIcon() { - return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); -} - exports.getMessageImage = function(msg) { /* * icons should be 24x24px with 1bpp colors and 'Transparency to Color' From 6ad485c6287f15fb1f00f01859d2542e7677f50f Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 28 Jun 2022 13:39:46 +0100 Subject: [PATCH 08/23] Fix issue with >1 layout button on Bangle.js 2: http://forum.espruino.com/conversations/377235/#comment16576403 --- modules/Layout.js | 2 +- modules/Layout.min.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/Layout.js b/modules/Layout.js index fd5809a93..5b0d8b9f9 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -37,9 +37,9 @@ function Layout(layout, options) { if (this.options.btns) { var buttons = this.options.btns; - this.b = buttons; if (this.physBtns >= buttons.length) { // enough physical buttons + this.b = buttons; let btnHeight = Math.floor(Bangle.appRect.h / this.physBtns); if (this.physBtns > 2 && buttons.length==1) buttons.unshift({label:""}); // pad so if we have a button in the middle diff --git a/modules/Layout.min.js b/modules/Layout.min.js index 4523c547c..400cd4820 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -1,4 +1,4 @@ -function p(b,k){function d(h){h.id&&(f[h.id]=h);h.type||(h.type="");h.c&&h.c.forEach(d)}this._l=this.l=b;this.physBtns=2==process.env.HWVERSION?1:3;this.options=k||{};this.lazy=this.options.lazy||!1;if(2!=process.env.HWVERSION){var a=[];function h(m){"btn"==m.type&&a.push(m);m.c&&m.c.forEach(h)}h(b);a.length&&(this.physBtns=0,this.buttons=a,this.selectedButton=-1)}if(this.options.btns)if(this.b=b=this.options.btns,this.physBtns>=b.length){let h=Math.floor(Bangle.appRect.h/ +function p(b,k){function d(h){h.id&&(f[h.id]=h);h.type||(h.type="");h.c&&h.c.forEach(d)}this._l=this.l=b;this.physBtns=2==process.env.HWVERSION?1:3;this.options=k||{};this.lazy=this.options.lazy||!1;if(2!=process.env.HWVERSION){var a=[];function h(m){"btn"==m.type&&a.push(m);m.c&&m.c.forEach(h)}h(b);a.length&&(this.physBtns=0,this.buttons=a,this.selectedButton=-1)}if(this.options.btns)if(b=this.options.btns,this.physBtns>=b.length){this.b=b;let h=Math.floor(Bangle.appRect.h/ this.physBtns);for(2b.length;)b.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:b.map(m=>(m.type="txt",m.font="6x8",m.height=h,m.r=1,m))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:b.map(h=>(h.type="btn",h.filly=1,h.width=32,h.r=1,h))}]},a&&a.push.apply(a,this._l.c[1].c);this.setUI();var f=this;d(this._l);this.updateNeeded=!0}function r(b, k,d,a,f){var h=null==b.bgCol?f:g.toColor(b.bgCol);if(h!=f||"txt"==b.type||"btn"==b.type||"img"==b.type||"custom"==b.type){var m=b.c;delete b.c;var c="H"+E.CRC32(E.toJS(b));m&&(b.c=m);delete k[c]||((a[c]=[b.x,b.y,b.x+b.w-1,b.y+b.h-1]).bg=null==f?g.theme.bg:f,d&&(d.push(b),d=null))}if(b.c)for(var l of b.c)r(l,k,d,a,h)}p.prototype.setUI=function(){Bangle.setUI();let b;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back},k=>{var d=this.selectedButton,a=this.buttons.length;if(void 0===k&& this.buttons[d])return this.buttons[d].cb();this.buttons[d]&&(delete this.buttons[d].selected,this.render(this.buttons[d]));d=(d+a+k)%a;this.buttons[d]&&(this.buttons[d].selected=1,this.render(this.buttons[d]));this.selectedButton=d}),b=!0);this.options.back&&!b&&Bangle.setUI({mode:"custom",back:this.options.back});if(this.b){function k(d,a){.75e+(0|n.filly),0);c||(h b.h-b.pad);k++;b.c&&b.c.forEach(d=>this.debug(d,k))};p.prototype.update=function(){function b(a){"ram";k[a.type](a);if(a.r&1){var f=a._w;a._w=a._h;a._h=f}a._w=0|Math.max(a._w+(a.pad<<1),0|a.width);a._h=0|Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var k={txt:function(a){a.font.endsWith("%")&&(a.font="Vector"+Math.round(g.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var f=g.setFont(a.font).stringMetrics(a.label);a._w=f.width;a._h=f.height}},btn:function(a){a.font&& a.font.endsWith("%")&&(a.font="Vector"+Math.round(g.getHeight()*a.font.slice(0,-1)/100));var f=a.src?g.imageMetrics("function"==typeof a.src?a.src():a.src):g.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+f.height;a._w=20+f.width},img:function(a){var f=g.imageMetrics("function"==typeof a.src?a.src():a.src),h=a.scale||1;a._w=f.width*h;a._h=f.height*h},"":function(a){a._w=0;a._h=0},custom:function(a){a._w=0;a._h=0},h:function(a){a.c.forEach(b);a._h=a.c.reduce((f,h)=>Math.max(f,h._h),0);a._w= a.c.reduce((f,h)=>f+h._w,0);null==a.fillx&&a.c.some(f=>f.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(f=>f.filly)&&(a.filly=1)},v:function(a){a.c.forEach(b);a._h=a.c.reduce((f,h)=>f+h._h,0);a._w=a.c.reduce((f,h)=>Math.max(f,h._w),0);null==a.fillx&&a.c.some(f=>f.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(f=>f.filly)&&(a.filly=1)}},d=this._l;b(d);d.fillx||d.filly?(d.w=Bangle.appRect.w,d.h=Bangle.appRect.h,d.x=Bangle.appRect.x,d.y=Bangle.appRect.y):(d.w=d._w,d.h=d._h,d.x=Bangle.appRect.w-d.w>>1,d.y= -Bangle.appRect.y+(Bangle.appRect.h-d.h>>1));this.layout(d)};p.prototype.clear=function(b){b||(b=this._l);g.reset();void 0!==b.bgCol&&g.setBgColor(b.bgCol);g.clearRect(b.x,b.y,b.x+b.w-1,b.y+b.h-1)};exports=p +Bangle.appRect.y+(Bangle.appRect.h-d.h>>1));this.layout(d)};p.prototype.clear=function(b){b||(b=this._l);g.reset();void 0!==b.bgCol&&g.setBgColor(b.bgCol);g.clearRect(b.x,b.y,b.x+b.w-1,b.y+b.h-1)};exports=p \ No newline at end of file From 85dad6138650b16e3f00f21b9f432047ec95a300 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 28 Jun 2022 15:31:57 +0100 Subject: [PATCH 09/23] android 0.13: Added Bangle.http function (see Readme file for more info) --- apps/android/ChangeLog | 1 + apps/android/README.md | 19 +++++++++++++++++++ apps/android/boot.js | 39 ++++++++++++++++++++++++++++++++++++++ apps/android/metadata.json | 2 +- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index acdeeaaa0..42dc5c97d 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -10,3 +10,4 @@ 0.09: Alarm vibration, repeat, and auto-snooze now handled by sched 0.10: Fix SMS bug 0.12: Use default Bangle formatter for booleans +0.13: Added Bangle.http function (see Readme file for more info) diff --git a/apps/android/README.md b/apps/android/README.md index c10718aac..f9ab73699 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -32,6 +32,25 @@ Responses are sent back to Gadgetbridge simply as one line of JSON. More info on message formats on http://www.espruino.com/Gadgetbridge +## Functions provided + +The boot code also provides some useful functions: + +* `Bangle.messageResponse = function(msg,response)` - send a yes/no response to a message. `msg` is a message object, and `response` is a boolean. +* `Bangle.musicControl = function(cmd)` - control music, cmd = `play/pause/next/previous/volumeup/volumedown` +* `Bangle.http = function(url,options)` - make an HTTPS request to a URL and return a promise with the data. Requires the [internet enabled `Bangle.js Gadgetbridge` app](http://www.espruino.com/Gadgetbridge#http-requests). `options` can contain: + * `id` - a custom (string) ID + * `timeout` - a timeout for the request in milliseconds (default 30000ms) + * `xpath` an xPath query to run on the request (but right now the URL requested must be XML - HTML is rarely XML compliant) + +eg: + +``` +Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{ + console.log("Got ",data); +}); +``` + ## Testing Bangle.js can only hold one connection open at a time, so it's hard to see diff --git a/apps/android/boot.js b/apps/android/boot.js index 9cdc019a6..1717bf812 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -118,11 +118,50 @@ var cal = require("Storage").readJSON("android.calendar.json",true); if (!cal || !Array.isArray(cal)) cal = []; gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)}); + }, + "http":function() { + //get the promise and call the promise resolve + if (Bangle.httpRequest === undefined) return; + var objID=Bangle.httpRequest[event.id]; + if (objID === undefined) return; //already timedout or wrong id + delete Bangle.httpRequest[event.id]; + clearInterval(objID.t); //t = timeout variable + if(event.err!==undefined) //if is error + objID.j(event.err); //r = reJect function + else + objID.r(event); //r = resolve function } }; var h = HANDLERS[event.t]; if (h) h(); else console.log("GB Unknown",event); }; + // HTTP request handling - see the readme + // options = {id,timeout,xpath} + Bangle.http = (url,options)=>{ + options = options||{}; + if (Bangle.httpRequest === undefined) + Bangle.httpRequest={}; + if (options.id === undefined) { + // try and create a unique ID + do { + options.id = Math.random().toString().substr(2); + } while( Bangle.httpRequest[options.id]!==undefined); + } + //send the request + var req = {t: "http", url:url, id:options.id}; + if (options.xpath) req.xpath = options.xpath; + gbSend(req); + //create the promise + var promise = new Promise(function(resolve,reject) { + //save the resolve function in the dictionary and create a timeout (30 seconds default) + Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{ + //if after "timeoutMillisec" it still hasn't answered -> reject + delete Bangle.httpRequest[options.id]; + reject("Timeout"); + },options.timeout||30000)}; + }); + return promise; + } // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } diff --git a/apps/android/metadata.json b/apps/android/metadata.json index ec8b8b0fe..84b28b7a2 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.12", + "version": "0.13", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", From 828f9821adf85c817a1f7fdf57ad2af674d258b1 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Wed, 29 Jun 2022 09:16:45 +0100 Subject: [PATCH 10/23] Fix issue caused by minification (moving the declaration of 'var btnList/a' into an 'if' statement). Now we can use 'let' and the minifier doesn't try and do stupid things with that --- modules/Layout.js | 5 ++--- modules/Layout.min.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/Layout.js b/modules/Layout.js index 5b0d8b9f9..fdcf0ceae 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -10,15 +10,14 @@ */ -function Layout(layout, options) { +function Layout(layout, options) { this._l = this.l = layout; // Do we have >1 physical buttons? this.physBtns = (process.env.HWVERSION==2) ? 1 : 3; this.options = options || {}; this.lazy = this.options.lazy || false; - - var btnList; + let btnList; if (process.env.HWVERSION!=2) { // no touchscreen, find any buttons in 'layout' btnList = []; diff --git a/modules/Layout.min.js b/modules/Layout.min.js index 400cd4820..b5a924358 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -1,4 +1,4 @@ -function p(b,k){function d(h){h.id&&(f[h.id]=h);h.type||(h.type="");h.c&&h.c.forEach(d)}this._l=this.l=b;this.physBtns=2==process.env.HWVERSION?1:3;this.options=k||{};this.lazy=this.options.lazy||!1;if(2!=process.env.HWVERSION){var a=[];function h(m){"btn"==m.type&&a.push(m);m.c&&m.c.forEach(h)}h(b);a.length&&(this.physBtns=0,this.buttons=a,this.selectedButton=-1)}if(this.options.btns)if(b=this.options.btns,this.physBtns>=b.length){this.b=b;let h=Math.floor(Bangle.appRect.h/ +function p(b,k){function d(h){h.id&&(f[h.id]=h);h.type||(h.type="");h.c&&h.c.forEach(d)}this._l=this.l=b;this.physBtns=2==process.env.HWVERSION?1:3;this.options=k||{};this.lazy=this.options.lazy||!1;let a;if(2!=process.env.HWVERSION){a=[];function h(m){"btn"==m.type&&a.push(m);m.c&&m.c.forEach(h)}h(b);a.length&&(this.physBtns=0,this.buttons=a,this.selectedButton=-1)}if(this.options.btns)if(b=this.options.btns,this.physBtns>=b.length){this.b=b;let h=Math.floor(Bangle.appRect.h/ this.physBtns);for(2b.length;)b.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:b.map(m=>(m.type="txt",m.font="6x8",m.height=h,m.r=1,m))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:b.map(h=>(h.type="btn",h.filly=1,h.width=32,h.r=1,h))}]},a&&a.push.apply(a,this._l.c[1].c);this.setUI();var f=this;d(this._l);this.updateNeeded=!0}function r(b, k,d,a,f){var h=null==b.bgCol?f:g.toColor(b.bgCol);if(h!=f||"txt"==b.type||"btn"==b.type||"img"==b.type||"custom"==b.type){var m=b.c;delete b.c;var c="H"+E.CRC32(E.toJS(b));m&&(b.c=m);delete k[c]||((a[c]=[b.x,b.y,b.x+b.w-1,b.y+b.h-1]).bg=null==f?g.theme.bg:f,d&&(d.push(b),d=null))}if(b.c)for(var l of b.c)r(l,k,d,a,h)}p.prototype.setUI=function(){Bangle.setUI();let b;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back},k=>{var d=this.selectedButton,a=this.buttons.length;if(void 0===k&& this.buttons[d])return this.buttons[d].cb();this.buttons[d]&&(delete this.buttons[d].selected,this.render(this.buttons[d]));d=(d+a+k)%a;this.buttons[d]&&(this.buttons[d].selected=1,this.render(this.buttons[d]));this.selectedButton=d}),b=!0);this.options.back&&!b&&Bangle.setUI({mode:"custom",back:this.options.back});if(this.b){function k(d,a){.75 Date: Wed, 29 Jun 2022 19:14:00 +0200 Subject: [PATCH 11/23] Verison 0.12 - Implements a 2D menu. --- apps/bwclk/ChangeLog | 3 +- apps/bwclk/README.md | 35 ++++- apps/bwclk/app.js | 269 +++++++++++++++++++++++---------------- apps/bwclk/metadata.json | 2 +- 4 files changed, 191 insertions(+), 118 deletions(-) diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index ef4edcff0..7cc79e2cc 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -8,4 +8,5 @@ 0.08: Select the color of widgets correctly. Additional settings to hide colon. 0.09: Larger font size if colon is hidden to improve readability further. 0.10: HomeAssistant integration if HomeAssistant is installed. -0.11: Performance improvements. \ No newline at end of file +0.11: Performance improvements. +0.12: Implements a 2D menu. \ No newline at end of file diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index 190488d6b..97b282046 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -3,16 +3,39 @@ ![](screenshot.png) ## Features +A very minimalistic clock keeping date and time in focus. Nevertheless, a +2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger. + +Simply click left / right to go through the menu entries such as Bangle, Timer etc. +and click up/down to move into this sub-menu. You can then click in the middle of the screen +to e.g. send a trigger via HomeAssistant once you selected it. + +``` + Bpm ... + | | + Steps 10 min. ... ... + | | | | + Battery 5 min Temp. Trigger1 + | | | | + Bangle -- Timer[Optional] -- Weather[Optional] -- HomeAssistant [Optional] +``` + +The following list shows which apps must be installed in order to get the optional menu entries: +- Timer - Sched lib +- Weather - Weather app +- HomeAssistant - HomeAssistant app + + +## Other settings - Fullscreen on/off -- Tab left/right of screen to show steps, temperature etc. -- Enable / disable lock icon in the settings. -- If the "sched" app is installed tab top / bottom of the screen to set the timer. -- If HomeAssistant is installed, triggers are shown. Simple select the trigger and touch the middle of the screen to send the trigger to HomeAssistant. -- The design is adapted to the theme of your bangle. -- The colon (e.g. 7:35 = 735) can be hidden now in the settings. +- Enable/disable lock icon in the settings +- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font +- The design of your bangle sys settings is used (e.g. you can also set a blue background) + ## Thanks to Icons created by Flaticon + ## Creator - [David Peer](https://github.com/peerdavid) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 4c4359a4f..5e2fb5eaa 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -1,10 +1,10 @@ -/* +/************ * Includes */ const locale = require('locale'); const storage = require('Storage'); -/* +/************ * Statics */ const SETTINGS_FILE = "bwclk.setting.json"; @@ -12,14 +12,15 @@ const TIMER_IDX = "bwclk"; const W = g.getWidth(); const H = g.getHeight(); -/* +/************ * Settings */ let settings = { fullscreen: false, showLock: true, hideColon: false, - showInfo: 0, + menuPosX: 0, + menuPosY: 0, }; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; @@ -28,11 +29,21 @@ for (const key in saved_settings) { } -/* +/************ * Assets */ - // Manrope font +Graphics.prototype.setXLargeFont = function(scale) { + // Actual height 53 (55 - 3) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('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'))), + 46, + atob("FR4uHyopKyksJSssGA=="), + 70+(scale<<8)+(1<<16) + ); +}; + + Graphics.prototype.setLargeFont = function(scale) { // Actual height 48 (49 - 2) this.setFontCustom( @@ -44,15 +55,6 @@ Graphics.prototype.setLargeFont = function(scale) { return this; }; -Graphics.prototype.setXLargeFont = function(scale) { - // Actual height 53 (55 - 3) - this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('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'))), - 46, - atob("FR4uHyopKyksJSssGA=="), - 70+(scale<<8)+(1<<16) - ); -}; Graphics.prototype.setMediumFont = function(scale) { // Actual height 41 (42 - 2) @@ -60,6 +62,7 @@ Graphics.prototype.setMediumFont = function(scale) { return this; }; + Graphics.prototype.setSmallFont = function(scale) { // Actual height 28 (27 - 0) this.setFontCustom( @@ -71,6 +74,7 @@ Graphics.prototype.setSmallFont = function(scale) { return this; }; + function imgLock(){ return { width : 16, height : 16, bpp : 1, @@ -111,6 +115,14 @@ function imgTemperature() { } } +function imgWeather(){ + return { + width : 24, height : 24, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AAcYAQ0MgEwAQUAngLB/8AgP/wACCgf/4Fz//OAQQICCIoaCEAQpGHA4ACA=")) + } +} + function imgWind () { return { width : 24, height : 24, bpp : 1, @@ -127,14 +139,6 @@ function imgTimer() { } } -function imgCharging() { - return { - width : 24, height : 24, bpp : 1, - transparent : 1, - buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA=")) - } -} - function imgWatch() { return { width : 24, height : 24, bpp : 1, @@ -143,56 +147,102 @@ function imgWatch() { } } +function imgHomeAssistant() { + return { + width : 48, height : 48, bpp : 1, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AD8BwAFDg/gAocP+AFDj4FEn/8Aod//wFD/1+FAf4j+8AoMD+EPDAUH+OPAoUP+fPAoUfBYk/C4l/EYIwC//8n//FwIFEgYFD4EH+E8nkP8BdBAonjjk44/wj/nzk58/4gAFDF4PgCIMHAoPwhkwh4FB/EEkEfIIWAHwIFC4A+BAoXgg4FDL4IFDL4IFDLIYFkAEQA==")) + } +} -/* - * INFO ENTRIES - * List of [Data, Icon, left/right, Function to execute] + +/************ + * 2D MENU with entries of: + * [name, icon, opt[customUpFun], opt[customDownFun], opt[customCenterFun]] + * + * An example is shown below: + * + * Bpm ... + * | | + * Steps 10 min. ... ... + * | | | | + * Battery 5-min Temp. Trigger1 + * | | | | + * BangleJs -- Timer -- Weather[Optional] -- HomeAssistant [Optional] */ -var infoArray = [ - function(){ return [ null, null, "left", null ] }, - function(){ return [ "Bangle", imgWatch(), "right", null ] }, - function(){ return [ E.getBattery() + "%", imgBattery(), "left", null ] }, - function(){ return [ getSteps(), imgSteps(), "left", null ] }, - function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm(), "left", null] }, - function(){ return [ getWeather().temp, imgTemperature(), "left", null ] }, - function(){ return [ getWeather().wind, imgWind(), "left", null ] }, -]; +var menu = [ + [ + function(){ return [ null, null ] }, + ], + [ + function(){ return [ "Bangle", imgWatch() ] }, + function(){ return [ E.getBattery() + (Bangle.isCharging() ? "% ++" : "%"), imgBattery() ] }, + function(){ return [ getSteps(), imgSteps() ] }, + function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] }, + ] +] /* - * We append the HomeAssistant integrations if HomeAssistant is available + * Timer Menu + */ +try{ + require('sched'); + menu.push([ + function(){ + var text = isAlarmEnabled() ? "T-" + getAlarmMinutes() + " min." : "Timer"; + return [text, imgTimer(), () => increaseAlarm(), () => decreaseAlarm(), null ] + }, + ]); +} catch(ex) { + // If sched is not installed, we hide this menu item +} + +/* + * WEATHER MENU + */ +if(storage.readJSON('weather.json') !== undefined){ + menu.push([ + function(){ return [ "Weather", imgWeather() ] }, + function(){ return [ getWeather().temp, imgTemperature() ] }, + function(){ return [ getWeather().wind, imgWind() ] }, + ]); +} + + +/* + * HOME ASSISTANT MENU */ try{ var triggers = require("ha.lib.js").getTriggers(); + var haMenu = [ + function(){ return [ "Home", imgHomeAssistant() ] }, + ]; + triggers.forEach(trigger => { - infoArray.push(function(){ - return [trigger.display, trigger.getIcon(), "left", function(){ + haMenu.push(function(){ + return [trigger.display, trigger.getIcon(), () => {}, () => {}, function(){ var ha = require("ha.lib.js"); ha.sendTrigger("TRIGGER_BW"); ha.sendTrigger(trigger.trigger); }] }); }) + menu.push(haMenu); } catch(ex){ - // Nothing to do if HomeAssistant is not available... -} -const NUM_INFO=infoArray.length; - - -function getInfoEntry(){ - if(isAlarmEnabled()){ - return [getAlarmMinutes() + " min.", imgTimer(), "left", null] - } else if(Bangle.isCharging()){ - return [E.getBattery() + "%", imgCharging(), "left", null] - } else{ - // In case the user removes HomeAssistant entries, showInfo - // could be larger than infoArray.length... - settings.showInfo = settings.showInfo % infoArray.length; - return infoArray[settings.showInfo](); - } + // If HomeAssistant is not installed, we hide this item } -/* +function getMenuEntry(){ + // In case the user removes HomeAssistant entries, showInfo + // could be larger than infoArray.length... + settings.menuPosX = settings.menuPosX % menu.length; + settings.menuPosY = settings.menuPosY % menu[settings.menuPosX].length; + return menu[settings.menuPosX][settings.menuPosY](); +} + + +/************ * Helper */ function getSteps() { @@ -215,38 +265,20 @@ function getSteps() { function getWeather(){ - var weatherJson; + var weatherJson = storage.readJSON('weather.json'); + var weather = weatherJson.weather; - try { - weatherJson = storage.readJSON('weather.json'); - var weather = weatherJson.weather; + weather.temp = locale.temp(weather.temp-273.15); - // Temperature - weather.temp = locale.temp(weather.temp-273.15); + weather.hum = weather.hum + "%"; - // Humidity - weather.hum = weather.hum + "%"; + var wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); + weather.wind = Math.round(wind[1]) + " km/h"; - // Wind - const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - weather.wind = Math.round(wind[1]) + " km/h"; - - return weather - - } catch(ex) { - // Return default - } - - return { - temp: "? °C", - hum: "-", - txt: "-", - wind: "? km/h", - wdir: "-", - wrose: "-" - }; + return weather } + function isAlarmEnabled(){ try{ var alarm = require('sched'); @@ -261,6 +293,7 @@ function isAlarmEnabled(){ return false; } + function getAlarmMinutes(){ if(!isAlarmEnabled()){ return -1; @@ -271,6 +304,7 @@ function getAlarmMinutes(){ return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); } + function increaseAlarm(){ try{ var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; @@ -282,6 +316,7 @@ function increaseAlarm(){ } catch(ex){ } } + function decreaseAlarm(){ try{ var minutes = getAlarmMinutes(); @@ -301,10 +336,9 @@ function decreaseAlarm(){ } -/* - * DRAW functions +/************ + * DRAW */ - function draw() { // Queue draw again queueDraw(); @@ -338,13 +372,12 @@ function drawDate(){ var fullDateW = dateW + 10 + dayW; g.setFontAlign(-1,0); + g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12); + g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11); + g.setMediumFont(); g.setColor(g.theme.fg); g.drawString(dateStr, W/2 - fullDateW / 2, y+1); - - g.setSmallFont(); - g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12); - g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11); } @@ -368,13 +401,13 @@ function drawTime(){ // Set y coordinates correctly y += parseInt((H - y)/2) + 5; - var infoEntry = getInfoEntry(); - var infoStr = infoEntry[0]; - var infoImg = infoEntry[1]; - var printImgLeft = infoEntry[2] == "left"; + var menuEntry = getMenuEntry(); + var menuName = menuEntry[0]; + var menuImg = menuEntry[1]; + var printImgLeft = settings.menuPosY != 0; // Show large or small time depending on info entry - if(infoStr == null){ + if(menuName == null){ if(settings.hideColon){ g.setXLargeFont(); } else { @@ -386,8 +419,8 @@ function drawTime(){ } g.drawString(timeStr, W/2, y); - // Draw info if set - if(infoStr == null){ + // Draw menu if set + if(menuName == null){ return; } @@ -395,18 +428,18 @@ function drawTime(){ g.setFontAlign(0,0); g.setSmallFont(); var imgWidth = 0; - if(infoImg !== undefined){ - imgWidth = 26.0; - var strWidth = g.stringWidth(infoStr); - var scale = imgWidth / infoImg.width; + if(menuImg !== undefined){ + imgWidth = 24.0; + var strWidth = g.stringWidth(menuName); + var scale = imgWidth / menuImg.width; g.drawImage( - infoImg, + menuImg, W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - parseInt(imgWidth/2), y - parseInt(imgWidth/2), { scale: scale } ); } - g.drawString(infoStr, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3); + g.drawString(menuName, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3); } @@ -480,36 +513,52 @@ Bangle.on('touch', function(btn, e){ if(is_upper){ Bangle.buzz(40, 0.6); - increaseAlarm(); + settings.menuPosY = (settings.menuPosY+1) % menu[settings.menuPosX].length; + + // Handle custom menu entry function + var menuEntry = getMenuEntry(); + if(menuEntry.length > 2){ + menuEntry[2](); + } + drawTime(); } if(is_lower){ Bangle.buzz(40, 0.6); - decreaseAlarm(); + settings.menuPosY = settings.menuPosY-1; + settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].length-1 : settings.menuPosY; + + // Handle custom menu entry function + var menuEntry = getMenuEntry(); + if(menuEntry.length > 3){ + menuEntry[3](); + } + drawTime(); } if(is_right){ Bangle.buzz(40, 0.6); - settings.showInfo = (settings.showInfo+1) % NUM_INFO; + settings.menuPosX = (settings.menuPosX+1) % menu.length; + settings.menuPosY = 0; drawTime(); } if(is_left){ Bangle.buzz(40, 0.6); - settings.showInfo = settings.showInfo-1; - settings.showInfo = settings.showInfo < 0 ? NUM_INFO-1 : settings.showInfo; + settings.menuPosY = 0; + settings.menuPosX = settings.menuPosX-1; + settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; drawTime(); } if(is_center){ - var infoEntry = getInfoEntry(); - var fun = infoEntry[3]; - if(fun != null){ + var menuEntry = getMenuEntry(); + if(menuEntry.length > 4){ Bangle.buzz(80, 0.6).then(()=>{ try{ - fun(); + menuEntry[4](); setTimeout(()=>{ Bangle.buzz(80, 0.6); }, 250); diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index 834712743..4c0335df8 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,7 +1,7 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.11", + "version": "0.12", "description": "BW Clock.", "readme": "README.md", "icon": "app.png", From 3bf5407dff3cf7af81ec871a739dc0441424a0f3 Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 19:15:00 +0200 Subject: [PATCH 12/23] Minor improvement --- apps/bwclk/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index 97b282046..a789542a7 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -21,9 +21,9 @@ to e.g. send a trigger via HomeAssistant once you selected it. ``` The following list shows which apps must be installed in order to get the optional menu entries: -- Timer - Sched lib -- Weather - Weather app -- HomeAssistant - HomeAssistant app +- Timer - The Scheduler library must be installed +- Weather - The Weather app must be installed +- HomeAssistant - The HomeAssistant app must be installed ## Other settings From b5e691a53806e00077dd95000b5fd6b1eb88b0ee Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 19:25:34 +0200 Subject: [PATCH 13/23] Stability improvement --- apps/bwclk/app.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 5e2fb5eaa..5c225e160 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -265,17 +265,37 @@ function getSteps() { function getWeather(){ - var weatherJson = storage.readJSON('weather.json'); - var weather = weatherJson.weather; + var weatherJson; - weather.temp = locale.temp(weather.temp-273.15); + try { + weatherJson = storage.readJSON('weather.json'); + var weather = weatherJson.weather; - weather.hum = weather.hum + "%"; + // Temperature + weather.temp = locale.temp(weather.temp-273.15); - var wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - weather.wind = Math.round(wind[1]) + " km/h"; + // Humidity + weather.hum = weather.hum + "%"; - return weather + // Wind + const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); + var speedFactor = settings.speed == "kph" ? 1.0 : 1.0 / 1.60934; + weather.wind = Math.round(wind[1] * speedFactor); + + return weather + + } catch(ex) { + // Return default + } + + return { + temp: " ? ", + hum: " ? ", + txt: " ? ", + wind: " ? ", + wdir: " ? ", + wrose: " ? " + }; } From 775929753854a7ff74d6551fc38adddf9d2f1a42 Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 19:33:23 +0200 Subject: [PATCH 14/23] Minor improvement for charging state --- apps/bwclk/app.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 5c225e160..1f7d4bf00 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -516,6 +516,10 @@ Bangle.on('lock', function(isLocked) { Bangle.on('charging',function(charging) { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; + + // Jump to battery + settings.menuPosX = 1; + settings.menuPosY = 1; draw(); }); From a840b1a14a8f97b01f540fd9aa2d77cfe6ff9bd1 Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 19:35:45 +0200 Subject: [PATCH 15/23] Change charging icon --- apps/bwclk/app.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 1f7d4bf00..11b17b1b6 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -99,6 +99,14 @@ function imgBattery(){ } } +function imgCharging() { + return { + width : 24, height : 24, bpp : 1, + transparent : 1, + buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA=")) + } +} + function imgBpm() { return { width : 24, height : 24, bpp : 1, @@ -176,7 +184,7 @@ var menu = [ ], [ function(){ return [ "Bangle", imgWatch() ] }, - function(){ return [ E.getBattery() + (Bangle.isCharging() ? "% ++" : "%"), imgBattery() ] }, + function(){ return [ E.getBattery(), Bangle.isCharging() ? imgCharging() : imgBattery() ] }, function(){ return [ getSteps(), imgSteps() ] }, function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] }, ] From 4a6bfad90d06c8625c09dfa2d7af5385c1c8f0bb Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 19:36:00 +0200 Subject: [PATCH 16/23] Minor change --- apps/bwclk/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 11b17b1b6..241b4b01a 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -184,7 +184,7 @@ var menu = [ ], [ function(){ return [ "Bangle", imgWatch() ] }, - function(){ return [ E.getBattery(), Bangle.isCharging() ? imgCharging() : imgBattery() ] }, + function(){ return [ E.getBattery() + "%", Bangle.isCharging() ? imgCharging() : imgBattery() ] }, function(){ return [ getSteps(), imgSteps() ] }, function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] }, ] From d169dd94ec725f6a0054c82c7ffef46e51e659cc Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 19:43:15 +0200 Subject: [PATCH 17/23] Minor improvement --- apps/bwclk/app.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 241b4b01a..5b9244a54 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -287,8 +287,7 @@ function getWeather(){ // Wind const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - var speedFactor = settings.speed == "kph" ? 1.0 : 1.0 / 1.60934; - weather.wind = Math.round(wind[1] * speedFactor); + weather.wind = Math.round(wind[1]) + "kph"; return weather From fe9e98230606585ab86a78d83ca63e5ad1025030 Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 22:03:10 +0200 Subject: [PATCH 18/23] Minor design improvements --- apps/bwclk/README.md | 27 +++++++++++++++------------ apps/bwclk/app.js | 14 +++++++------- apps/bwclk/metadata.json | 2 +- apps/bwclk/screenshot.png | Bin 2725 -> 2945 bytes apps/bwclk/screenshot_2.png | Bin 3043 -> 3215 bytes apps/bwclk/screenshot_3.png | Bin 3133 -> 3312 bytes apps/bwclk/screenshot_4.png | Bin 3389 -> 3283 bytes 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index a789542a7..5d0336b3b 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -1,9 +1,18 @@ # BW Clock +A very minimalistic clock with date and time in focus. ![](screenshot.png) ## Features -A very minimalistic clock keeping date and time in focus. Nevertheless, a +Altough date and time is the most important thing, the BW clock provides many features +as well as 3rd party integtations. The following things are integrated: +- Bangle: Steps, Heart rate, Battery (including charging state) +- Timer: +/- 5 min. Note: The Scheduler library must be installed, otherwise it's hidden +- Weather: Temperature, Wind. Note: The Weather app must be installed, otherwise it's hidden. +- HomeAssistant - All triggers are shown. Note: The HomeAssistant app must be installed, otherwise it's hidden. + + +## Menu 2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger. Simply click left / right to go through the menu entries such as Bangle, Timer etc. @@ -20,17 +29,11 @@ to e.g. send a trigger via HomeAssistant once you selected it. Bangle -- Timer[Optional] -- Weather[Optional] -- HomeAssistant [Optional] ``` -The following list shows which apps must be installed in order to get the optional menu entries: -- Timer - The Scheduler library must be installed -- Weather - The Weather app must be installed -- HomeAssistant - The HomeAssistant app must be installed - - -## Other settings -- Fullscreen on/off -- Enable/disable lock icon in the settings -- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font -- The design of your bangle sys settings is used (e.g. you can also set a blue background) +## Settings +- Fullscreen on/off (widgets are still loaded). +- Enable/disable lock icon in the settings. Useful if fullscreen is on. +- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. +- There are no design settings, as your bangle sys settings are used. ## Thanks to diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 5b9244a54..f698f59ea 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -45,12 +45,12 @@ Graphics.prototype.setXLargeFont = function(scale) { Graphics.prototype.setLargeFont = function(scale) { - // Actual height 48 (49 - 2) + // Actual height 47 (48 - 2) this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('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'))), + atob('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAD/AAAAAAAAA/wAAAAAAAAP8AAAAAAAAD/AAAAAAAAA/wAAAAAAAAP8AAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAD/AAAAAAAAP/wAAAAAAAf/8AAAAAAB///AAAAAAH///wAAAAAf///8AAAAB/////AAAAH////8AAAAP////wAAAA/////AAAAB////+AAAAA////4AAAAAP///gAAAAAD//+AAAAAAA//4AAAAAAAP/gAAAAAAAD/AAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///+AAAAAB////8AAAAB/////wAAAA/////+AAAA//////wAAAf/////+AAAH//////wAAD//////+AAB/+AAAf/gAAf+AAAA/8AAH/AAAAH/AAD/gAAAA/4AA/wAAAAH+AAP8AAAAB/gAD+AAAAAf4AA/gAAAAH+AAP4AAAAA/gAD+AAAAAf4AA/wAAAAH+AAP8AAAAB/gAD/AAAAA/4AA/4AAAAP+AAH/AAAAH/AAB/4AAAH/wAAP/wAAP/4AAD//////+AAAf//////AAAD//////gAAAf/////wAAAD/////4AAAAf////4AAAAB////4AAAAAB///gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAH/AAAAAAAAD/gAAAAAAAA/4AAAAAAAAf8AAAAAAAAH+AAAAAAAAD/gAAAAAAAB/wAAAAAAAAf8AAAAAAAAP///////AAD///////wAA///////8AAP///////AAD///////wAA///////8AAP///////AAD///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAB/AAAA/gAAA/wAAA/4AAAf8AAAf+AAAP/AAAP/gAAH/wAAH/4AAD/8AAD/+AAB//AAA//gAA//wAAf/AAAP/8AAH/AAAH//AAD/gAAD//wAA/wAAB//8AAP8AAA///AAD/AAAf+fwAA/gAAP/n8AAP4AAH/x/AAD+AAD/4fwAA/gAB/8H8AAP8AAf+B/AAD/AAP/AfwAA/4AH/gH8AAH/AH/wB/AAB/8H/4AfwAAP///8AH8AAD////AB/AAAf///gAfwAAD///wAH8AAAf//4AB/AAAD//4AAfwAAAP/8AAH8AAAAf4AAB/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAADgAAAfwAAAB+AAAH8AAAAfwAAB/AAAAH+AAAfwAAAB/wAAH8AAAA/+AAB/AAAAP/gAAfwA4AA/8AAH8AfgAH/AAB/AP8AA/4AAfwD/gAH+AAH8B/4AB/gAB/A/8AAf4AAfwf/AAD+AAH8P/wAA/gAB/H/8AAf4AAfz//gAH+AAH8//4AB/gAB/f//AA/4AAf/+/4Af8AAH//P/AP/AAB//j////gAAf/wf///4AAH/4H///8AAB/8A///+AAAf+AH///AAAH/AA///gAAB/gAD//wAAAfwAAP/wAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAH/wAAAAAAAH/8AAAAAAAH//AAAAAAAH//wAAAAAAH//8AAAAAAH///AAAAAAH///wAAAAAH///8AAAAAP//9/AAAAAP//8fwAAAAP//4H8AAAAP//4B/AAAAP//4AfwAAAP//4AH8AAAD//4AB/AAAA//4AAfwAAAP/4AAH8AAAD/wAAB/AAAA/wAAAfwAAAPwAH////AADwAB////wAAwAAf///8AAAAAH////AAAAAB////wAAAAAf///8AAAAAH////AAAAAA////wAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAGAHwAAAB///gB+AAAH///8AfwAAB////AP+AAAf///wD/wAAH///+A/+AAB////gP/gAAf///4A/8AAH/8P8AH/AAB/AD+AA/4AAfwA/gAH+AAH8AfwAB/gAB/AH8AAf4AAfwB/AAH+AAH8AfwAB/gAB/AH8AAf4AAfwB/gAH+AAH8Af4AB/gAB/AH/AA/wAAfwB/4Af8AAH8AP/AP/AAB/AD////gAAfwAf///wAAH8AD///8AAB/AA///+AAAfwAH///AAAAAAA///gAAAAAAD//gAAAAAAAP/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///4AAAAAH////wAAAAH/////AAAAD/////4AAAB//////AAAA//////4AAAf//////AAAP//////4AAD/8D/w/+AAB/4B/wD/wAAf8A/wAf8AAP+AP4AD/gAD/AD+AAf4AA/wB/AAH+AAP4AfwAB/gAD+AH8AAf4AA/gB/AAH+AAP4AfwAB/gAD+AH+AAf4AA/wB/gAH+AAP8Af8AD/gAD/gH/gB/wAAf8A/8A/8AAH/AP///+AAB/gB////gAAPwAP///wAAB4AD///4AAAMAAf//8AAAAAAD//+AAAAAAAP/+AAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAAAAAAfwAAAAAAAAH8AAAAAAAAB/AAAAABwAAfwAAAAB8AAH8AAAAD/AAB/AAAAD/wAAfwAAAH/8AAH8AAAH//AAB/AAAP//wAAfwAAP//8AAH8AAf//+AAB/AAf//8AAAfwA///8AAAH8A///4AAAB/A///4AAAAfx///wAAAAH9///wAAAAB////gAAAAAf///gAAAAAH///AAAAAAB///AAAAAAAf/+AAAAAAAH/+AAAAAAAB/8AAAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAf/AAAAAP+Af/8AAAAP/4P//wAAAP//P//+AAAH//////wAAB//////8AAA///////gAAf//////8AAH////gP/AAD/wf/wA/wAA/4D/4AP+AAP8Af8AB/gAD/AH/AAf4AA/gA/wAH+AAP4AP4AA/gAD+AD/AAP4AA/gA/wAH+AAP8Af8AB/gAD/AH/AAf4AA/4D/4AP+AAP/B//AH/AAB////4D/wAAf//////8AAD//////+AAAf//////AAAH//////wAAA//8///4AAAD/+D//8AAAAP+Af/8AAAAAAAB/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/gAAAAAAAB//AAAAAAAB//8AAAAAAB///gAAgAAA///8AAcAAAf///gAPAAAH///8AH4AAD////AD/AAB/+H/4B/wAAf+Af+Af8AAP+AB/wD/gAD/gAf8Af4AA/wAD/AH+AAP8AA/wB/gAD+AAH8AP4AA/gAB/AD+AAP4AAfwB/gAD+AAH8Af4AA/wAD/AH+AAP8AA/gD/gAD/gAf4A/wAAf8AP8A/8AAH/gH/Af/AAA///////gAAP//////wAAB//////8AAAP/////+AAAB//////AAAAP/////AAAAA/////gAAAAD////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wA/wAAAAAP8AP8AAAAAD/AD/AAAAAA/wA/wAAAAAP8AP8AAAAAD/AD/AAAAAA/wA/wAAAAAP8AP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), 46, - atob("EhooGyUkJiUnISYnFQ=="), - 63+(scale<<8)+(1<<16) + atob("ExspGyUkJiQnISYnFQ=="), + 62+(scale<<8)+(1<<16) ); return this; }; @@ -197,7 +197,7 @@ try{ require('sched'); menu.push([ function(){ - var text = isAlarmEnabled() ? "T-" + getAlarmMinutes() + " min." : "Timer"; + var text = isAlarmEnabled() ? getAlarmMinutes() + " min." : "Timer"; return [text, imgTimer(), () => increaseAlarm(), () => decreaseAlarm(), null ] }, ]); @@ -384,8 +384,8 @@ function drawDate(){ g.reset().clearRect(0,0,W,W); // Draw date - y = parseInt(y/2); - y += settings.fullscreen ? 2 : 15; + y = parseInt(y/2)+4; + y += settings.fullscreen ? 0 : 13; var date = new Date(); var dateStr = date.getDate(); dateStr = ("0" + dateStr).substr(-2); diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index 4c0335df8..66dbb579d 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -2,7 +2,7 @@ "id": "bwclk", "name": "BW Clock", "version": "0.12", - "description": "BW Clock.", + "description": "A very minimalistic clock with date and time in focus.", "readme": "README.md", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}], diff --git a/apps/bwclk/screenshot.png b/apps/bwclk/screenshot.png index 55091342296e3d342175e44c5f1b199b3e89e748..3a75f13d1694d1f6bfeeedc1c2ed2d45f8c7b3b7 100644 GIT binary patch literal 2945 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 Xv5wplOP_VhnHo6ce*6G$KRMwa>ezR( literal 2725 zcmd6pSv1>=7RLYiOIwMl5o%Vmwy3#E(Go+=9Sv2Hqlk#sP)dYK(3vA>drFaBl&eKj z1P9d!RUHg5i-WYKE|E5J#C*6tulM0TpKq=GeQWLC+g|(OyLZOhRSK>Q2LM3I?X=Um zpGp6F#P|MmTGK50XF#r>b9Dr2`&1VI03Ph-iO@kTu?%g+-iPAU+0=6Zj zM3n&f&Z5r?nzCGjb-&aZ`U8<`=y*XDsY^`;x*0Wd z3_d5m2VOSY7exL4+Z5o8jcW>&p6ehh?Fn)!1daL_^Va(0aRm%iSo9J(`hLf z>#Sut{kc8+X%^t&ok1mb5R>1Qr#^eIsQp4ZE|X$tkGk*a7nVEXtX=$p z##PMgTuMU?MRQCOPf_;^K#|nmjOlJZog=Z}N^xp+;qu3G?Qa8F7Nr4KAn)R`htIq8 z@c%3PORBw_s+3(BQDG7)kGcv-r1#^N_h0#kQ=*3r7v*%J)uJGDh3P3h0b0D5e}h2M=1xg*?z37yGp*C$fGqO3%JfkH zuYLs4|3mL2T(_kUl*YA3(hJ<$cJ=429Je*Zn@>GhyZ+Tn5U9^MiwMdILaZ|&v zEJY;q&45KTnII-k=HasEFV)EOY~p~N%tS1SJyT&0$IRMUg4UpZT{xU6 z+jjQ+8m)ftqj7fQQe`UO$T~h6fk^>BNaSMEiEF+DOQED8`6VH!96{LEElQT^%9Hl0 zIiZ#aM34thm5-G1-{#3D_tIg81rMcuOqzaKAkQs#XeJf@JUYZA@7+`55_2=Se%&>3 zrgA-{f;y^^@3Z(;_~sjR%ORO!5y>5>c5c7-tAmzT%*Xb;Yai#K<6k(PmI#4;F^+)^ zg3_C!?O@#2n-o0A-!U4I#>^i2#nc*(mcuMa<^WkYCv9tn?4vHFJgUXPVl29H1?`aS zQL9a{eev#EJhPdu)7%|v>Fgv4&Ega6@aGAaC|a9?-w)&HK+0GK77R{x&=>v7s54)G zhU3kx*Yu^-r{`511QkJne6#*Rj#!pwvHYlp2G(IK9)+tgxFj}0sLr?0wz}Zkj?#C{1bEn0JE6r@9m@!`h{!n$BHILu9KoQ7sEL22*@@0M7vke zW^_f39D?f`JBIxEOaBW}+uu~Zc@%QrXbylJmB*emg*FF&doG9Mo{L>YqDWg|Xm`_p zB#1r2j80ou+IX;VXQ*t4)KF2GD;KfiiW^v3SY}N|!Izhm+k5y81c`w?Me zQ{?jZm!fXYd?Gc5bapvnD8jA==fh*Sm#((HL{?3d>c^#Z>3ih3PMMGxrR)ku0-|$z z^jRul?R{ZOVgjS^hVTz5`bERY7mRj!lLlk_UrjtSMr3^0Dk3(c7~c%6iaSe*mVO~i z@;{y<9%xHzzjkt_d2aWHum}hi7D4C-p5Su|d5F})WFcRJb}BhJC46~1TQcX2&rqFi zzd?Bkc12gTveh8v+5c}F^)%X;yPakWni?Q3*j-KPZGal@$zl&+>624WVIJixZ26e5c=#@y*B9j$viELL*@6LgbASk_HRJO zNex9<&@8$DpaIbmB;B%_AuHss;&-v|)1 zFUweH`) zH1+Sr4eo-QA_P}~^qih3=08~9nfqB;0dfp1+7s=oVbDSR{+4&;aq#4uTNH3jc{hG0 z6sm<&G9_UDBMfySb=>J%qIJo0AN9SM={KUgaSSbY|M*lg^GQ358f%5k(7}C3GSb;& z8zR(dF$6p~wt}72dUNK)x3&(|gzN6g11j$I-syW64wv4ZyWPBKC_xXU>&5W#N8_WR za?wD; z7Dy$R^*uvw+`8H6<(qcMiM_vUtF)!5h?!T#*85j@nP55iE`zEpQUD(ye0Zc}`!zF{QV{rT25;hmVb(BB|U7 scCdleQbMjsL7QXO<;#)(U?4F5z|VXl&k4Nvvv&e+&fZS7j+pfS0`p=8UjP6A diff --git a/apps/bwclk/screenshot_2.png b/apps/bwclk/screenshot_2.png index ccbc9aae1626f4b024081c9c8f30bbaf45d89213..31bf6373ef212f3f10d427f8052c672c41d67453 100644 GIT binary patch literal 3215 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 CkEnzI diff --git a/apps/bwclk/screenshot_3.png b/apps/bwclk/screenshot_3.png index 5bf7083f0c1b9c9a10e7f86848dc1eb2adf3a682..f9a9a7d3fdaf5fc43aa87825bddf80455f98fbbc 100644 GIT binary patch literal 3312 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 literal 3133 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 Q-v9sr07*qoM6N<$f;XZ&IsgCw From 1cc51c470a1d91f4acfc702a5b1605af158b39a0 Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 22:10:20 +0200 Subject: [PATCH 19/23] Minor changes --- apps/bwclk/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index 5d0336b3b..8863077ad 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -4,13 +4,13 @@ A very minimalistic clock with date and time in focus. ![](screenshot.png) ## Features -Altough date and time is the most important thing, the BW clock provides many features -as well as 3rd party integtations. The following things are integrated: -- Bangle: Steps, Heart rate, Battery (including charging state) -- Timer: +/- 5 min. Note: The Scheduler library must be installed, otherwise it's hidden -- Weather: Temperature, Wind. Note: The Weather app must be installed, otherwise it's hidden. -- HomeAssistant - All triggers are shown. Note: The HomeAssistant app must be installed, otherwise it's hidden. +The BW clock provides many features as well as 3rd party integrations: +- Bangle data such as steps, heart rate, battery or charging state. +- A timer can be set directly. *Requirement: Scheduler library* +- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app* +- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app* +Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden. ## Menu 2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger. From 157135242c9cb49a11cf3ba76993bbce5e3dc5b4 Mon Sep 17 00:00:00 2001 From: David Peer Date: Wed, 29 Jun 2022 22:13:03 +0200 Subject: [PATCH 20/23] Minor change --- apps/bwclk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bwclk/README.md b/apps/bwclk/README.md index 8863077ad..8cf3120dd 100644 --- a/apps/bwclk/README.md +++ b/apps/bwclk/README.md @@ -41,4 +41,4 @@ to e.g. send a trigger via HomeAssistant once you selected it. ## Creator -- [David Peer](https://github.com/peerdavid) +[David Peer](https://github.com/peerdavid) From 44905c795ed2ec32ca1320f089f583ca6ae20af6 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 1 Jul 2022 08:10:29 +0100 Subject: [PATCH 21/23] include sched in installed apps - bin/firmwaremaker_c.js --- bin/firmwaremaker_c.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index fd8072e06..4aa8e087f 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -23,13 +23,13 @@ if (DEVICE=="BANGLEJS") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs1_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", - "about","alarm","widbat","widbt","welcome" + "about","alarm","sched","widbat","widbt","welcome" ]; } else if (DEVICE=="BANGLEJS2") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs2_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","antonclk","setting", - "about","alarm","health","widlock","widbat","widbt","widid","welcome" + "about","alarm","sched","health","widlock","widbat","widbt","widid","welcome" ]; } else { console.log("USAGE:"); From 20d5bae555d53edf7cf75cccfeadbd3ad8016950 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 1 Jul 2022 11:01:27 +0100 Subject: [PATCH 22/23] bthrm 0.11: App now shows status info while connecting + Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central) --- apps/bthrm/ChangeLog | 2 ++ apps/bthrm/boot.js | 63 ++++++++++++++++++++++------------------ apps/bthrm/bthrm.js | 10 ++++++- apps/bthrm/metadata.json | 2 +- 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 00ed856d6..6121a845a 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -23,3 +23,5 @@ 0.08: Allow scanning for devices in settings 0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655) 0.10: Use default Bangle formatter for booleans +0.11: App now shows status info while connecting + Fixes to allow cached BluetoothRemoteGATTCharacteristic to work with 2v14.14 onwards (>1 central) diff --git a/apps/bthrm/boot.js b/apps/bthrm/boot.js index e9e640563..eb0f1b72d 100644 --- a/apps/bthrm/boot.js +++ b/apps/bthrm/boot.js @@ -5,11 +5,11 @@ ); var log = function(text, param){ + if (global.showStatusInfo) + showStatusInfo(text) if (settings.debuglog){ var logline = new Date().toISOString() + " - " + text; - if (param){ - logline += " " + JSON.stringify(param); - } + if (param) logline += ": " + JSON.stringify(param); print(logline); } }; @@ -30,7 +30,7 @@ }; var addNotificationHandler = function(characteristic) { - log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler); + log("Setting notification handler"/*supportedCharacteristics[characteristic.uuid].handler*/); characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value)); }; @@ -61,7 +61,8 @@ writeCache(cache); }; - var characteristicsFromCache = function() { + var characteristicsFromCache = function(device) { + var service = { device : device }; // fake a BluetoothRemoteGATTService log("Read cached characteristics"); var cache = getCache(); if (!cache.characteristics) return []; @@ -75,6 +76,7 @@ r.properties = {}; r.properties.notify = cached.notify; r.properties.read = cached.read; + r.service = service; addNotificationHandler(r); log("Restored characteristic: ", r); restored.push(r); @@ -141,7 +143,7 @@ src: "bthrm" }; - log("Emitting HRM: ", repEvent); + log("Emitting HRM", repEvent); Bangle.emit("HRM", repEvent); } @@ -155,7 +157,7 @@ if (battery) newEvent.battery = battery; if (sensorContact) newEvent.contact = sensorContact; - log("Emitting BTHRM: ", newEvent); + log("Emitting BTHRM", newEvent); Bangle.emit("BTHRM", newEvent); } }, @@ -236,8 +238,8 @@ if (!currentRetryTimeout){ var clampedTime = retryTime < 100 ? 100 : retryTime; - - log("Set timeout for retry as " + clampedTime); + + log("Set timeout for retry as " + clampedTime); clearRetryTimeout(); currentRetryTimeout = setTimeout(() => { log("Retrying"); @@ -257,8 +259,8 @@ var buzzing = false; var onDisconnect = function(reason) { log("Disconnect: " + reason); - log("GATT: ", gatt); - log("Characteristics: ", characteristics); + log("GATT", gatt); + log("Characteristics", characteristics); retryTime = initialRetryTime; clearRetryTimeout(); switchInternalHrm(); @@ -273,13 +275,13 @@ }; var createCharacteristicPromise = function(newCharacteristic) { - log("Create characteristic promise: ", newCharacteristic); + log("Create characteristic promise", newCharacteristic); var result = Promise.resolve(); // For values that can be read, go ahead and read them, even if we might be notified in the future // Allows for getting initial state of infrequently updating characteristics, like battery if (newCharacteristic.readValue){ result = result.then(()=>{ - log("Reading data for " + JSON.stringify(newCharacteristic)); + log("Reading data", newCharacteristic); return newCharacteristic.readValue().then((data)=>{ if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) { supportedCharacteristics[newCharacteristic.uuid].handler(data); @@ -289,8 +291,8 @@ } if (newCharacteristic.properties.notify){ result = result.then(()=>{ - log("Starting notifications for: ", newCharacteristic); - var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started for ", newCharacteristic)); + log("Starting notifications", newCharacteristic); + var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic)); if (settings.gracePeriodNotification > 0){ log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications"); startPromise = startPromise.then(()=>{ @@ -301,7 +303,7 @@ return startPromise; }); } - return result.then(()=>log("Handled characteristic: ", newCharacteristic)); + return result.then(()=>log("Handled characteristic", newCharacteristic)); }; var attachCharacteristicPromise = function(promise, characteristic) { @@ -312,11 +314,11 @@ }; var createCharacteristicsPromise = function(newCharacteristics) { - log("Create characteristics promise: ", newCharacteristics); + log("Create characteristics promis ", newCharacteristics); var result = Promise.resolve(); for (var c of newCharacteristics){ if (!supportedCharacteristics[c.uuid]) continue; - log("Supporting characteristic: ", c); + log("Supporting characteristic", c); characteristics.push(c); if (c.properties.notify){ addNotificationHandler(c); @@ -328,10 +330,10 @@ }; var createServicePromise = function(service) { - log("Create service promise: ", service); + log("Create service promise", service); var result = Promise.resolve(); result = result.then(()=>{ - log("Handling service: " + service.uuid); + log("Handling service" + service.uuid); return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c)); }); return result.then(()=>log("Handled service" + service.uuid)); @@ -368,7 +370,7 @@ } promise = promise.then((d)=>{ - log("Got device: ", d); + log("Got device", d); d.on('gattserverdisconnected', onDisconnect); device = d; }); @@ -379,14 +381,14 @@ }); } else { promise = Promise.resolve(); - log("Reuse device: ", device); + log("Reuse device", device); } promise = promise.then(()=>{ if (gatt){ - log("Reuse GATT: ", gatt); + log("Reuse GATT", gatt); } else { - log("GATT is new: ", gatt); + log("GATT is new", gatt); characteristics = []; var cachedId = getCache().id; if (device.id !== cachedId){ @@ -404,7 +406,10 @@ promise = promise.then((gatt)=>{ if (!gatt.connected){ - var connectPromise = gatt.connect(connectSettings); + log("Connecting..."); + var connectPromise = gatt.connect(connectSettings).then(function() { + log("Connected."); + }); if (settings.gracePeriodConnect > 0){ log("Add " + settings.gracePeriodConnect + "ms grace period after connecting"); connectPromise = connectPromise.then(()=>{ @@ -432,7 +437,7 @@ promise = promise.then(()=>{ if (!characteristics || characteristics.length === 0){ - characteristics = characteristicsFromCache(); + characteristics = characteristicsFromCache(device); } }); @@ -445,11 +450,11 @@ }); characteristicsPromise = characteristicsPromise.then((services)=>{ - log("Got services:", services); + log("Got services", services); var result = Promise.resolve(); for (var service of services){ if (!(supportedServices.includes(service.uuid))) continue; - log("Supporting service: ", service.uuid); + log("Supporting service", service.uuid); result = attachServicePromise(result, service); } if (settings.gracePeriodService > 0) { @@ -496,7 +501,7 @@ log("Power off for " + app); if (gatt) { if (gatt.connected){ - log("Disconnect with gatt: ", gatt); + log("Disconnect with gatt", gatt); try{ gatt.disconnect().then(()=>{ log("Successful disconnect"); diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index dd9230386..3b84980b1 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -42,12 +42,20 @@ function draw(y, type, event) { if (event.energy) str += " kJoule: " + event.energy.toFixed(0); g.setFontVector(12).drawString(str,px,y+60); } - } var firstEventBt = true; var firstEventInt = true; + +// This can get called for the boot code to show what's happening +function showStatusInfo(txt) { + var R = Bangle.appRect; + g.reset().clearRect(R.x,R.y2-24,R.x2,R.y2).setFont("6x8"); + txt = g.wrapString(txt, R.w)[0]; + g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2); +} + function onBtHrm(e) { if (firstEventBt){ clear(24); diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 9e40896f0..c9add7a54 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,7 +2,7 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.10", + "version": "0.11", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "type": "app", From 72f5769486c7e84cdb31c4f81f03d0f05832e92f Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Fri, 1 Jul 2022 11:07:22 +0100 Subject: [PATCH 23/23] fix clear amount --- apps/bthrm/bthrm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index 3b84980b1..fadf2a5d8 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -51,7 +51,7 @@ var firstEventInt = true; // This can get called for the boot code to show what's happening function showStatusInfo(txt) { var R = Bangle.appRect; - g.reset().clearRect(R.x,R.y2-24,R.x2,R.y2).setFont("6x8"); + g.reset().clearRect(R.x,R.y2-8,R.x2,R.y2).setFont("6x8"); txt = g.wrapString(txt, R.w)[0]; g.setFontAlign(0,1).drawString(txt, (R.x+R.x2)/2, R.y2); }