Merge branch 'master' into feat/openhaystack-battery

Conflicts:
	apps/openhaystack/ChangeLog
master
Rob Pilling 2025-03-26 07:34:07 +00:00
commit 571062576a
787 changed files with 61140 additions and 4711 deletions

View File

@ -112,6 +112,7 @@ module.exports = {
"getSerial": "readonly", "getSerial": "readonly",
"getTime": "readonly", "getTime": "readonly",
"global": "readonly", "global": "readonly",
"globalThis": "readonly",
"HIGH": "readonly", "HIGH": "readonly",
"I2C1": "readonly", "I2C1": "readonly",
"Infinity": "readonly", "Infinity": "readonly",

View File

@ -5,3 +5,5 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
reviewers:
- "gfwilliams"

View File

@ -270,7 +270,8 @@ and which gives information about the app for the Launcher.
// 'notify' - provides 'notify' library for showing notifications // 'notify' - provides 'notify' library for showing notifications
// 'locale' - provides 'locale' library for language-specific date/distance/etc // 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware) // (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching // 'defaultconfig' - a set of apps that will can be installed and will wipe out all previously installed apps
"tags": "", // comma separated tag list for searching (don't include uppercase or spaces)
// common types are: // common types are:
// 'clock' - it's a clock // 'clock' - it's a clock
// 'widget' - it is (or provides) a widget // 'widget' - it is (or provides) a widget
@ -289,6 +290,7 @@ and which gives information about the app for the Launcher.
"dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets "dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets
"provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require' "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
"provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message' "provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message'
"provides_features" : ["welcome"] // optional, this app provides some feature, used to ensure two aren't installed at once. Currently just 'welcome'
"default" : true, // set if an app is the default implementer of something (a widget/module/etc) "default" : true, // set if an app is the default implementer of something (a widget/module/etc)
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc) // that contains more information about this app (usage, etc)

View File

@ -187,14 +187,18 @@
</details> </details>
</div> </div>
<div id="more-deviceinfo" style="display:none"> <div id="more-deviceinfo" style="display:none">
<h3>Device info</h3> <h3>Device info</h3>
<div id="more-deviceinfo-content"></div> <div id="more-deviceinfo-content"></div>
<div class="editor--terminal">
<div class="editor__canvas" style="position:relative;height:20rem;display:none;"></div>
<button class="btn" id="terminalEnable">Enable Terminal</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<footer class="floating hidden"> <footer class="floating hidden">
<!-- Install button, hidden by default --> <!-- PWA Install button, hidden by default -->
<div id="installContainer" class="hidden"> <div id="installContainer" class="hidden">
<button id="butInstall" type="button"> <button id="butInstall" type="button">
Install Install
@ -203,7 +207,7 @@
</footer> </footer>
<script src="webtools/puck.js"></script> <script src="webtools/puck.js"></script>
<script src="webtools/heatshrink.js"></script> <script src="webtools/heatshrink.js"></script>
<script src="core/lib/marked.min.js"></script> <script src="core/lib/marked.min.js"></script>
<script src="core/lib/espruinotools.js"></script> <script src="core/lib/espruinotools.js"></script>
<script src="core/js/utils.js"></script> <script src="core/js/utils.js"></script>
@ -416,7 +420,7 @@ if (el) el.addEventListener("click", event=>{
if (webrtc) showWebRTCID(webrtc.peerId); if (webrtc) showWebRTCID(webrtc.peerId);
else { else {
webrtc = webrtcInit({ webrtc = webrtcInit({
bridge:true, bridge:true,
onStatus : function(s) { onStatus : function(s) {
showToast(s); showToast(s);
}, },
@ -432,7 +436,7 @@ if (el) el.addEventListener("click", event=>{
onPortDisconnect : function(serialPort) { onPortDisconnect : function(serialPort) {
}, },
onPortWrite : function(data, cb) { onPortWrite : function(data, cb) {
Puck.write(data, cb); Puck.write(data, cb);
} }
}); });
connection.on("data", function(d) { connection.on("data", function(d) {

View File

@ -5,3 +5,4 @@
0.05: Display time, even on Thursday 0.05: Display time, even on Thursday
0.06: Fix light theme issue, where widgets would end up on a light strip 0.06: Fix light theme issue, where widgets would end up on a light strip
0.07: Minor code improvements 0.07: Minor code improvements
0.08: Support Fast Loading

View File

@ -1,146 +1,162 @@
// get 12 hour status, code from barclock {
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; // get 12 hour status, code from barclock
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
// define background // define background
var imgBg = require("heatshrink").decompress(atob("2GwgJC/AH4A/AH4A/AH4A/AH4A/ACcGAhAV/Cp3gvdug+Gj0AgeABYMBAQMIggVEg/w/9/h/Gn8As3ACpk559zznmseAs0B13nq/Rie+uodCIIUZw9hzFmv+AgcCmco7MRilow1ACpN8gFhwMilFRCoMowgVEIIVhIINhwFg4GiCpfw/dhx/mn4uBCoXRhWktAVFTIVhw9mj8YseDkUnqPEoeuugVEAAlgSgICBACAVC8AUQCQQVSAEsD/4ASeYgA/ACkHNiK5Cj4VR/AVBng+RCQVwCqMOAQPhIKOHgEB44VR8YVBx4VR+eAgOfCqPxwEDCqX5CoKvS/PAgc/YqQVU/gV/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/CsMfCqP4CoOfCqP54EBx4VR+OAgPPCqPzwEA44VR4cAgHhCqMHCoNwAQIAPjwCBngVRvgCBV6XwCoMHCqPAHyIA/AEigEf4IAOkAEDoAPJWAtA+PHv+Al6uPCofAGAgALoHz51/8AVT+IVS+4VPpMR73woH27n/8Eh8+ZmadIqsoyGICofAkMUktJFZAVBzgVBv34YgMhi8RkIVJnGQIIN8/H34FB8kJiIVIkVEyGQkF8/Pj4GBkhBKCoOexEQvHx8fBgMXzMxTJkICoXCVx8AggDGABsD/4AB/AVQAH4APA")); const imgBg = require("heatshrink").decompress(atob("2GwgJC/AH4A/AH4A/AH4A/AH4A/ACcGAhAV/Cp3gvdug+Gj0AgeABYMBAQMIggVEg/w/9/h/Gn8As3ACpk559zznmseAs0B13nq/Rie+uodCIIUZw9hzFmv+AgcCmco7MRilow1ACpN8gFhwMilFRCoMowgVEIIVhIINhwFg4GiCpfw/dhx/mn4uBCoXRhWktAVFTIVhw9mj8YseDkUnqPEoeuugVEAAlgSgICBACAVC8AUQCQQVSAEsD/4ASeYgA/ACkHNiK5Cj4VR/AVBng+RCQVwCqMOAQPhIKOHgEB44VR8YVBx4VR+eAgOfCqPxwEDCqX5CoKvS/PAgc/YqQVU/gV/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/CsMfCqP4CoOfCqP54EBx4VR+OAgPPCqPzwEA44VR4cAgHhCqMHCoNwAQIAPjwCBngVRvgCBV6XwCoMHCqPAHyIA/AEigEf4IAOkAEDoAPJWAtA+PHv+Al6uPCofAGAgALoHz51/8AVT+IVS+4VPpMR73woH27n/8Eh8+ZmadIqsoyGICofAkMUktJFZAVBzgVBv34YgMhi8RkIVJnGQIIN8/H34FB8kJiIVIkVEyGQkF8/Pj4GBkhBKCoOexEQvHx8fBgMXzMxTJkICoXCVx8AggDGABsD/4AB/AVQAH4APA"));
// define fonts // define fonts
// reg number first char 48 28 by 41 // reg number first char 48 28 by 41
var fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAAD/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/wAAAAH/H/gH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wB/4AP/4H/4A//4f/4D//5//4P//h//4//+B//4AAAAAAAAAAAAAAAAAf/+AAAB//4gAAD//jgAAD/+PgABj/4/gAHj/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f88AAfx/8wAAfH/8AAAcf/8AAAR//4AAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA4AAAAAD4AAYAAP4AD8AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAHgAH/H/GH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAP//AAAAP//AAAAP//AAAAP/8AAAAP/2AAAAP/eAAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAB/7x/4AH/7H/4Af/4f/4B//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wAAAD//wAAAj//gAADj/+AAAPj/5gAA/j/ngAD/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8AA8f8fwAAx/8fAAAH/8cAAAf/8QAAA//8AAAA//8AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//0//j4//Y/+P4/94/4/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/AAPH/H8AAMf/HwAAB//HAAAH//EAAAH//AAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAGAAAAAAOAAAAAAeAAAAAA+AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB8AAAAADx/4B/4HH/4H/4Mf/4f/4R//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wP/+D//w//4j//z//jj//T/+Pj/9j/4/j/3j/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f+8f8fx/+x/8fH/+H/8cf/+f/8R//4f/8H//gf/8AAAAAAAAAAAAAA//8AAAA//8AAAI//8AAA4//0AAD4//YAAP4/94AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/H/vH/H8f/sf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); const fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAAD/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/wAAAAH/H/gH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wB/4AP/4H/4A//4f/4D//5//4P//h//4//+B//4AAAAAAAAAAAAAAAAAf/+AAAB//4gAAD//jgAAD/+PgABj/4/gAHj/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f88AAfx/8wAAfH/8AAAcf/8AAAR//4AAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA4AAAAAD4AAYAAP4AD8AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAHgAH/H/GH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAP//AAAAP//AAAAP//AAAAP/8AAAAP/2AAAAP/eAAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAB/7x/4AH/7H/4Af/4f/4B//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wAAAD//wAAAj//gAADj/+AAAPj/5gAA/j/ngAD/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8AA8f8fwAAx/8fAAAH/8cAAAf/8QAAA//8AAAA//8AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//0//j4//Y/+P4/94/4/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/AAPH/H8AAMf/HwAAB//HAAAH//EAAAH//AAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAGAAAAAAOAAAAAAeAAAAAA+AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB8AAAAADx/4B/4HH/4H/4Mf/4f/4R//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wP/+D//w//4j//z//jj//T/+Pj/9j/4/j/3j/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f+8f8fx/+x/8fH/+H/8cf/+f/8R//4f/8H//gf/8AAAAAAAAAAAAAA//8AAAA//8AAAI//8AAA4//0AAD4//YAAP4/94AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/H/vH/H8f/sf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// tiny font for percentage first char 48 6 by 8 // tiny font for percentage first char 48 6 by 8
//var fontTiny = atob("AH6BgYF+ACFB/wEBAGGDhYlxAEKBkZFuAAx0hP8EAPqRkZGOAH6RkZFOAICHmKDAAG6RkZFuAHKJiYl+AAAAAAAAAAAAAAAA"); //var fontTiny = atob("AH6BgYF+ACFB/wEBAGGDhYlxAEKBkZFuAAx0hP8EAPqRkZGOAH6RkZFOAICHmKDAAG6RkZFuAHKJiYl+AAAAAAAAAAAAAAAA");
// date font first char 48 12 by 15 // date font first char 48 12 by 15
var fontDate = atob("AAAAAfv149wAeADwAeADwAeADvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAPHn9/AAAAAAP0A9wweGDwweGDwweGDvAL8AAAAAAAAAAAgwOGDwweGDwweGDvHp98AAAAA/gB6AAwAGAAwAGAAwAGAPHj9/AAAAAfgF6BwweGDwweGDwweGDgHoB+AAAAAfv169wweGDwweGDwweGDgHoB+AAAAAAAAAAgAGAAwAGAAwAGAAvHh9/AAAAAfv169wweGDwweGDwweGDvHr9+AAAAAfgF6BwweGDwweGDwweGDvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); const fontDate = atob("AAAAAfv149wAeADwAeADwAeADvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAPHn9/AAAAAAP0A9wweGDwweGDwweGDvAL8AAAAAAAAAAAgwOGDwweGDwweGDvHp98AAAAA/gB6AAwAGAAwAGAAwAGAPHj9/AAAAAfgF6BwweGDwweGDwweGDgHoB+AAAAAfv169wweGDwweGDwweGDgHoB+AAAAAAAAAAgAGAAwAGAAwAGAAvHh9/AAAAAfv169wweGDwweGDwweGDvHr9+AAAAAfgF6BwweGDwweGDwweGDvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
// define days of the week images // define days of the week images
var imgMon = E.toArrayBuffer(atob("Ig8BgHwfD5AvB8HD8z8wMPzPzMQzM/M/DMz8z8c7f7f7z////3Oz+3+PzPzPw/M/M/D8z8z8PzPzPw/vB8/n/8H3/A==")); const imgMon = E.toArrayBuffer(atob("Ig8BgHwfD5AvB8HD8z8wMPzPzMQzM/M/DMz8z8c7f7f7z////3Oz+3+PzPzPw/M/M/D8z8z8PzPzPw/vB8/n/8H3/A=="));
var imgTue = E.toArrayBuffer(atob("Ig8BwDv9wDAOfmgf/5+Z///n5n/5+fmf/n5+Z//fv9oH////Af37/b/+fn5n/5+fmf/n5+Z/+fn5n/5/g+gfn+D8AA==")); const imgTue = E.toArrayBuffer(atob("Ig8BwDv9wDAOfmgf/5+Z///n5n/5+fmf/n5+Z//fv9oH////Af37/b/+fn5n/5+fmf/n5+Z/+fn5n/5/g+gfn+D8AA=="));
var imgWed = E.toArrayBuffer(atob("Ig8Bf7gHgM/NA9Az8z/z8PzP/Pw/M/8/D8z/z8c7QPf7z+A//3O3/3+MzP/PwzM/8/D8z/z8PzP/PxAtA9A4B4B4DA==")); const imgWed = E.toArrayBuffer(atob("Ig8Bf7gHgM/NA9Az8z/z8PzP/Pw/M/8/D8z/z8c7QPf7z+A//3O3/3+MzP/PwzM/8/D8z/z8PzP/PxAtA9A4B4B4DA=="));
var imgThu = E.toArrayBuffer(atob("Ig8BgHf7f6Ac/M/P/z8z8//PzPzz8/M/PPz8z8+/QLf7/+A///v3+3+8/PzPzz8/M/PPz8z88/PzPzz8/vB/P3/8HA==")); const imgThu = E.toArrayBuffer(atob("Ig8BgHf7f6Ac/M/P/z8z8//PzPzz8/M/PPz8z8+/QLf7/+A///v3+3+8/PzPzz8/M/PPz8z88/PzPzz8/vB/P3/8HA=="));
var imgFri = E.toArrayBuffer(atob("Ig8B/wDwP7+geg/P5/5+c/n/n5z+f+fnP5/5+c/oHoF7/AfAf/7/7/+/n/k/z+f+R/P5/5j8/n/nHz+/++PP7//8+A==")); const imgFri = E.toArrayBuffer(atob("Ig8B/wDwP7+geg/P5/5+c/n/n5z+f+fnP5/5+c/oHoF7/AfAf/7/7/+/n/k/z+f+R/P5/5j8/n/nHz+/++PP7//8+A=="));
var imgSat = E.toArrayBuffer(atob("Ig8B4DwDwDgOgXAJ/5+f/n/n5/+f+fn55/5+fnoHoF/fAfAf//+b/f3/5n5+f/mfn5/+Z+fn//n5+eAef358B7//nA==")); const imgSat = E.toArrayBuffer(atob("Ig8B4DwDwDgOgXAJ/5+f/n/n5/+f+fn55/5+fnoHoF/fAfAf//+b/f3/5n5+f/mfn5/+Z+fn//n5+eAef358B7//nA=="));
var imgSun = E.toArrayBuffer(atob("Ig8BwHf7D7Ac/MHD/z8wMP/PzMQ/8/M/D/z8z8QPf7f6A/////83+3+/zPzPz/M/M/P8z8z8//PzPwA/B8/oD8H3/A==")); const imgSun = E.toArrayBuffer(atob("Ig8BwHf7D7Ac/MHD/z8wMP/PzMQ/8/M/D/z8z8QPf7f6A/////83+3+/zPzPz/M/M/P8z8z8//PzPwA/B8/oD8H3/A=="));
// define icons // define icons
var imgSep = E.toArrayBuffer(atob("BhsBAAAAAA///////////////AAAAAAA")); const imgSep = E.toArrayBuffer(atob("BhsBAAAAAA///////////////AAAAAAA"));
//var imgPercent = E.toArrayBuffer(atob("BwcBuq7ffbqugA==")); //var imgPercent = E.toArrayBuffer(atob("BwcBuq7ffbqugA=="));
var img24hr = E.toArrayBuffer(atob("EwgBj7vO53na73tcDtu9uDev7vA93g==")); const img24hr = E.toArrayBuffer(atob("EwgBj7vO53na73tcDtu9uDev7vA93g=="));
var imgPM = E.toArrayBuffer(atob("EwgB+HOfdnPu1X3ar4dV9+q+/bfftg==")); const imgPM = E.toArrayBuffer(atob("EwgB+HOfdnPu1X3ar4dV9+q+/bfftg=="));
//vars //vars
var separator = true; let separator = true;
var is24hr = !is12Hour; let is24hr = !is12Hour;
var leadingZero = true; let leadingZero = true;
//the following 2 sections are used from waveclk to schedule minutely updates //the following 2 sections are used from waveclk to schedule minutely updates
// timeout used to update every minute // timeout used to update every minute
var drawTimeout; let drawTimeout;
// schedule a draw for the next minute // schedule a draw for the next minute
function queueDraw() { let queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
function drawBackground() {
g.setBgColor(0,0,0);
g.setColor(1,1,1);
g.clear();
g.drawImage(imgBg,0,0);
g.reset();
}
function draw(){
drawBackground();
var date = new Date();
var h = date.getHours(), m = date.getMinutes();
var d = date.getDate(), w = date.getDay();
g.reset();
g.setBgColor(0,0,0);
g.setColor(1,1,1);
//draw 24 hr indicator and 12 hr specific behavior
if (is24hr){
g.drawImage(img24hr,32, 65);
if (leadingZero){
h = ("0"+h).substr(-2);
}
} else if (h > 12) {
g.drawImage(imgPM,40, 70);
h = h - 12;
if (leadingZero){
h = ("0"+h).substr(-2);
} else {
h = " " + h;
}
} else if (h === 0) {
// display 12:00 instead of 00:00 for 12 hr mode
h = "12";
}
//draw separator
if (separator){
g.drawImage(imgSep, 85,98);}
//draw day of week
var imgW = null;
if (w == 0) {imgW = imgSun;}
if (w == 1) {imgW = imgMon;}
if (w == 2) {imgW = imgTue;}
if (w == 3) {imgW = imgWed;}
if (w == 4) {imgW = imgThu;}
if (w == 5) {imgW = imgFri;}
if (w == 6) {imgW = imgSat;}
g.drawImage(imgW, 85, 63);
// draw nums
// draw time
g.setColor(0,0,0);
g.setBgColor(1,1,1);
g.setFontCustom(fontNum, 48, 28, 41);
if (h<10) {
if (leadingZero) {
h = ("0"+h).substr(-2);
} else {
h = " " + h;
}
}
g.drawString(h, 25, 90, true);
g.drawString(("0"+m).substr(-2), 92, 90, true);
// draw date
g.setFontCustom(fontDate, 48, 12, 15);
g.drawString(("0"+d).substr(-2), 123,63, true);
// widget redraw
Bangle.drawWidgets();
queueDraw();
}
/**
* This watch is mostly dark, it does not make sense to respect the
* light theme as you end up with a white strip at the top for the
* widgets and black watch. So set the colours to the dark theme.
*
*/
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
draw();
//the following section is also from waveclk
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = setTimeout(function() {
} drawTimeout = undefined;
}); draw();
}, 60000 - (Date.now() % 60000));
};
Bangle.setUI("clock"); let drawBackground = function() {
Bangle.loadWidgets(); g.setBgColor(0, 0, 0);
Bangle.drawWidgets(); g.setColor(1, 1, 1);
g.clear();
g.drawImage(imgBg, 0, 0);
g.reset();
};
let draw = function() {
drawBackground();
let date = new Date();
let h = date.getHours(),
m = date.getMinutes();
let d = date.getDate(),
w = date.getDay();
g.reset();
g.setBgColor(0, 0, 0);
g.setColor(1, 1, 1);
//draw 24 hr indicator and 12 hr specific behavior
if (is24hr){
g.drawImage(img24hr,32, 65);
if (leadingZero){
h = ("0"+h).substr(-2);
}
} else if (h > 12) {
g.drawImage(imgPM,40, 70);
h = h - 12;
if (leadingZero){
h = ("0"+h).substr(-2);
} else {
h = " " + h;
}
} else if (h === 0) {
// display 12:00 instead of 00:00 for 12 hr mode
h = "12";
}
//draw separator
if (separator) {
g.drawImage(imgSep, 85, 98);
}
//draw day of week
let imgW = null;
if (w == 0) {imgW = imgSun;}
if (w == 1) {imgW = imgMon;}
if (w == 2) {imgW = imgTue;}
if (w == 3) {imgW = imgWed;}
if (w == 4) {imgW = imgThu;}
if (w == 5) {imgW = imgFri;}
if (w == 6) {imgW = imgSat;}
g.drawImage(imgW, 85, 63);
// draw nums
// draw time
g.setColor(0, 0, 0);
g.setBgColor(1, 1, 1);
g.setFontCustom(fontNum, 48, 28, 41);
if (h < 10) {
if (leadingZero) {
h = ("0" + h).substr(-2);
} else {
h = " " + h;
}
}
g.drawString(h, 25, 90, true);
g.drawString(("0" + m).substr(-2), 92, 90, true);
// draw date
g.setFontCustom(fontDate, 48, 12, 15);
g.drawString(("0" + d).substr(-2), 123, 63, true);
// widget redraw
Bangle.drawWidgets();
queueDraw();
};
/**
* This watch is mostly dark, it does not make sense to respect the
* light theme as you end up with a white strip at the top for the
* widgets and black watch. So set the colours to the dark theme.
*
*/
g.setTheme({
bg: "#000",
fg: "#fff",
dark: true
}).clear();
draw();
//the following section is also from waveclk
let onLCDPower = on => {
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
};
Bangle.on('lcdPower', onLCDPower);
Bangle.setUI({
mode: "clock",
remove: function() {
if (drawTimeout) clearTimeout(drawTimeout);
Bangle.removeListener('lcdPower', onLCDPower);
}
});
Bangle.loadWidgets();
Bangle.drawWidgets();
}

View File

@ -3,7 +3,7 @@
"shortName":"93 Dub", "shortName":"93 Dub",
"icon": "93dub.png", "icon": "93dub.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"version": "0.07", "version": "0.08",
"description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo", "description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo",
"tags": "clock", "tags": "clock",
"type": "clock", "type": "clock",

View File

@ -5,7 +5,7 @@
"version": "0.02", "version": "0.02",
"description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.", "description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.",
"icon": "app.png", "icon": "app.png",
"tags": "Color,input,buttons,touch,UI", "tags": "color,input,buttons,touch,ui",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS"],
"readme": "README.md", "readme": "README.md",
"screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}], "screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}],

View File

@ -4,8 +4,8 @@
"version": "0.04", "version": "0.04",
"description": "Wrist mounted ukulele chords", "description": "Wrist mounted ukulele chords",
"icon": "app.png", "icon": "app.png",
"tags": "uke, chords", "tags": "uke,chords",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"Uke.app.js","url":"app.js"}, {"name":"Uke.app.js","url":"app.js"},

View File

@ -24,10 +24,10 @@
var dateStr = require("locale").date(date); var dateStr = require("locale").date(date);
// draw time // draw time
g.setFontAlign(0,0).setFont("Vector",48); g.setFontAlign(0,0).setFont("Vector",48);
g.clearRect(0,y-15,g.getWidth(),y+25); // clear the background g.clearRect(0,y-20,g.getWidth(),y+25); // clear the background
g.drawString(timeStr,x,y); g.drawString(timeStr,x,y);
// draw date // draw date
y += 35; y += 30;
g.setFontAlign(0,0).setFont("6x8"); g.setFontAlign(0,0).setFont("6x8");
g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background g.clearRect(0,y-4,g.getWidth(),y+4); // clear the background
g.drawString(dateStr,x,y); g.drawString(dateStr,x,y);
@ -41,6 +41,8 @@
// Show launcher when middle button pressed // Show launcher when middle button pressed
Bangle.setUI({mode:"clock", remove:function() { Bangle.setUI({mode:"clock", remove:function() {
// free any memory we allocated to allow fast loading // free any memory we allocated to allow fast loading
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}}); }});
// Load widgets // Load widgets
Bangle.loadWidgets(); Bangle.loadWidgets();

View File

@ -29,5 +29,4 @@
} }
E.showMenu(buildMainMenu()); E.showMenu(buildMainMenu());
}); })

View File

@ -109,4 +109,4 @@
}, },
}; };
E.showMenu(menu); E.showMenu(menu);
}); })

View File

@ -15,3 +15,5 @@
0.13: Show day of the week in date 0.13: Show day of the week in date
0.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones 0.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones
0.15: Minor code improvements 0.15: Minor code improvements
0.16: Correct date for all day events in negative timezones, improve locale display
0.17: Fixed "Today" and "Tomorrow" labels displaying in non-current weeks

View File

@ -30,20 +30,26 @@ var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp); CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
function getDate(timestamp) { function getDate(timestamp, allDay) {
return new Date(timestamp*1000); // All day events are always in UTC and always start at 00:00:00, so we
// need to "undo" the timezone offsetting to make sure that the day is
// correct.
var offset = allDay ? new Date().getTimezoneOffset() * 60 : 0
return new Date((timestamp+offset)*1000);
} }
function formatDay(date) { function formatDay(date) {
let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/,*\s*\d\d\d\d/,"");
if (!settings.useToday) { if (!settings.useToday) {
return formattedDate; return formattedDate;
} }
const today = new Date(Date.now()); const today = new Date(Date.now());
if (date.getDay() == today.getDay() && date.getMonth() == today.getMonth()) if (date.getDate() == today.getDate())
return /*LANG*/"Today "; return /*LANG*/"Today ";
else { else {
const tomorrow = new Date(Date.now() + 86400 * 1000); var tomorrow = new Date();
if (date.getDay() == tomorrow.getDay() && date.getMonth() == tomorrow.getMonth()) { tomorrow.setDate(tomorrow.getDate() + 1);
if (date.getDate() == tomorrow.getDate()) {
return /*LANG*/"Tomorrow "; return /*LANG*/"Tomorrow ";
} }
return formattedDate; return formattedDate;
@ -57,8 +63,9 @@ function formatDateLong(date, includeDay, allDay) {
} }
return shortTime; return shortTime;
} }
function formatDateShort(date, allDay) { function formatDateShort(date, allDay) {
return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date)); return formatDay(date)+(allDay?"":" "+Locale.time(date,1)+Locale.meridian(date));
} }
var lines = []; var lines = [];
@ -69,16 +76,19 @@ function showEvent(ev) {
//var lines = []; //var lines = [];
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10); if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
var titleCnt = lines.length; var titleCnt = lines.length;
var start = getDate(ev.timestamp); var start = getDate(ev.timestamp, ev.allDay);
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds)); // All day events end at the midnight boundary of the following day. Here, we
// subtract one second for all day events so the days display correctly.
const allDayEndCorrection = ev.allDay ? 1 : 0;
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds) - allDayEndCorrection, ev.allDay);
var includeDay = true; var includeDay = true;
if (titleCnt) lines.push(""); // add blank line after title if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth()) if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false; includeDay = false;
if(includeDay && ev.allDay) { if(!includeDay && ev.allDay) {
//single day all day (average to avoid getting previous day) //single day all day
lines = lines.concat( lines = lines.concat(
g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10)); g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10));
} else if(includeDay || ev.allDay) { } else if(includeDay || ev.allDay) {
lines = lines.concat( lines = lines.concat(
/*LANG*/"Start"+":", /*LANG*/"Start"+":",
@ -137,7 +147,7 @@ function showList() {
if (!ev) return; if (!ev) return;
var isPast = false; var isPast = false;
var x = r.x+2, title = ev.title; var x = r.x+2, title = ev.title;
var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location"); var body = formatDateShort(getDate(ev.timestamp, ev.allDay),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000; if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
if (title) g.setFontAlign(-1,-1).setFont(fontBig) if (title) g.setFontAlign(-1,-1).setFont(fontBig)
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2); .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2);

View File

@ -1,7 +1,7 @@
{ {
"id": "agenda", "id": "agenda",
"name": "Agenda", "name": "Agenda",
"version": "0.15", "version": "0.17",
"description": "Simple agenda", "description": "Simple agenda",
"icon": "agenda.png", "icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],

View File

@ -68,4 +68,4 @@ function buildMainMenu() {
} }
E.showMenu(buildMainMenu()); E.showMenu(buildMainMenu());
}); })

View File

@ -51,3 +51,7 @@
0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it. 0.46: Show alarm groups if the Show Group setting is ON. Scroll alarms menu back to previous position when getting back to it.
0.47: Fix wrap around when snoozed through midnight 0.47: Fix wrap around when snoozed through midnight
0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year. 0.48: Use datetimeinput for Events, if available. Scroll back when getting out of group. Menu date format setting for shorter dates on current year.
0.49: fix uncaught error if no scroller (Bangle 1). Would happen when trying
to select an alarm in the main menu.
0.50: Bangle.js 2: Long touch of alarm in main menu toggle it on/off. Touching the icon on
the right will do the same.

View File

@ -20,6 +20,8 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
- `Disable All` &rarr; Disable _all_ enabled alarms & timers - `Disable All` &rarr; Disable _all_ enabled alarms & timers
- `Delete All` &rarr; Delete _all_ alarms & timers - `Delete All` &rarr; Delete _all_ alarms & timers
On Bangle.js 2 it's possible to toggle alarms, timers and events from the main menu. This is done by clicking the indicator icons of corresponding entries. Or long pressing anywhere on them.
## Creator ## Creator
- [Gordon Williams](https://github.com/gfwilliams) - [Gordon Williams](https://github.com/gfwilliams)
@ -29,6 +31,7 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features - [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support - [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
- [storm64](https://github.com/storm64) - Fix redrawing in submenus - [storm64](https://github.com/storm64) - Fix redrawing in submenus
- [thyttan](https://github.com/thyttan) - Toggle alarms directly from main menu.
## Attributions ## Attributions

View File

@ -88,13 +88,23 @@ function showMainMenu(scroll, group, scrollback) {
const getGroups = settings.showGroup && !group; const getGroups = settings.showGroup && !group;
const groups = getGroups ? {} : undefined; const groups = getGroups ? {} : undefined;
var showAlarm; var showAlarm;
const getIcon = (e)=>{return e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff);};
alarms.forEach((e, index) => { alarms.forEach((e, index) => {
showAlarm = !settings.showGroup || (group ? e.group === group : !e.group); showAlarm = !settings.showGroup || (group ? e.group === group : !e.group);
if(showAlarm) { if(showAlarm) {
menu[trimLabel(getLabel(e),40)] = { const label = trimLabel(getLabel(e),40);
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff), menu[label] = {
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller.scroll, group) value: e.on,
onchange: (v, touch) => {
if (touch && (2==touch.type || 145<touch.x)) { // Long touch or touched icon.
e.on = v;
saveAndReload();
} else {
setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index, undefined, scroller?scroller.scroll:undefined, group);
}
},
format: v=>getIcon(e)
}; };
} else if (getGroups) { } else if (getGroups) {
groups[e.group] = undefined; groups[e.group] = undefined;
@ -102,7 +112,7 @@ function showMainMenu(scroll, group, scrollback) {
}); });
if (!group) { if (!group) {
Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller.scroll)); Object.keys(groups).sort().forEach(g => menu[g] = () => showMainMenu(null, g, scroller?scroller.scroll:undefined));
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu(); menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
} }

View File

@ -2,7 +2,7 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.48", "version": "0.50",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm", "tags": "tool,alarm",

View File

@ -48,4 +48,4 @@
}; };
E.showMenu(appMenu); E.showMenu(appMenu);
}); })

View File

@ -2,13 +2,26 @@ Alpine Navigator
================ ================
App that performs GPS monitoring to track and display position relative to a given origin in realtime. App that performs GPS monitoring to track and display position relative to a given origin in realtime.
![screenshot](./sample.png) ![screenshot](./sample.png)
[compass 5]
altitude
[start 1] [current 2]
distance
[from start 3] [track 4]
[btn1 -- screen lock]
[btn2 -- remove points]
[btn3 -- pause]
Functions Functions
--------- ---------
Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially. Note if you've not used GPS yet, I suggest using one of the GPS apps to get your first fix and confirm, as I've found that helps initially.
The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed: The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. All your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit, but I've kept it fairly conservative here, as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
1. altitude at origin, this is displayed left of the centre. 1. altitude at origin, this is displayed left of the centre.
2. current altitude, displayed centre right 2. current altitude, displayed centre right
@ -16,12 +29,12 @@ The GPS and magnetometer will be turned on and after a few moments, when the wat
4. distance travelled, bottom right (meters) 4. distance travelled, bottom right (meters)
5. compass heading, at the top 5. compass heading, at the top
For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals. For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time, because the waypoints will be reduced when it reaches a set threshold, so you may see the path smooth out slightly at intervals.
If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough. If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring, but often just moving your wrist a bit is enough.
The buttons do the following: The buttons do the following:
BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work. BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! Soft and hard reset will both still work.
BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route. BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route.
BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen. BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen.

View File

@ -5,3 +5,5 @@
0.05: Fix support for dark theme + support widgets + 0.05: Fix support for dark theme + support widgets +
add settings for widgets, order of drawing and hour hand length add settings for widgets, order of drawing and hour hand length
0.06: Fix issue showing widgets when app is fast-loaded into from launcher with widgets disabled 0.06: Fix issue showing widgets when app is fast-loaded into from launcher with widgets disabled
0.07: Enable fast loading and queue updates to the second
0.08: Restore redraw on charging event + fixup for safer fast-loading

View File

@ -1,3 +1,4 @@
{
const defaultSettings = { const defaultSettings = {
loadWidgets : false, loadWidgets : false,
textAboveHands : false, textAboveHands : false,
@ -11,9 +12,9 @@ const zahlpos=(function() {
let z=[]; let z=[];
let sk=1; let sk=1;
for(let i=-10;i<50;i+=5){ for(let i=-10;i<50;i+=5){
let win=i*2*Math.PI/60; let win=i*2*Math.PI/60;
let xsk =c.x+2+Math.cos(win)*(c.x-10), let xsk =c.x+2+Math.cos(win)*(c.x-10),
ysk =c.y+2+Math.sin(win)*(c.x-10); ysk =c.y+2+Math.sin(win)*(c.x-10);
if(sk==3){xsk-=10;} if(sk==3){xsk-=10;}
if(sk==6){ysk-=10;} if(sk==6){ysk-=10;}
if(sk==9){xsk+=10;} if(sk==9){xsk+=10;}
@ -25,18 +26,15 @@ const zahlpos=(function() {
return z; return z;
})(); })();
let unlock = false; const zeiger = function(len,dia,tim) {
function zeiger(len,dia,tim){
const x=c.x+ Math.cos(tim)*len/2, const x=c.x+ Math.cos(tim)*len/2,
y=c.y + Math.sin(tim)*len/2, y=c.y + Math.sin(tim)*len/2,
d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)}, d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)},
pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y]; pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y];
return pol; return pol;
};
} const drawHands = function(d) {
function drawHands(d) {
let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds(); let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds();
g.setColor(1,1,1); g.setColor(1,1,1);
@ -61,32 +59,60 @@ function drawHands(d) {
g.fillPoly(sekz,true); g.fillPoly(sekz,true);
} }
g.fillCircle(c.x,c.y,4); g.fillCircle(c.x,c.y,4);
} };
function drawText(d) { const drawText = function(d) {
g.setFont("Vector",10); g.setFont("Vector",10);
g.setBgColor(0,0,0); g.setBgColor(0,0,0);
g.setColor(1,1,1); g.setColor(1,1,1);
let dateStr = require("locale").date(d); const dateStr = require("locale").date(d);
g.drawString(dateStr, c.x, c.y+20, true); g.drawString(dateStr, c.x, c.y+20, true);
let batStr = Math.round(E.getBattery()/5)*5+"%"; const batStr = Math.round(E.getBattery()/5)*5+"%";
if (Bangle.isCharging()) { if (Bangle.isCharging()) {
g.setBgColor(1,0,0); g.setBgColor(1,0,0);
} }
g.drawString(batStr, c.x, c.y+40, true); g.drawString(batStr, c.x, c.y+40, true);
} };
function drawNumbers() { const drawNumbers = function() {
//draws the numbers on the screen //draws the numbers on the screen
g.setFont("Vector",20); g.setFont("Vector",20);
g.setColor(1,1,1); g.setColor(1,1,1);
g.setBgColor(0,0,0); g.setBgColor(0,0,0);
for(let i = 0;i<12;i++){ for(let i = 0;i<12;i++){
g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true); g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true);
} }
} };
function draw(){ let drawTimeout;
let queueMillis = 1000;
let unlock = true;
const updateState = function() {
if (Bangle.isLCDOn()) {
if (!Bangle.isLocked()) {
queueMillis = 1000;
unlock = true;
} else {
queueMillis = 60000;
unlock = false;
}
draw();
} else {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
};
const queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, queueMillis - (Date.now() % queueMillis));
};
const draw = function() {
// draw black rectangle in the middle to clear screen from scale and hands // draw black rectangle in the middle to clear screen from scale and hands
g.setColor(0,0,0); g.setColor(0,0,0);
g.fillRect(10,10,2*c.x-10,2*c.x-10); g.fillRect(10,10,2*c.x-10,2*c.x-10);
@ -100,10 +126,11 @@ function draw(){
} else { } else {
drawText(d); drawHands(d); drawText(d); drawHands(d);
} }
} queueDraw();
};
//draws the scale once the app is startet //draws the scale once the app is startet
function drawScale(){ const drawScale = function() {
// clear the screen // clear the screen
g.setBgColor(0,0,0); g.setBgColor(0,0,0);
g.clear(); g.clear();
@ -117,36 +144,35 @@ function drawScale(){
g.fillRect(10,10,2*c.x-10,2*c.x-10); g.fillRect(10,10,2*c.x-10,2*c.x-10);
g.setColor(1,1,1); g.setColor(1,1,1);
} }
} };
//// main running sequence //// //// main running sequence ////
// Show launcher when middle button pressed, and widgets that we're clock // Show launcher when middle button pressed, and widgets that we're clock
Bangle.setUI("clock"); Bangle.setUI({
mode: "clock",
remove: function() {
Bangle.removeListener('lcdPower', updateState);
Bangle.removeListener('lock', updateState);
Bangle.removeListener('charging', draw);
// We clear drawTimout after removing all listeners, because they can add one again
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
require("widget_utils").show();
}
});
// Load widgets if needed, and make them show swipeable // Load widgets if needed, and make them show swipeable
if (settings.loadWidgets) { if (settings.loadWidgets) {
Bangle.loadWidgets(); Bangle.loadWidgets();
require("widget_utils").swipeOn(); require("widget_utils").swipeOn();
} else if (global.WIDGETS) require("widget_utils").hide(); } else if (global.WIDGETS) require("widget_utils").hide();
// Clear the screen once, at startup
drawScale();
draw();
let secondInterval = setInterval(draw, 1000);
// Stop updates when LCD is off, restart when on // Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{ Bangle.on('lcdPower', updateState);
if (secondInterval) clearInterval(secondInterval); Bangle.on('lock', updateState);
secondInterval = undefined; Bangle.on('charging', draw); // Immediately redraw when charger (dis)connected
if (on) {
secondInterval = setInterval(draw, 1000); updateState();
draw(); // draw immediately drawScale();
} draw();
}); }
Bangle.on('lock',on=>{
unlock = !on;
if (secondInterval) clearInterval(secondInterval);
secondInterval = setInterval(draw, unlock ? 1000 : 60000);
draw(); // draw immediately
});
Bangle.on('charging',on=>{draw();});

View File

@ -1,7 +1,7 @@
{ "id": "andark", { "id": "andark",
"name": "Analog Dark", "name": "Analog Dark",
"shortName":"AnDark", "shortName":"AnDark",
"version":"0.06", "version":"0.08",
"description": "analog clock face without disturbing widgets", "description": "analog clock face without disturbing widgets",
"icon": "andark_icon.png", "icon": "andark_icon.png",
"type": "clock", "type": "clock",

View File

@ -25,4 +25,4 @@
}; };
E.showMenu(appMenu); E.showMenu(appMenu);
}); })

View File

@ -36,4 +36,13 @@
0.34: Implement API for activity tracks fetching (Recorder app logs). 0.34: Implement API for activity tracks fetching (Recorder app logs).
0.35: Implement API to enable/disable acceleration data tracking. 0.35: Implement API to enable/disable acceleration data tracking.
0.36: Move from wrapper function to {} and let - faster execution at boot 0.36: Move from wrapper function to {} and let - faster execution at boot
Allow `calendar-` to take an array of items to remove Allow `calendar-` to take an array of items to remove
0.37: Support Gadgetbridge canned responses
0.38: Don't rewrite settings file on every boot!
0.39: Move GB message handling into a library to reduce boot time from 40ms->13ms
0.40: Ensure we send health 'activity' message to gadgetbridge (added 2v26)
0.41: When using `actfetch`, fetch historical activity type too
0.42: Add handling for android STREAM_MUSIC volume level info, emitting on
arrival. (Needs Gadgetbridge nightly (either flavor) for now, or stable
version 85 when it's out)

View File

@ -49,11 +49,21 @@ The boot code also provides some useful functions:
* `body` the body of the HTTP request * `body` the body of the HTTP request
* `headers` an object of headers, eg `{HeaderOne : "headercontents"}` * `headers` an object of headers, eg `{HeaderOne : "headercontents"}`
`Bangle.http` returns a promise which contains:
```JS
{
t:"http",
id: // the ID of this HTTP request
resp: "...." // a string containing the response
}
```
eg: eg:
``` ```JS
Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{ Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
console.log("Got ",data); console.log("Got ",data.resp);
}); });
``` ```

View File

@ -1,350 +1,24 @@
/* global GB */ /* global GB */
{ {
let gbSend = function(message) { // settings var is deleted after this executes to save memory
Bluetooth.println(""); let settings = Object.assign({rp:true,as:true,vibrate:".."},
Bluetooth.println(JSON.stringify(message)); require("Storage").readJSON("android.settings.json",1)||{}
} );
let lastMsg; // for music messages - may not be needed now...
let actInterval; // Realtime activity reporting interval when `act` is true
let actHRMHandler; // For Realtime activity reporting
let gpsState = {}; // keep information on GPS via Gadgetbridge
// this settings var is deleted after this executes to save memory
let settings = require("Storage").readJSON("android.settings.json",1)||{};
//default alarm settings
if (settings.rp == undefined) settings.rp = true;
if (settings.as == undefined) settings.as = true;
if (settings.vibrate == undefined) settings.vibrate = "..";
require('Storage').writeJSON("android.settings.json", settings);
let _GB = global.GB; let _GB = global.GB;
let fetchRecInterval; global.GB = e => {
global.GB = (event) => {
// feed a copy to other handlers if there were any // feed a copy to other handlers if there were any
if (_GB) setTimeout(_GB,0,Object.assign({},event)); if (_GB) setTimeout(_GB,0,Object.assign({},e));
Bangle.emit("GB",e);
require("android").gbHandler(e);
/* 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});
// 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
"notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
// {t:"find", n:bool} // find my phone
"find" : function() {
if (Bangle.findDeviceInterval) {
clearInterval(Bangle.findDeviceInterval);
delete Bangle.findDeviceInterval;
}
if (event.n) // Ignore quiet mode: we always want to find our watch
Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
},
// {t:"musicstate", state:"play/pause",position,shuffle,repeat}
"musicstate" : function() {
require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
},
// {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
"musicinfo" : function() {
require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
},
// {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"})
"call" : function() {
Object.assign(event, {
t:event.cmd=="incoming"?"add":"remove",
id:"call", src:"Phone",
positive:true, negative:true,
title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
require("messages").pushMessage(event);
},
// {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
"alarm" : function() {
//wipe existing GB alarms
var sched;
try { sched = require("sched"); } catch (e) {}
if (!sched) return; // alarms may not be installed
var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
for (var i = 0; i < gbalarms.length; i++)
sched.setAlarm(gbalarms[i].id, undefined);
var alarms = sched.getAlarms();
var time = new Date();
var currentTime = time.getHours() * 3600000 +
time.getMinutes() * 60000 +
time.getSeconds() * 1000;
for (var j = 0; j < event.d.length; j++) {
// prevents all alarms from going off at once??
var dow = event.d[j].rep;
var rp = false;
if (!dow) {
dow = 127; //if no DOW selected, set alarm to all DOW
} else {
rp = true;
}
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
a.appid = "gbalarms";
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.rp = rp;
a.last = last;
alarms.push(a);
}
sched.setAlarms(alarms);
sched.reload();
},
//TODO perhaps move those in a library (like messages), used also for viewing events?
//add and remove events based on activity on phone (pebble-like)
// {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
"calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
var i = cal.findIndex(e=>e.id==event.id);
if(i<0)
cal.push(event);
else
cal[i] = event;
require("Storage").writeJSON("android.calendar.json", cal);
},
// {t:"calendar-", id:int}
"calendar-" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
//if any of those happen we are out of sync!
if (!cal || !Array.isArray(cal)) cal = [];
if (Array.isArray(event.id))
cal = cal.filter(e=>!event.id.includes(e.id));
else
cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal);
},
//triggered by GB, send all ids
// { t:"force_calendar_sync_start" }
"force_calendar_sync_start" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
},
// {t:"http",resp:"......",[id:"..."]}
"http":function() {
//get the promise and call the promise resolve
if (Bangle.httpRequest === undefined) return;
var request=Bangle.httpRequest[event.id];
if (request === undefined) return; //already timedout or wrong id
delete Bangle.httpRequest[event.id];
clearTimeout(request.t); //t = timeout variable
if(event.err!==undefined) //if is error
request.j(event.err); //r = reJect function
else
request.r(event); //r = resolve function
},
// {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
"gps": function() {
if (!settings.overwriteGps) return;
// modify event for using it as Bangle GPS event
delete event.t;
if (!isFinite(event.satellites)) event.satellites = NaN;
if (!isFinite(event.course)) event.course = NaN;
event.fix = 1;
if (event.long!==undefined) { // for earlier Gadgetbridge implementations
event.lon = event.long;
delete event.long;
}
if (event.time){
event.time = new Date(event.time);
}
if (!gpsState.lastGPSEvent) {
// this is the first event, save time of arrival and deactivate internal GPS
Bangle.moveGPSPower(0);
} else {
// this is the second event, store the intervall for expecting the next GPS event
gpsState.interval = Date.now() - gpsState.lastGPSEvent;
}
gpsState.lastGPSEvent = Date.now();
// in any case, cleanup the GPS state in case no new events arrive
if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS);
gpsState.timeoutGPS = setTimeout(()=>{
// reset state
gpsState.lastGPSEvent = undefined;
gpsState.timeoutGPS = undefined;
gpsState.interval = undefined;
// did not get an expected GPS event but have GPS clients, switch back to internal GPS
if (Bangle.isGPSOn()) Bangle.moveGPSPower(1);
}, (gpsState.interval || 10000) + 1000);
Bangle.emit('GPS', event);
},
// {t:"is_gps_active"}
"is_gps_active": function() {
gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
},
// {t:"act", hrm:bool, stp:bool, int:int}
"act": function() {
if (actInterval) clearInterval(actInterval);
actInterval = undefined;
if (actHRMHandler)
actHRMHandler = undefined;
Bangle.setHRMPower(event.hrm,"androidact");
if (!(event.hrm || event.stp)) return;
if (!isFinite(event.int)) event.int=1;
var lastSteps = Bangle.getStepCount();
var lastBPM = 0;
actHRMHandler = function(e) {
lastBPM = e.bpm;
};
Bangle.on('HRM',actHRMHandler);
actInterval = setInterval(function() {
var steps = Bangle.getStepCount();
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
lastSteps = steps;
}, event.int*1000);
},
// {t:"actfetch", ts:long}
"actfetch": function() {
gbSend({t: "actfetch", state: "start"});
var actCount = 0;
var actCb = function(r) {
// The health lib saves the samples at the start of the 10-minute block
// However, GB expects them at the end of the block, so let's offset them
// here to keep a consistent API in the health lib
var sampleTs = r.date.getTime() + 600000;
if (sampleTs >= event.ts) {
gbSend({
t: "act",
ts: sampleTs,
stp: r.steps,
hrm: r.bpm,
mov: r.movement
});
actCount++;
}
}
if (event.ts != 0) {
require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
} else {
require("health").readFullDatabase(actCb);
}
gbSend({t: "actfetch", state: "end", count: actCount});
},
//{t:"listRecs", id:"20230616a"}
"listRecs": function() {
let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21));
if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back.
let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id);
if (-1 == firstNonsyncedIdx) {
recs = []
} else {
recs = recs.slice(firstNonsyncedIdx);
}
}
gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions?
},
//{t:"fetchRec", id:"20230616a"}
"fetchRec": function() {
// TODO: Decide on what names keys should have.
if (fetchRecInterval) {
clearInterval(fetchRecInterval);
fetchRecInterval = undefined;
}
if (event.id=="stop") {
return
} else {
let log = require("Storage").open("recorder.log"+event.id+".csv","r");
let lines = "init";// = log.readLine();
let pkgcnt = 0;
gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch.
let sendlines = ()=>{
lines = log.readLine();
for (var i = 0; i < 3; i++) {
let line = log.readLine();
if (line) lines += line;
}
pkgcnt++;
gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt});
if (!lines && fetchRecInterval) {
clearInterval(fetchRecInterval);
fetchRecInterval = undefined;
}
}
fetchRecInterval = setInterval(sendlines, 50)
}
},
"nav": function() {
event.id="nav";
if (event.instr) {
event.t="add";
event.src="maps"; // for the icon
event.title="Navigation";
if (require("messages").getMessages().find(m=>m.id=="nav"))
event.t = "modify";
} else {
event.t="remove";
}
require("messages").pushMessage(event);
},
"cards" : function() {
// we receive all, just override what we have
if (Array.isArray(event.d))
require("Storage").writeJSON("android.cards.json", event.d);
},
"accelsender": function () {
require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval});
load();
}
};
var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event);
}; };
// HTTP request handling - see the readme // HTTP request handling - see the readme
// options = {id,timeout,xpath} Bangle.http = (url,options)=>require("android").httpHandler(url,options);
Bangle.http = (url,options)=>{
options = options||{};
if (!NRF.getSecurityStatus().connected)
return Promise.reject(/*LANG*/"Not connected to Bluetooth");
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;
if (options.return) req.return = options.return; // for xpath
if (options.method) req.method = options.method;
if (options.body) req.body = options.body;
if (options.headers) req.headers = options.headers;
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 // Battery monitor
let sendBattery = function() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } let sendBattery = function() { require("android").gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
Bangle.on("charging", sendBattery); Bangle.on("charging", sendBattery);
NRF.on("connect", () => setTimeout(function() { NRF.on("connect", () => setTimeout(function() {
sendBattery(); sendBattery();
gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION}); require("android").gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
}, 2000)); }, 2000));
NRF.on("disconnect", () => { NRF.on("disconnect", () => {
@ -358,81 +32,24 @@
setInterval(sendBattery, 10*60*1000); setInterval(sendBattery, 10*60*1000);
// Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins // Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
Bangle.on('health', h=>{ Bangle.on('health', h=>{
gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement }); require("android").gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement, act: h.activity }); // h.activity added in 2v26
}); });
// Music control // Music control
Bangle.musicControl = cmd => { Bangle.musicControl = cmd => {
// play/pause/next/previous/volumeup/volumedown // play/pause/next/previous/volumeup/volumedown
gbSend({ t: "music", n:cmd }); require("android").gbSend({ t: "music", n:cmd });
}; };
// Message response // Message response
Bangle.messageResponse = (msg,response) => { Bangle.messageResponse = (msg,response) => {
if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" }); if (msg.id=="call") return require("android").gbSend({ t: "call", n:response?"ACCEPT":"REJECT" });
if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id });
// error/warn here? // error/warn here?
}; };
Bangle.messageIgnore = msg => { Bangle.messageIgnore = msg => {
if (isFinite(msg.id)) return gbSend({ t: "notify", n:"MUTE", id: msg.id }); if (isFinite(msg.id)) return require("android").gbSend({ t: "notify", n:"MUTE", id: msg.id });
}; };
// GPS overwrite logic // GPS overwrite logic
if (settings.overwriteGps) { // if the overwrite option is set.. if (settings.overwriteGps) require("android").overwriteGPS();
const origSetGPSPower = Bangle.setGPSPower;
Bangle.moveGPSPower = (state) => {
if (Bangle.isGPSOn()){
let orig = Bangle._PWR.GPS;
delete Bangle._PWR.GPS;
origSetGPSPower(state);
Bangle._PWR.GPS = orig;
}
};
// work around Serial1 for GPS not working when connected to something
let serialTimeout;
let wrap = function(f){
return (s)=>{
if (serialTimeout) clearTimeout(serialTimeout);
origSetGPSPower(1, "androidgpsserial");
f(s);
serialTimeout = setTimeout(()=>{
serialTimeout = undefined;
origSetGPSPower(0, "androidgpsserial");
}, 10000);
};
};
Serial1.println = wrap(Serial1.println);
Serial1.write = wrap(Serial1.write);
// replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
Bangle.setGPSPower = ((isOn, appID) => {
let pwr;
if (!this.lastGPSEvent){
// use internal GPS power function if no gps event has arrived from GadgetBridge
pwr = origSetGPSPower(isOn, appID);
} else {
// we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app
if (!Bangle._PWR) Bangle._PWR={};
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
if (!appID) appID="?";
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
pwr = Bangle._PWR.GPS.length>0;
// stop internal GPS, no clients left
if (!pwr) origSetGPSPower(0);
}
// always update Gadgetbridge on current power state
gbSend({ t: "gps_power", status: pwr });
return pwr;
}).bind(gpsState);
// allow checking for GPS via GadgetBridge
Bangle.isGPSOn = () => {
return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
};
// stop GPS on boot if not activated
setTimeout(()=>{
if (!Bangle.isGPSOn()) gbSend({ t: "gps_power", status: false });
},3000);
}
// remove settings object so it's not taking up RAM // remove settings object so it's not taking up RAM
delete settings; delete settings;
} }

393
apps/android/lib.js Normal file
View File

@ -0,0 +1,393 @@
exports.gbSend = function(message) {
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
let lastMsg, // for music messages - may not be needed now...
gpsState = {}, // keep information on GPS via Gadgetbridge
settings = Object.assign({rp:true,as:true,vibrate:".."},
require("Storage").readJSON("android.settings.json",1)||{}
);
exports.gbHandler = (event) => {
var HANDLERS = {
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
"notify" : function() {
print("notify",event);
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
"notify-" : function() { event.t="remove";require("messages").pushMessage(event); },
// {t:"find", n:bool} // find my phone
"find" : function() {
if (Bangle.findDeviceInterval) {
clearInterval(Bangle.findDeviceInterval);
delete Bangle.findDeviceInterval;
}
if (event.n) // Ignore quiet mode: we always want to find our watch
Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000);
},
// {t:"musicstate", state:"play/pause",position,shuffle,repeat}
"musicstate" : function() {
require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state});
},
// {t:"musicinfo", artist,album,track,dur,c(track count),n(track num}
"musicinfo" : function() {
require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"}));
},
// {t:"audio", v:(percentage of max volume for android STREAM_MUSIC)}
"audio" : function() {
Bangle.emit("musicVolume", event.v);
},
// {"t":"call","cmd":"incoming/end/start/outgoing","name":"Bob","number":"12421312"})
"call" : function() {
Object.assign(event, {
t:event.cmd=="incoming"?"add":"remove",
id:"call", src:"Phone",
positive:true, negative:true,
title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
require("messages").pushMessage(event);
},
"canned_responses_sync" : function() {
require("Storage").writeJSON("replies.json", event.d);
},
// {"t":"alarm", "d":[{h:int,m:int,rep:int},... }
"alarm" : function() {
//wipe existing GB alarms
var sched;
try { sched = require("sched"); } catch (e) {}
if (!sched) return; // alarms may not be installed
var gbalarms = sched.getAlarms().filter(a=>a.appid=="gbalarms");
for (var i = 0; i < gbalarms.length; i++)
sched.setAlarm(gbalarms[i].id, undefined);
var alarms = sched.getAlarms();
var time = new Date();
var currentTime = time.getHours() * 3600000 +
time.getMinutes() * 60000 +
time.getSeconds() * 1000;
for (var j = 0; j < event.d.length; j++) {
// prevents all alarms from going off at once??
var dow = event.d[j].rep;
var rp = false;
if (!dow) {
dow = 127; //if no DOW selected, set alarm to all DOW
} else {
rp = true;
}
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
a.appid = "gbalarms";
a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.rp = rp;
a.last = last;
alarms.push(a);
}
sched.setAlarms(alarms);
sched.reload();
},
//TODO perhaps move those in a library (like messages), used also for viewing events?
//add and remove events based on activity on phone (pebble-like)
// {t:"calendar", id:int, type:int, timestamp:seconds, durationInSeconds, title:string, description:string,location:string,calName:string.color:int,allDay:bool
"calendar" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
var i = cal.findIndex(e=>e.id==event.id);
if(i<0)
cal.push(event);
else
cal[i] = event;
require("Storage").writeJSON("android.calendar.json", cal);
},
// {t:"calendar-", id:int}
"calendar-" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
//if any of those happen we are out of sync!
if (!cal || !Array.isArray(cal)) cal = [];
if (Array.isArray(event.id))
cal = cal.filter(e=>!event.id.includes(e.id));
else
cal = cal.filter(e=>e.id!=event.id);
require("Storage").writeJSON("android.calendar.json", cal);
},
//triggered by GB, send all ids
// { t:"force_calendar_sync_start" }
"force_calendar_sync_start" : function() {
var cal = require("Storage").readJSON("android.calendar.json",true);
if (!cal || !Array.isArray(cal)) cal = [];
exports.gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
},
// {t:"http",resp:"......",[id:"..."]}
"http":function() {
//get the promise and call the promise resolve
if (Bangle.httpRequest === undefined) return;
var request=Bangle.httpRequest[event.id];
if (request === undefined) return; //already timedout or wrong id
delete Bangle.httpRequest[event.id];
clearTimeout(request.t); //t = timeout variable
if(event.err!==undefined) //if is error
request.j(event.err); //r = reJect function
else
request.r(event); //r = resolve function
},
// {t:"gps", lat, lon, alt, speed, course, time, satellites, hdop, externalSource:true }
"gps": function() {
if (!settings.overwriteGps) return;
// modify event for using it as Bangle GPS event
delete event.t;
if (!isFinite(event.satellites)) event.satellites = NaN;
if (!isFinite(event.course)) event.course = NaN;
event.fix = 1;
if (event.long!==undefined) { // for earlier Gadgetbridge implementations
event.lon = event.long;
delete event.long;
}
if (event.time){
event.time = new Date(event.time);
}
if (!gpsState.lastGPSEvent) {
// this is the first event, save time of arrival and deactivate internal GPS
Bangle.moveGPSPower(0);
} else {
// this is the second event, store the intervall for expecting the next GPS event
gpsState.interval = Date.now() - gpsState.lastGPSEvent;
}
gpsState.lastGPSEvent = Date.now();
// in any case, cleanup the GPS state in case no new events arrive
if (gpsState.timeoutGPS) clearTimeout(gpsState.timeoutGPS);
gpsState.timeoutGPS = setTimeout(()=>{
// reset state
gpsState.lastGPSEvent = undefined;
gpsState.timeoutGPS = undefined;
gpsState.interval = undefined;
// did not get an expected GPS event but have GPS clients, switch back to internal GPS
if (Bangle.isGPSOn()) Bangle.moveGPSPower(1);
}, (gpsState.interval || 10000) + 1000);
Bangle.emit('GPS', event);
},
// {t:"is_gps_active"}
"is_gps_active": function() {
exports.gbSend({ t: "gps_power", status: Bangle.isGPSOn() });
},
// {t:"act", hrm:bool, stp:bool, int:int}
"act": function() {
if (exports.actInterval) clearInterval(exports.actInterval);
exports.actInterval = undefined;
if (exports.actHRMHandler)
exports.actHRMHandler = undefined;
Bangle.setHRMPower(event.hrm,"androidact");
if (!(event.hrm || event.stp)) return;
if (!isFinite(event.int)) event.int=1;
var lastSteps = Bangle.getStepCount();
var lastBPM = 0;
exports.actHRMHandler = function(e) {
lastBPM = e.bpm;
};
Bangle.on('HRM',exports.actHRMHandler);
exports.actInterval = setInterval(function() {
var steps = Bangle.getStepCount();
exports.gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
lastSteps = steps;
}, event.int*1000);
},
// {t:"actfetch", ts:long}
"actfetch": function() {
exports.gbSend({t: "actfetch", state: "start"});
var actCount = 0;
var actCb = function(r) {
// The health lib saves the samples at the start of the 10-minute block
// However, GB expects them at the end of the block, so let's offset them
// here to keep a consistent API in the health lib
var sampleTs = r.date.getTime() + 600000;
if (sampleTs >= event.ts) {
exports.gbSend({
t: "act",
ts: sampleTs,
stp: r.steps,
hrm: r.bpm,
mov: r.movement,
act: r.activity
});
actCount++;
}
}
if (event.ts != 0) {
require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
} else {
require("health").readFullDatabase(actCb);
}
exports.gbSend({t: "actfetch", state: "end", count: actCount});
},
//{t:"listRecs", id:"20230616a"}
"listRecs": function() {
let recs = require("Storage").list(/^recorder\.log.*\.csv$/,{sf:true}).map(s => s.slice(12, 21));
if (event.id.length > 2) { // Handle if there was no id supplied. Then we send a list all available recorder logs back.
let firstNonsyncedIdx = recs.findIndex((logId) => logId > event.id);
if (-1 == firstNonsyncedIdx) {
recs = []
} else {
recs = recs.slice(firstNonsyncedIdx);
}
}
exports.gbSend({t:"actTrksList", list: recs}); // TODO: split up in multiple transmissions?
},
//{t:"fetchRec", id:"20230616a"}
"fetchRec": function() {
// TODO: Decide on what names keys should have.
if (exports.fetchRecInterval) {
clearInterval(exports.fetchRecInterval);
exports.fetchRecInterval = undefined;
}
if (event.id=="stop") {
return;
} else {
let log = require("Storage").open("recorder.log"+event.id+".csv","r");
let lines = "init";// = log.readLine();
let pkgcnt = 0;
exports.gbSend({t:"actTrk", log:event.id, lines:"erase", cnt:pkgcnt}); // "erase" will prompt Gadgetbridge to erase the contents of a already fetched log so we can rewrite it without keeping lines from the previous (probably failed) fetch.
let sendlines = ()=>{
lines = log.readLine();
for (var i = 0; i < 3; i++) {
let line = log.readLine();
if (line) lines += line;
}
pkgcnt++;
exports.gbSend({t:"actTrk", log:event.id, lines:lines, cnt:pkgcnt});
if (!lines && exports.fetchRecInterval) {
clearInterval(exports.fetchRecInterval);
exports.fetchRecInterval = undefined;
}
};
exports.fetchRecInterval = setInterval(sendlines, 50);
}
},
"nav": function() {
event.id="nav";
if (event.instr) {
event.t="add";
event.src="maps"; // for the icon
event.title="Navigation";
if (require("messages").getMessages().find(m=>m.id=="nav"))
event.t = "modify";
} else {
event.t="remove";
}
require("messages").pushMessage(event);
},
"cards" : function() {
// we receive all, just override what we have
if (Array.isArray(event.d))
require("Storage").writeJSON("android.cards.json", event.d);
},
"accelsender": function () {
require("Storage").writeJSON("accelsender.json", {enabled: event.enable, interval: event.interval});
load();
}
};
var h = HANDLERS[event.t];
if (h) h(); else console.log("GB Unknown",event);
};
// HTTP request handling - see the readme
// options = {id,timeout,xpath}
exports.httpHandler = (url,options) => {
options = options||{};
if (!NRF.getSecurityStatus().connected)
return Promise.reject(/*LANG*/"Not connected to Bluetooth");
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;
if (options.return) req.return = options.return; // for xpath
if (options.method) req.method = options.method;
if (options.body) req.body = options.body;
if (options.headers) req.headers = options.headers;
exports.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;
};
exports.overwriteGPS = () => { // if the overwrite option is set, call this on init..
const origSetGPSPower = Bangle.setGPSPower;
Bangle.moveGPSPower = (state) => {
if (Bangle.isGPSOn()){
let orig = Bangle._PWR.GPS;
delete Bangle._PWR.GPS;
origSetGPSPower(state);
Bangle._PWR.GPS = orig;
}
};
// work around Serial1 for GPS not working when connected to something
let serialTimeout;
let wrap = function(f){
return (s)=>{
if (serialTimeout) clearTimeout(serialTimeout);
origSetGPSPower(1, "androidgpsserial");
f(s);
serialTimeout = setTimeout(()=>{
serialTimeout = undefined;
origSetGPSPower(0, "androidgpsserial");
}, 10000);
};
};
Serial1.println = wrap(Serial1.println);
Serial1.write = wrap(Serial1.write);
// replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
Bangle.setGPSPower = ((isOn, appID) => {
let pwr;
if (!this.lastGPSEvent){
// use internal GPS power function if no gps event has arrived from GadgetBridge
pwr = origSetGPSPower(isOn, appID);
} else {
// we are currently expecting the next GPS event from GadgetBridge, keep track of GPS state per app
if (!Bangle._PWR) Bangle._PWR={};
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];
if (!appID) appID="?";
if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID);
if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1);
pwr = Bangle._PWR.GPS.length>0;
// stop internal GPS, no clients left
if (!pwr) origSetGPSPower(0);
}
// always update Gadgetbridge on current power state
require("android").gbSend({ t: "gps_power", status: pwr });
return pwr;
}).bind(gpsState);
// allow checking for GPS via GadgetBridge
Bangle.isGPSOn = () => {
return !!(Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0);
};
// stop GPS on boot if not activated
setTimeout(()=>{
if (!Bangle.isGPSOn()) require("android").gbSend({ t: "gps_power", status: false });
},3000);
};

View File

@ -2,7 +2,7 @@
"id": "android", "id": "android",
"name": "Android Integration", "name": "Android Integration",
"shortName": "Android", "shortName": "Android",
"version": "0.36", "version": "0.42",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge", "tags": "tool,system,messages,notifications,gadgetbridge",
@ -13,7 +13,8 @@
{"name":"android.app.js","url":"app.js"}, {"name":"android.app.js","url":"app.js"},
{"name":"android.settings.js","url":"settings.js"}, {"name":"android.settings.js","url":"settings.js"},
{"name":"android.img","url":"app-icon.js","evaluate":true}, {"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"} {"name":"android.boot.js","url":"boot.js"},
{"name":"android","url":"lib.js"}
], ],
"data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}], "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}],
"sortorder": -8 "sortorder": -8

View File

@ -94,4 +94,4 @@
E.showMenu(mainmenu); E.showMenu(mainmenu);
}); })

View File

@ -3,4 +3,5 @@
0.03: Select GNSS systems to use for Bangle.js 2 0.03: Select GNSS systems to use for Bangle.js 2
0.04: Now turns GPS off after upload 0.04: Now turns GPS off after upload
0.05: Fix regression in 0.04 that caused AGPS data not to get loaded 0.05: Fix regression in 0.04 that caused AGPS data not to get loaded
0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default 0.06: Auto-set GPS output sentences - newer Bangle.js 2 don't include RMC (GPS direction + time) by default
0.07: Bangle.js 2 now gets estimated time + lat/lon from the browser (~3x faster fix)

View File

@ -49,7 +49,7 @@
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> BDS+GLONASS <input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> BDS+GLONASS
</label> </label>
<label class="form-radio"> <label class="form-radio">
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> GPS+BDS+GLONASS <input type="radio" name="gnss_select" value="7"><i class="form-icon"></i> GPS+BDS+GLONASS
</label> </label>
</div> </div>
</div> </div>
@ -60,6 +60,7 @@
<script> <script>
var isB1; // is Bangle.js 1? var isB1; // is Bangle.js 1?
var isB2; // is Bangle.js 2? var isB2; // is Bangle.js 2?
var currentPosition;
// When the 'upload' button is clicked... // When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() { document.getElementById("upload").addEventListener("click", function() {
@ -120,6 +121,7 @@
} }
// =================================================== Bangle.js 2 CASIC // =================================================== Bangle.js 2 CASIC
// https://www.espruino.com/Bangle.js2+Technical#gps
function CASIC_CHECKSUM(cmd) { function CASIC_CHECKSUM(cmd) {
var cs = 0; var cs = 0;
@ -128,6 +130,61 @@
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0'); return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
} }
// Send a binary CASIC packet, eg: {classId:6, messageId:0, payload:[]}
function CASIC_PKT(pkt) {
pkt.payload = pkt.payload || [];
var plen = pkt.payload.length;
var msg = new Uint8Array(10+pkt.payload.length);
msg.set([0xBA,0xCE,
plen, // LENGTH
0x00,
pkt.classId, // CLASS ID
pkt.messageId]); // MESSAGE ID
msg.set(pkt.payload, 6);
var dv = new DataView(msg.buffer);
// checksum
var ckSum = 0;
for (i = -4; i < plen; i+=4)
ckSum = 0|(ckSum+dv.getUint32(6+i, true));
dv.setUint32(6+plen, ckSum, true);
return msg;
}
// Send AID_INI message, {lat,lon,alt}
function AID_INI(pos) {
var msg = new Uint8Array(56);
var dv = new DataView(msg.buffer);
/*
double xOrLat, yOrLon, zOrAlt;
double tow; // 24
float df; // 32
float posAcc; // 36
float tAcc; // 40
float fAcc; // 44
unsigned int res; // 48
unsigned short int wn; // 52
unsigned char timeSource; // 54
unsigned char flags; // 55
*/
var ms = Date.now();
var wk = (ms-new Date("1980-01-06T00:00:00Z")) / 604800000;
var wn = Math.floor(wk); // week number
var tow = (wk-wn) * 604800; // seconds in week
dv.setFloat64(0, pos.lat, true); // xOrLat
dv.setFloat64(8, pos.lon, true); // yOrLon
dv.setFloat64(16, pos.alt, true); // zOrAlt
dv.setFloat64(24, tow, true); // tow
dv.setFloat32(32, 0, true); // df
dv.setFloat32(36, 0, true); // posAcc
dv.setFloat32(40, 0, true); // tAcc
dv.setFloat32(44, 0, true); // fAcc
dv.setUint32(48, 0, true); // res
dv.setUint16(52, wn, true); // wn
dv.setUint8(54,0); // timeSource
dv.setUint8(55, 0x23); // flags ( lat/lon and clock valid, no drift data )
return CASIC_PKT({classId:0x0B, messageId:0x01, payload:msg});
}
// =================================================== // ===================================================
function jsFromBase64(b64) { function jsFromBase64(b64) {
@ -140,7 +197,6 @@
js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time js += `\x10Serial1.write(atob("${btoa(String.fromCharCode.apply(null,UBX_MGA_INI_TIME_UTC()))}"))\n`; // set GPS time
} }
if (isB2) { // CASIC if (isB2) { // CASIC
// Select what GNSS System to use for decreased fix time. // Select what GNSS System to use for decreased fix time.
var radios = document.getElementsByName('gnss_select'); var radios = document.getElementsByName('gnss_select');
var gnss_select="1"; var gnss_select="1";
@ -150,11 +206,11 @@
js += `\x10var t=getTime()+0.5;while (getTime()<t);\n`; // This is nasty - but we just wait here until the GPS has had time to boot js += `\x10var t=getTime()+0.5;while (getTime()<t);\n`; // This is nasty - but we just wait here until the GPS has had time to boot
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnss_select)}")\n`; // set GNSS mode
js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS03,1,0,0,1,1,0,0,0")}")\n`; // enable GGA,GSV,RMC packets (new Bangle.js 2 GPS firmwares don't include RMC by default!) js += `\x10Serial1.println("${CASIC_CHECKSUM("$PCAS03,1,0,0,1,1,0,0,0")}")\n`; // enable GGA,GSV,RMC packets (new Bangle.js 2 GPS firmwares don't include RMC by default!)
// If the browser let us have the current location, give it to the GPS chip to get a faster fix
if (currentPosition) {
js += `\x10Serial1.write([${AID_INI(currentPosition).join(",")}])\n`;
}
// Serial1.println("$PCAS06,0*1B") gets the current firmware version // Serial1.println("$PCAS06,0*1B") gets the current firmware version
// What about:
// NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01)
} }
for (var i=0;i<bin.length;i+=chunkSize) { for (var i=0;i<bin.length;i+=chunkSize) {
@ -184,8 +240,20 @@
document.getElementById("banglejs1-info").style = isB1?"":"display:none"; document.getElementById("banglejs1-info").style = isB1?"":"display:none";
document.getElementById("banglejs2-info").style = isB2?"":"display:none"; document.getElementById("banglejs2-info").style = isB2?"":"display:none";
document.getElementById("upload-wrap").style = ""; document.getElementById("upload-wrap").style = "";
if (isB2) {
// get current position for AGPS improvement
navigator.geolocation.getCurrentPosition(function(position) {
currentPosition = {
lat : position.coords.latitude,
lon : position.coords.longitude,
alt : 0|position.coords.altitude
};
});
}
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
"id": "assistedgps", "id": "assistedgps",
"name": "Assisted GPS Updater (AGPS)", "name": "Assisted GPS Updater (AGPS)",
"shortName": "AGPS", "shortName": "AGPS",
"version": "0.06", "version": "0.07",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"sortorder": -1, "sortorder": -1,
"icon": "app.png", "icon": "app.png",

2
apps/ateatimer/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: First release
0.02: Fix icon, utilize sched, show running timer on app relaunch

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIKHgwFKo0gAofmsALEGR0H/+f//+gEP/4ACAoXAn4FDAQn8g0DAoX4g0BAoXx4E4AoXhAoN/8EP4AzBn/4h/IC4M//kPzgjBz/+h+MAoMfj0PNYUfh4FDh8HAo0wg/454RBmBDBAoRnBCIIjCAAMPF4IFDHYOIgEBj5HBzkAIIPAIIIFBn4hBLIU+AoPgwEQvwFBOIX8CgP5w0RAoSJC/AsB/0EJwIgB/+Aj/wAoN/VgPgQwQFBwBKCXAQWBAAfgAoocCAoQcCAAPAj7XEcYIABcYLIBAAJBBA=="))

156
apps/ateatimer/app.js Normal file
View File

@ -0,0 +1,156 @@
// Tea Timer Application for Bangle.js 2 using sched library
let timerDuration = (() => {
let file = require("Storage").open("ateatimer.data", "r");
let data = file.read(4); // Assuming 4 bytes for storage
return data ? parseInt(data, 10) : 4 * 60; // Default to 4 minutes
})();
let timeRemaining = timerDuration;
let timerRunning = false;
function saveDefaultDuration() {
let file = require("Storage").open("ateatimer.data", "w");
file.write(timerDuration.toString());
}
function drawTime() {
g.clear();
g.setFont("Vector", 40);
g.setFontAlign(0, 0); // Center align
const minutes = Math.floor(Math.abs(timeRemaining) / 60);
const seconds = Math.abs(timeRemaining) % 60;
const sign = timeRemaining < 0 ? "-" : "";
const timeStr = `${sign}${minutes}:${seconds.toString().padStart(2, '0')}`;
g.drawString(timeStr, g.getWidth() / 2, g.getHeight() / 2);
// Draw Increase button (triangle pointing up)
g.fillPoly([
g.getWidth() / 2, g.getHeight() / 2 - 80, // Top vertex
g.getWidth() / 2 - 20, g.getHeight() / 2 - 60, // Bottom-left vertex
g.getWidth() / 2 + 20, g.getHeight() / 2 - 60 // Bottom-right vertex
]);
// Draw Decrease button (triangle pointing down)
g.fillPoly([
g.getWidth() / 2, g.getHeight() / 2 + 80, // Bottom vertex
g.getWidth() / 2 - 20, g.getHeight() / 2 + 60, // Top-left vertex
g.getWidth() / 2 + 20, g.getHeight() / 2 + 60 // Top-right vertex
]);
g.flip();
}
function startTimer() {
if (timerRunning) return;
if (timeRemaining == 0) return;
timerRunning = true;
// Save the default duration on timer start
timerDuration = timeRemaining;
saveDefaultDuration();
scheduleTimer();
// Start the secondary timer to update the display
setInterval(updateDisplay, 1000);
}
function scheduleTimer() {
// Schedule a new timer using the sched library
require("sched").setAlarm("ateatimer", {
msg: "Tea is ready!",
timer: timeRemaining * 1000, // Convert to milliseconds
vibrate: ".." // Default vibration pattern
});
// Ensure the scheduler updates
require("sched").reload();
}
function resetTimer() {
// Cancel the existing timer
require("sched").setAlarm("ateatimer", undefined);
require("sched").reload();
timerRunning = false;
timeRemaining = timerDuration;
drawTime();
}
function adjustTime(amount) {
if (-amount > timeRemaining) {
// Return if result will be negative
return;
}
timeRemaining += amount;
timeRemaining = Math.max(0, timeRemaining); // Ensure time doesn't go negative
if (timerRunning) {
// Update the existing timer with the new remaining time
let alarm = require("sched").getAlarm("ateatimer");
if (alarm) {
// Cancel the current alarm
require("sched").setAlarm("ateatimer", undefined);
// Set a new alarm with the updated time
scheduleTimer();
}
}
drawTime();
}
function handleTouch(x, y) {
const centerY = g.getHeight() / 2;
if (y < centerY - 40) {
// Increase button area
adjustTime(60);
} else if (y > centerY + 40) {
// Decrease button area
adjustTime(-60);
} else {
// Center area
if (!timerRunning) {
startTimer();
}
}
}
// Function to update the display every second
function updateDisplay() {
if (timerRunning) {
let alarm = require("sched").getAlarm("ateatimer");
timeRemaining = Math.floor(require("sched").getTimeToAlarm(alarm) / 1000);
drawTime();
if (timeRemaining <= 0) {
timeRemaining = 0;
clearInterval(updateDisplay);
timerRunning = false;
}
}
}
// Handle physical button press for resetting timer
setWatch(() => {
if (timerRunning) {
resetTimer();
} else {
startTimer();
}
}, BTN1, { repeat: true, edge: "falling" });
// Handle touch
Bangle.on("touch", (zone, xy) => {
handleTouch(xy.x, xy.y, false);
});
let isRunning = require("sched").getAlarm("ateatimer");
if (isRunning) {
timerRunning = true;
// Start the timer to update the display
setInterval(updateDisplay, 1000);
} else {
// Draw the initial timer display
drawTime();
}

1
apps/ateatimer/app.json Normal file
View File

@ -0,0 +1 @@
{ "duration": 240 }

BIN
apps/ateatimer/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,14 @@
{ "id": "ateatimer",
"name": "A Tea Timer",
"shortName":"A Tea Timer",
"icon": "app.png",
"version":"0.02",
"description": "Simple app for setting timers for tea. Touch up and down to change time, and time or button to start counting. When timer is running, button will stop timer and reset counter to last used value.",
"tags": "timer",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"ateatimer.app.js","url":"app.js"},
{"name":"ateatimer.img","url":"app-icon.js","evaluate":true}
],
"dependencies": {"scheduler":"type"}
}

View File

@ -1,6 +1,5 @@
0.01: New App! 0.01: New App!
0.02: Don't fire if the app uses swipes already. 0.02: Don't fire if the app uses swipes already.
0.03: Only count defined handlers in the handler array. 0.03: Only count defined handlers in the handler array.
0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs 0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs a refresh by deselecting and reselecting the "Messages" app throught Back Swipe settings.
a refresh by deselecting and reselecting the "Messages" app throught Back Swipe 0.05: React on swipes before the active app (for the most part) by using `prependListener`.
settings.

View File

@ -38,6 +38,7 @@
// if we're in an app that has a back button, run the callback for it // if we're in an app that has a back button, run the callback for it
if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) { if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) {
global.BACK(); global.BACK();
E.stopEventPropagation();
} }
} }
} }
@ -56,5 +57,5 @@
} }
// Listen to left to right swipe // Listen to left to right swipe
Bangle.on("swipe", goBack); Bangle.prependListener("swipe", goBack);
})(); })();

View File

@ -1,7 +1,7 @@
{ "id": "backswipe", { "id": "backswipe",
"name": "Back Swipe", "name": "Back Swipe",
"shortName":"BackSwipe", "shortName":"BackSwipe",
"version":"0.04", "version":"0.05",
"description": "Service that allows you to use an app's back button using left to right swipe gesture", "description": "Service that allows you to use an app's back button using left to right swipe gesture",
"icon": "app.png", "icon": "app.png",
"tags": "back,gesture,swipe", "tags": "back,gesture,swipe",

View File

@ -1,2 +1,3 @@
0.01: New app! 0.01: New app!
0.02: Minor code improvements 0.02: Minor code improvements
0.03: Remove clearing of the screen (will break running apps) and fix lint errors

View File

@ -2,8 +2,8 @@
"id": "banglebridge", "id": "banglebridge",
"name": "BangleBridge", "name": "BangleBridge",
"shortName": "BangleBridge", "shortName": "BangleBridge",
"version": "0.02", "version": "0.03",
"description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App", "description": "Widget that allows Bangle.js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App (**Note:** this has nothing to do with Gadgetbridge)",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",
"tags": "widget", "tags": "widget",

View File

@ -1,10 +1,10 @@
(() => { (() => {
/** /**
* Widget measurements * Widget measurements
* Description: * Description:
* name: connection.wid.js * name: connection.wid.js
*icon: conectionIcon.icon *icon: conectionIcon.icon
* *
*/ */
//Font //Font
@ -24,7 +24,7 @@
//Sensors code //Sensors code
/** /**
* *
* @author Jorge * @author Jorge
*/ */
function accel() { function accel() {
@ -35,8 +35,7 @@
}); });
setInterval(function () { setInterval(function () {
//acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag;
acclS = accelN.x + "##" + accelN.y + "##" + accelN.z + "\n" + accelN.diff + "##" + accelN.mag;
data[3] = accelN; data[3] = accelN;
}, 2 * 1000); }, 2 * 1000);
@ -45,8 +44,7 @@
function btt() { function btt() {
setInterval(function () { setInterval(function () {
//bttS = E.getBattery(); //return String
bttS = E.getBattery(); //return String
data[2] = E.getBattery(); data[2] = E.getBattery();
}, 15 * 1000); }, 15 * 1000);
@ -65,9 +63,9 @@
setInterval(function () { setInterval(function () {
compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" + /*compssS = "A: " + compssN.x + " ## " + compssN.y + " ## " + compssN.z + "\n" +
"B: " + compssN.dx + " ## " + compssN.dy + " ## " + compssN.dz + " ## " + "\n" + "B: " + compssN.dx + " ## " + compssN.dy + " ## " + compssN.dz + " ## " + "\n" +
"C: " + compssN.heading; //return String "C: " + compssN.heading; *///return String
data[4] = compssN; data[4] = compssN;
}, 2 * 1000); }, 2 * 1000);
@ -86,8 +84,8 @@
setInterval(function () { setInterval(function () {
gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" + /*gpsS = "A: " + gpsN.lat + " ## " + gpsN.lon + " ## " + gpsN.alt + "\n" + "B: " + gpsN.speed + " ## " + gpsN.course + " ## " + gpsN.time + "\n" +
"C: " + gpsN.satellites + " ## " + gpsN.fix; //return String "C: " + gpsN.satellites + " ## " + gpsN.fix; *///return String
// work out how to display the current time // work out how to display the current time
var d = new Date(); var d = new Date();
var year = d.getFullYear(); var year = d.getFullYear();
@ -150,7 +148,7 @@
//console.log("Index ==> "+ index); //console.log("Index ==> "+ index);
msr[indexFinal] = nueva; msr[indexFinal] = nueva;
item = nueva; //item = nueva;
lastInsert = indexFinal; lastInsert = indexFinal;
} }
@ -180,7 +178,7 @@
hrmN = normalize(hrmN); hrmN = normalize(hrmN);
var roundedRate = parseFloat(hrmN).toFixed(2); var roundedRate = parseFloat(hrmN).toFixed(2);
hrmS = String.valueOf(roundedRate); //return String //hrmS = String.valueOf(roundedRate); //return String
//console.log("array----->" + msr); //console.log("array----->" + msr);
data[0] = roundedRate; data[0] = roundedRate;
@ -205,7 +203,7 @@
setInterval(function () { setInterval(function () {
stepS = String.valueOf(stepN); //return String //stepS = String.valueOf(stepN); //return String
data[1] = stepN; data[1] = stepN;
}, 2 * 1000); }, 2 * 1000);
@ -240,12 +238,11 @@
g.setFont("Vector", 45); g.setFont("Vector", 45);
g.drawString(prueba,100,200);*/ g.drawString(prueba,100,200);*/
if (flip == 1) { //when off if (flip == 1) { //when off
flip = 0; flip = 0;
//Bangle.buzz(1000); //Bangle.buzz(1000);
g.clear();
} else { //when on } else { //when on
flip = 1; flip = 1;
g.setFont("Vector", 30); g.setFont("Vector", 30);
g.drawString(data[0], 65, 180); g.drawString(data[0], 65, 180);
@ -283,7 +280,7 @@
com: data[4], com: data[4],
gps: data[5] gps: data[5]
}; };
/* g.clear(); /*
g.drawString(compssS,100,200); g.drawString(compssS,100,200);
*/ */
@ -293,7 +290,7 @@
//draw(); //draw();
}, 5 * 1000); }, 5 * 1000);
WIDGETS["banglebridge"]={ WIDGETS["banglebridge"]={
area: "tl", area: "tl",
width: 10, width: 10,

View File

@ -17,4 +17,4 @@
} }
} }
}); });
}); })

View File

@ -32,4 +32,4 @@
} }
require("ClockFace_menu").addItems(menu, save, items); require("ClockFace_menu").addItems(menu, save, items);
E.showMenu(menu); E.showMenu(menu);
}); })

View File

@ -0,0 +1 @@
0.01: New app introduced to the app loader!

View File

@ -0,0 +1,30 @@
# Battery Booster
A Bangle.js app designed to optimize battery life through smart screen and power management features.
## Features
### 1. Auto Soft-Off
- Automatically puts the watch into soft-off mode after 3 hours (10,800,000 ms) of being locked
- This feature is activated when the watch is locked and cancelled when unlocked
### 2. Dynamic Screen Timeout
- Sets LCD timeout to 2 seconds when the watch is locked
- Extends LCD timeout to 10 seconds when the screen is touched
- Helps preserve battery life while maintaining usability
### 3. Adaptive Brightness Control
- Automatically adjusts screen brightness based on the time of day
- Uses a sinusoidal pattern that follows natural daylight:
- Peak brightness at noon
- Lowest brightness at midnight
- Gradual transitions in between
- Updates brightness every hour
## How It Works
The app runs in the background and manages three main aspects of power consumption:
1. Screen timeout duration
2. Automatic soft-off functionality
3. Time-based brightness adjustment
This combination of features helps extend battery life while maintaining a good user experience.

BIN
apps/batterybooster/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

View File

@ -0,0 +1,26 @@
{
let softOffTimeout;
Bangle.on("lock", (on) => {
if (on) {
softOffTimeout = setTimeout(() => Bangle.softOff(), 10800000);
Bangle.setLCDTimeout(2);
}
else {
if (softOffTimeout) clearTimeout(softOffTimeout);
}
});
Bangle.on("touch", () => {
Bangle.setLCDTimeout(10);
});
setInterval(() => {
let getBrightness = (hour) => {
let radians = (Math.PI / 12) * (hour - 6);
let brightness = Math.sin(radians) / 2 + 0.5;
return brightness;
};
const d = new Date();
let hour = d.getHours();
Bangle.setLCDBrightness(getBrightness(hour));
}, 3600000);
}

View File

@ -0,0 +1,19 @@
{
"id": "batterybooster",
"name": "Battery Booster",
"icon": "app.png",
"version": "0.01",
"description": "A bootloader app which adds scripts to boost battery life of your Bangle.js 2",
"type": "bootloader",
"tags": "tools,system",
"readme": "README.md",
"supports": [
"BANGLEJS2"
],
"storage": [
{
"name": "batterybooster.boot.js",
"url": "boot.js"
}
]
}

View File

@ -6,7 +6,7 @@
"description": "A fully featured watch face with a playable game on the side.", "description": "A fully featured watch face with a playable game on the side.",
"readme":"README.md", "readme":"README.md",
"type": "clock", "type": "clock",
"tags": "clock, game", "tags": "clock,game",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],
"storage": [ "storage": [
{"name":"bblobface.app.js","url":"app.js"}, {"name":"bblobface.app.js","url":"app.js"},

View File

@ -5,7 +5,7 @@
"version": "0.01", "version": "0.01",
"description": "Aerobic fitness test created by Léger & Lambert", "description": "Aerobic fitness test created by Léger & Lambert",
"icon": "beeptest.png", "icon": "beeptest.png",
"tags": "Health", "tags": "health",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [

View File

@ -23,4 +23,4 @@
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
}); })

View File

@ -68,4 +68,4 @@
E.showMenu(appMenu); E.showMenu(appMenu);
}); })

View File

@ -1 +1,4 @@
0.10: New app introduced to the app loader! 0.10: New app introduced to the app loader!
0.20: skipped (internal revision)
0.30: skipped (internal revision)
0.40: added functionality for customizing colors

View File

@ -9,5 +9,9 @@ From top to bottom the watch face shows four rows of leds:
* day (yellow leds, top row) * day (yellow leds, top row)
* month (yellow leds, bottom row) * month (yellow leds, bottom row)
As usual, luminous leds represent a logical one, dark leds a logcal '0'. The colors are default colors and can be changed at the settings page, also, the outer ring can be disabled.
The rightmost LED represents 1, the second 2, the third 4, the next 8 and so on, i.e. values are the powers of two.
As usual, luminous leds represent a logical one, and "dark" leds a logcal '0'. Dark means the color of the background.
Widgets aren't affected and are shown as normal. Widgets aren't affected and are shown as normal.

View File

@ -2,135 +2,181 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears { // must be inside our own scope here so that when we are unloaded everything disappears
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
let drawTimeout;
// Actually draw the watch face
let draw = function()
{
// Bangle.js2 -> 176x176
var x_rgt = g.getWidth();
var y_bot = g.getHeight();
//var x_cntr = x_rgt / 2;
var y_cntr = y_bot / 18*7; // not to high because of widget-field (1/3 is to high)
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
let white = [1,1,1];
let red = [1,0,0];
let green = [0,1,0];
//let blue = [0,0,1];
let yellow = [1,1,0];
//let magenta = [1,0,1];
//let cyan = [0,1,1];
let black = [0,0,0];
let bord_col = white;
let col_off = black;
var col = new Array(red, green, yellow, yellow); // [R,G,B]
let pot_2 = [1, 2, 4, 8, 16, 32]; // array with powers of two, because power-op (**)
// doesn't work -> maybe also faster
const SETTINGSFILE = "BinaryClk.settings.json";
var nr_lines = 4; // 4 rows: hour (hr), minute (min), day (day), month (mon)
// Arrays: [hr, min, day, mon]
//No of Bits: 5 6 5 4
let msbits = [4, 5, 4, 3]; // MSB = No bits - 1
let rad = [12, 12, 8, 8]; // radiuses for each row
var x_dist = 28;
let y_dist = [0, 30, 60, 85]; // y-position from y_centr for each row from top
// don't calc. automatic as for x, because of different spaces
var x_offs_rgt = 16; // distance from right border (layout)
// Date-Time-Array: 4x6 Bit // variables defined from settings
//var idx_hr = 0; let HourCol;
//var idx_min = 1; let MinCol;
//var idx_day = 2; let DayCol;
//var idx_mon = 3; let MonCol;
var dt_bit_arr = [[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0]]; let RingOn;
var date_time = new Date(); // color arrays
var hr = date_time.getHours(); // 0..23 // !!! don't change order unless change oder in BinaryClk.settings.js !!!
var min = date_time.getMinutes(); // 0..59 // !!! order must correspond to each other between arrays !!!
var day = date_time.getDate(); // 1..31 let LED_Colors = ["#FFF", "#F00", "#0F0", "#00F", "#FF0", "#F0F", "#0FF", "#000"];
var mon = date_time.getMonth() + 1; // GetMonth() -> 0..11 let LED_ColorNames = ["white", "red", "green", "blue", "yellow", "magenta", "cyan", "black"];
let dt_array = [hr, min, day, mon]; // load settings
let loadSettings = function()
////////////////////////////////////////
// compute bit-pattern from time/date and draw leds
////////////////////////////////////////
var line_cnt = 0;
var cnt = 0;
var bit_cnt = 0;
while (line_cnt < nr_lines)
{
////////////////////////////////////////
// compute bit-pattern
bit_cnt = msbits[line_cnt];
while (bit_cnt >= 0)
{
if (dt_array[line_cnt] >= pot_2[bit_cnt])
{
dt_array[line_cnt] -= pot_2[bit_cnt];
dt_bit_arr[line_cnt][bit_cnt] = 1;
}
else
{
dt_bit_arr[line_cnt][bit_cnt] = 0;
}
bit_cnt--;
}
////////////////////////////////////////
// draw leds (first white border for black screen, then led itself)
cnt = 0;
while (cnt <= msbits[line_cnt])
{
g.setColor(bord_col[0], bord_col[1], bord_col[2]);
g.drawCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-20+y_dist[line_cnt], rad[line_cnt]);
if (dt_bit_arr[line_cnt][cnt] == 1)
{
g.setColor(col[line_cnt][0], col[line_cnt][1], col[line_cnt][2]);
}
else
{
g.setColor(col_off[0], col_off[1], col_off[2]);
}
g.fillCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-20+y_dist[line_cnt], rad[line_cnt]-1);
cnt++;
}
line_cnt++;
}
// queue next draw
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function()
{ {
drawTimeout = undefined; function def (value, def) {return value !== undefined ? value : def;}
draw();
}, 60000 - (Date.now() % 60000));
};
// Show launcher when middle button pressed var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
Bangle.setUI( // get name from setting, find index of name and assign corresponding color code by index
{ HourCol = LED_Colors[LED_ColorNames.indexOf(def(settings.HourCol, "red"))];
mode : "clock", MinCol = LED_Colors[LED_ColorNames.indexOf(def(settings.MinCol, "green"))];
remove : function() DayCol = LED_Colors[LED_ColorNames.indexOf(def(settings.DayCol, "yellow"))];
{ MonCol = LED_Colors[LED_ColorNames.indexOf(def(settings.MonCol, "yellow"))];
// Called to unload all of the clock app RingOn = def(settings.RingOn, true);
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; delete settings; // settings in local var -> no more required
} }
});
// Load widgets let drawTimeout;
Bangle.loadWidgets();
draw(); // actually draw the watch face
setTimeout(Bangle.drawWidgets,0); let draw = function()
{
// Bangle.js2 -> 176x176
var x_rgt = g.getWidth();
var y_bot = g.getHeight();
//var x_cntr = x_rgt / 2;
var y_cntr = y_bot / 18*7;
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
var white = "#FFF";
var black = "#000";
var bord_col = white;
var col_off = black;
var col = new Array(HourCol, MinCol, DayCol, MonCol); // each #rgb
if (g.theme.dark)
{
bord_col = white;
col_off = black;
}
else
{
bord_col = black;
col_off = white;
}
let pwr2 = [1, 2, 4, 8, 16, 32]; // array with powers of 2, because poweroperator '**' doesnt work
// maybe also faster
var no_lines = 4; // 4 rows: hour (hr), minute (min), day (day), month (mon)
var no_hour = 5;
var no_min = 6;
var no_day = 5;
var no_mon = 4;
// arrays: [hr, min, day, mon]
let msbits = [no_hour-1, no_min-1, no_day-1, no_mon-1]; // MSB = No bits - 1
let rad = [13, 13, 9, 9]; // radiuses for each row
var x_dist = 29;
let y_dist = [0, 35, 75, 100]; // y-position from y_centr for each row from top
// don't calc. automatic as for x, because of different spaces
var x_offs_rgt = 15; // offset from right border (layout)
var y_offs_cntr = 25; // vertical offset from center
////////////////////////////////////////
// compute bit-pattern from time/date and draw leds
////////////////////////////////////////
// date-time-array: 4x6 bit
//var idx_hour = 0;
//var idx_min = 1;
//var idx_day = 2;
//var idx_mon = 3;
var dt_bit_arr = [[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0],[0, 0, 0, 0, 0, 0]];
var date_time = new Date();
var hour = date_time.getHours(); // 0..23
var min = date_time.getMinutes(); // 0..59
var day = date_time.getDate(); // 1..31
var mon = date_time.getMonth() + 1; // GetMonth() -> 0..11
let dt_array = [hour, min, day, mon];
var line_cnt = 0;
var cnt = 0;
var bit_cnt = 0;
while (line_cnt < no_lines)
{
////////////////////////////////////////
// compute bit-pattern
bit_cnt = msbits[line_cnt];
while (bit_cnt >= 0)
{
if (dt_array[line_cnt] >= pwr2[bit_cnt])
{
dt_array[line_cnt] -= pwr2[bit_cnt];
dt_bit_arr[line_cnt][bit_cnt] = 1;
}
else
{
dt_bit_arr[line_cnt][bit_cnt] = 0;
}
bit_cnt--;
}
////////////////////////////////////////
// draw leds (and border, if enabled)
cnt = 0;
while (cnt <= msbits[line_cnt])
{
if (RingOn) // draw outer ring, if enabled
{
g.setColor(bord_col);
g.drawCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-y_offs_cntr+y_dist[line_cnt], rad[line_cnt]);
}
if (dt_bit_arr[line_cnt][cnt] == 1)
{
g.setColor(col[line_cnt]);
}
else
{
g.setColor(col_off);
}
g.fillCircle(x_rgt-x_offs_rgt-cnt*x_dist, y_cntr-y_offs_cntr+y_dist[line_cnt], rad[line_cnt]-1);
cnt++;
}
line_cnt++;
}
// queue next draw
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function()
{
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
// Init the settings of the app
loadSettings();
// Show launcher when middle button pressed
Bangle.setUI(
{
mode : "clock",
remove : function()
{
// Called to unload all of the clock app
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
// Load widgets
Bangle.loadWidgets();
draw();
setTimeout(Bangle.drawWidgets,0);
} }

72
apps/blc/blc.settings.js Normal file
View File

@ -0,0 +1,72 @@
// Change settings for BinaryClk
(function(back){
// color array -- don't change order unless change oder in BinaryClk.js
let LED_ColorNames = ["white", "red", "green", "blue", "yellow", "magenta", "cyan", "black"];
var FILE = "BinaryClk.settings.json";
// Load settings
var settings = Object.assign({
HourCol: "red",
MinCol: "green",
DayCol: "yellow",
MonCol: "yellow",
RingOn: true,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings(){
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values
function stringItems(startvalue, writer, values) {
return{
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => values[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
onchange: v => {
writer(values[v]);
writeSettings();
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values) {
return stringItems(settings[name], v => settings[name] = v, values);
}
// Show the menu
var mainmenu = {
"" : {
"title" : "BinaryCLK"
},
"< Back" : () => back(),
'Color Hour.:': stringInSettings("HourCol", LED_ColorNames),
'Color Minute:': stringInSettings("MinCol", LED_ColorNames),
'Color Day': stringInSettings("DayCol", LED_ColorNames),
'Color Month:': stringInSettings("MonCol", LED_ColorNames),
'LED ring on/off': {
value: (settings.RingOn !== undefined ? settings.RingOn : true),
onchange: v => {
settings.RingOn = v;
writeSettings();
}
},
};
// Show submenues
//var submenu1 = {
//"": {
// "title": "Show sub1..."
//},
//"< Back": () => E.showMenu(mainmenu),
//"ItemName": stringInSettings("settingsVar", ["Yes", "No", "DontCare"]),
//};
E.showMenu(mainmenu);
})

View File

@ -0,0 +1,7 @@
{
"HourCol": "red",
"MinCol": "green",
"DayCol": "yellow",
"MonCol": "yellow",
"RingOn": true
}

View File

@ -1,10 +1,10 @@
{ {
"id":"blc", "id":"blc",
"name":"Binary LED Clock", "name":"Binary LED Clock",
"version": "0.10", "version": "0.40",
"description": "Binary LED Clock with date", "description": "a binary LED-Clock with time and date and customizable LED-colors",
"icon":"blc-icon.png", "icon":"blc-icon.png",
"screenshots": [{"url":"screenshot_blc.bmp"}], "screenshots": [{"url":"screenshot_blc_1.bmp"},{"url":"screenshot_blc_2.bmp"}],
"type": "clock", "type": "clock",
"tags": "clock", "tags": "clock",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
@ -12,6 +12,8 @@
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"blc.app.js","url":"blc.js"}, {"name":"blc.app.js","url":"blc.js"},
{"name":"blc.settings.js","url":"blc.settings.js"},
{"name":"blc.img","url":"blc-icon.js","evaluate":true} {"name":"blc.img","url":"blc-icon.js","evaluate":true}
] ],
"data": [{"name":"blc.settings.json"}]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -70,5 +70,3 @@
}; };
return ci; return ci;
}) })

View File

@ -72,3 +72,7 @@
0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined') 0.61: Instead of breaking execution with an Exception when updating boot, just use if..else (fix 'Uncaught undefined')
0.62: Handle setting for configuring BLE privacy 0.62: Handle setting for configuring BLE privacy
0.63: Only set BLE `display:1` if we have a passkey 0.63: Only set BLE `display:1` if we have a passkey
0.64: Automatically create .widcache and .clkinfocache to speed up loads
Bangle.loadWidgets overwritten with fast version on success
Refuse to work on firmware <2v16 and remove old polyfills
0.65: Only display interpreter errors if log is nonzero

View File

@ -12,14 +12,13 @@ if (DEBUG) {
boot += "var _tm=Date.now()\n"; boot += "var _tm=Date.now()\n";
bootPost += "delete _tm;"; bootPost += "delete _tm;";
} }
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed if (FWVERSION < 216) {
let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); E.showMessage(/*LANG*/"Please update Bangle.js firmware\n\nCurrent = "+process.env.VERSION,{title:"ERROR"});
boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; throw new Error("Old firmware");
} else {
let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
boot += `if(E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} }
boot += `{eval(require('Storage').read('bootupdate.js'));print("Storage Updated!")}else{\n`; let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.js$/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if(E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.js$/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
boot += `{eval(require('Storage').read('bootupdate.js'));}else{\n`;
boot += `E.setFlags({pretokenise:1});\n`; boot += `E.setFlags({pretokenise:1});\n`;
boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`;
bootPost += `NRF.setServices(bleServices,bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code bootPost += `NRF.setServices(bleServices,bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code
@ -44,7 +43,7 @@ LoopbackA.setConsole(true);\n`;
boot += ` boot += `
Bluetooth.line=""; Bluetooth.line="";
Bluetooth.on('data',function(d) { Bluetooth.on('data',function(d) {
var l = (Bluetooth.line + d).split(/[\\n\\r]/); let l = (Bluetooth.line + d).split(/[\\n\\r]/);
Bluetooth.line = l.pop(); Bluetooth.line = l.pop();
l.forEach(n=>Bluetooth.emit("line",n)); l.forEach(n=>Bluetooth.emit("line",n));
}); });
@ -67,12 +66,12 @@ if (s.ble===false) boot += `if (!NRF.getSecurityStatus().connected) NRF.sleep();
if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`; if (s.timeout!==undefined) boot += `Bangle.setLCDTimeout(${s.timeout});\n`;
if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`; if (!s.timeout) boot += `Bangle.setLCDPower(1);\n`;
boot += `E.setTimeZone(${s.timezone});`; boot += `E.setTimeZone(${s.timezone});`;
// Draw out of memory errors onto the screen // Draw out of memory errors onto the screen if logging enabled
boot += `E.on('errorFlag', function(errorFlags) { if (s.log) boot += `E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip(); g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:", errorFlags); print("Interpreter error:", errorFlags);
E.getErrorFlags(); // clear flags so we get called next time E.getErrorFlags();
});\n`; });\n`;// E.getErrorFlags() -> clear flags so we get called next time
// stop users doing bad things! // stop users doing bad things!
if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`; if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`;
// Apply any settings-specific stuff // Apply any settings-specific stuff
@ -86,30 +85,18 @@ if (s.bleprivacy || (s.passkey!==undefined && s.passkey.length==6)) {
if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`; if (s.blename === false) boot+=`NRF.setAdvertising({},{showName:false});\n`;
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`; if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
boot+=`Bangle.loadWidgets=function(){if(!global.WIDGETS)eval(require("Storage").read(".widcache"))};\n`;
// ================================================== FIXING OLDER FIRMWARES // ================================================== FIXING OLDER FIRMWARES
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
boot += `Bangle.on('mag',e=>{if(!isNaN(e.heading))e.heading=360-e.heading;});
Bangle.getCompass=(c=>(()=>{e=c();if(!isNaN(e.heading))e.heading=360-e.heading;return e;}))(Bangle.getCompass);`;
// deleting stops us getting confused by our own decl. builtins can't be deleted // deleting stops us getting confused by our own decl. builtins can't be deleted
// this is a polyfill without fastloading capability // this is a polyfill without fastloading capability
delete Bangle.showClock; delete Bangle.showClock;
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`; if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
delete Bangle.load; delete Bangle.load;
if (!Bangle.load) boot += `Bangle.load = load;\n`; if (!Bangle.load) boot += `Bangle.load = load;\n`;
let date = new Date();
delete date.toLocalISOString; // toLocalISOString was only introduced in 2v15
if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function() {
var o = this.getTimezoneOffset();
var d = new Date(this.getTime() - o*60000);
var sign = o>0?"-":"+";
o = Math.abs(o);
return d.toISOString().slice(0,-1)+sign+Math.floor(o/60).toString().padStart(2,0)+(o%60).toString().padStart(2,0);
};\n`;
// show timings // show timings
if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n` if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
// ================================================== BOOT.JS // ================================================== .BOOT0
// Append *.boot.js files. // Append *.boot.js files.
// Name files with a number - eg 'foo.5.boot.js' to enforce order (lowest first). Numbered files get placed before non-numbered // Name files with a number - eg 'foo.5.boot.js' to enforce order (lowest first). Numbered files get placed before non-numbered
// These could change bleServices/bleServiceOptions if needed // These could change bleServices/bleServiceOptions if needed
@ -128,17 +115,47 @@ let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
}); });
// precalculate file size // precalculate file size
bootPost += "}"; bootPost += "}";
let fileSize = boot.length + bootPost.length; let fileOffset,fileSize;
bootFiles.forEach(bootFile=>{ /* code to output a file, plus preable and postable
// match the size of data we're adding below in bootFiles.forEach when called with dst==undefined it just increments
if (DEBUG) fileSize += 2+bootFile.length+1; // `//${bootFile}\n` comment fileOffset so we can see ho wbig the file has to be */
fileSize += require('Storage').read(bootFile).length+2; // boot code plus ";\n" let outputFile = (dst,src,pre,post) => {"ram";
if (DEBUG) fileSize += 48+E.toJS(bootFile).length; // `print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n` if (DEBUG) {
}); if (dst) require('Storage').write(dst,`//${src}\n`,fileOffset);
// write file in chunks (so as not to use up all RAM) fileOffset+=2+src.length+1;
require('Storage').write('.boot0',boot,0,fileSize); }
let fileOffset = boot.length; if (pre) {
bootFiles.forEach(bootFile=>{ if (dst) require('Storage').write(dst,pre,fileOffset);
fileOffset+=pre.length;
}
let f = require('Storage').read(src);
if (src.endsWith("clkinfo.js") && f[0]!="(") {
/* we shouldn't have to do this but it seems sometimes (sched 0.28) folks have
used libraries which get added into the clockinfo, and we can't use them directly
to we have to revert back to eval */
f = `eval(require('Storage').read(${E.toJS(src)}))`;
}
if (dst) {
// we can't just write 'f' in one go because it can be too big
let len = f.length;
let offset = 0;
while (len) {
let chunk = Math.min(len, 2048);
require('Storage').write(dst,f.substr(offset, chunk),fileOffset);
fileOffset+=chunk;
offset+=chunk;
len-=chunk;
}
} else
fileOffset+=f.length;
if (dst) require('Storage').write(dst,post,fileOffset);
fileOffset+=post.length;
if (DEBUG) {
if (dst) require('Storage').write(dst,`print(${E.toJS(src)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
fileOffset += 48+E.toJS(src).length;
}
};
let outputFileComplete = (dst,fn) => {
// we add a semicolon so if the file is wrapped in (function(){ ... }() // we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
// which would cause an error! // which would cause an error!
@ -146,31 +163,48 @@ bootFiles.forEach(bootFile=>{
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
// but we need to do this without ever loading everything into RAM as some // but we need to do this without ever loading everything into RAM as some
// boot files seem to be getting pretty big now. // boot files seem to be getting pretty big now.
if (DEBUG) { outputFile(dst,fn,"",";\n");
require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset); };
fileOffset+=2+bootFile.length+1; fileOffset = boot.length + bootPost.length;
} bootFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
let bf = require('Storage').read(bootFile); fileSize = fileOffset;
// we can't just write 'bf' in one go because at least in 2v13 and earlier require('Storage').write('.boot0',boot,0,fileSize);
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1 fileOffset = boot.length;
// it can be too big (especially BTHRM). bootFiles.forEach(fn=>outputFileComplete('.boot0',fn));
let bflen = bf.length;
let bfoffset = 0;
while (bflen) {
let bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk;
bfoffset+=bfchunk;
bflen-=bfchunk;
}
require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2;
if (DEBUG) {
require('Storage').write('.boot0',`print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
fileOffset += 48+E.toJS(bootFile).length
}
});
require('Storage').write('.boot0',bootPost,fileOffset); require('Storage').write('.boot0',bootPost,fileOffset);
delete boot,bootPost,bootFiles;
// ================================================== .WIDCACHE for widgets
let widgetFiles = require("Storage").list(/\.wid\.js$/);
let widget = `// Made by bootupdate.js\nglobal.WIDGETS={};`, widgetPost = `var W=WIDGETS;WIDGETS={};
Object.keys(W).sort((a,b)=>(0|W[b].sortorder)-(0|W[a].sortorder)).forEach(k=>WIDGETS[k]=W[k]);`; // sort
if (DEBUG) widget+="var _tm=Date.now();";
outputFileComplete = (dst,fn) => {
outputFile(dst,fn,"try{",`}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`);
};
fileOffset = widget.length + widgetPost.length;
widgetFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
fileSize = fileOffset;
require('Storage').write('.widcache',widget,0,fileSize);
fileOffset = widget.length;
widgetFiles.forEach(fn=>outputFileComplete('.widcache',fn));
require('Storage').write('.widcache',widgetPost,fileOffset);
delete widget,widgetPost,widgetFiles;
// ================================================== .clkinfocache for clockinfos
let ciFiles = require("Storage").list(/\.clkinfo\.js$/);
let ci = `// Made by bootupdate.js\n`;
if (DEBUG) ci+="var _tm=Date.now();";
outputFileComplete = (dst,fn) => {
outputFile(dst,fn,"try{let fn=",`;let a=fn(),b=menu.find(x=>x.name===a.name);if(b)b.items=b.items.concat(a.items)else menu=menu.concat(a);}catch(e){print(${E.toJS(fn)},e,e.stack)}\n`);
};
fileOffset = ci.length;
ciFiles.forEach(fn=>outputFileComplete(undefined,fn)); // just get sizes
fileSize = fileOffset;
require('Storage').write('.clkinfocache',ci,0,fileSize);
fileOffset = ci.length;
ciFiles.forEach(fn=>outputFileComplete('.clkinfocache',fn));
delete ci,ciFiles;
// test with require("clock_info").load()
// ================================================== END
E.showMessage(/*LANG*/"Reloading..."); E.showMessage(/*LANG*/"Reloading...");
} }
// .bootcde should be run automatically after if required, since // .bootcde should be run automatically after if required, since

View File

@ -1,7 +1,7 @@
{ {
"id": "boot", "id": "boot",
"name": "Bootloader", "name": "Bootloader",
"version": "0.63", "version": "0.65",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png", "icon": "bootloader.png",
"type": "bootloader", "type": "bootloader",
@ -11,6 +11,9 @@
{"name":".boot0","url":"boot0.js"}, {"name":".boot0","url":"boot0.js"},
{"name":".bootcde","url":"bootloader.js"}, {"name":".bootcde","url":"bootloader.js"},
{"name":"bootupdate.js","url":"bootupdate.js"} {"name":"bootupdate.js","url":"bootupdate.js"}
],"data": [
{"name":".widcache"},
{"name":".clkinfocache"}
], ],
"sortorder": -10 "sortorder": -10
} }

View File

@ -6,7 +6,7 @@
"description": "Bangle version of a popular word search game", "description": "Bangle version of a popular word search game",
"supports" : ["BANGLEJS2"], "supports" : ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"tags": "game, text", "tags": "game,text",
"storage": [ "storage": [
{"name":"bordle.app.js","url":"bordle.app.js"}, {"name":"bordle.app.js","url":"bordle.app.js"},
{"name":"wordlencr.txt","url":"wordlencr.txt"}, {"name":"wordlencr.txt","url":"wordlencr.txt"},

View File

@ -12,4 +12,5 @@
- [+] Fixed optional seconds not displaying in time - [+] Fixed optional seconds not displaying in time
- [+] Fixed drag handler by adding E.stopEventPropagation() - [+] Fixed drag handler by adding E.stopEventPropagation()
- [+] General code optimization and cleanup - [+] General code optimization and cleanup
0.09: Revised event handler code 0.09: Revised event handler code
0.10: Revised suffix formatting in getDate function

View File

@ -208,12 +208,23 @@
// 7. String forming helper functions // 7. String forming helper functions
let getDate = function(short, shortMonth, disableSuffix) { let getDate = function(short, shortMonth, disableSuffix) {
const date = new Date(); const date = new Date();
const dayOfMonth = date.getDate(); const day = date.getDate();
const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0);
const year = date.getFullYear(); const year = date.getFullYear();
let suffix = ["st", "nd", "rd"][(dayOfMonth - 1) % 10] || "th";
let dayOfMonthStr = disableSuffix ? dayOfMonth : `${dayOfMonth}${suffix}`; const getSuffix = (day) => {
return `${month} ${dayOfMonthStr}${short ? '' : `, ${year}`}`; if (day >= 11 && day <= 13) return 'th';
const lastDigit = day % 10;
switch (lastDigit) {
case 1: return 'st';
case 2: return 'nd';
case 3: return 'rd';
default: return 'th';
}
};
const dayStr = disableSuffix ? day : `${day}${getSuffix(day)}`;
return `${month} ${dayStr}${short ? '' : `, ${year}`}`; // not including year for short version
}; };
let getDayOfWeek = function(date, short) { let getDayOfWeek = function(date, short) {

View File

@ -11,14 +11,14 @@
"xOffset": 3, "xOffset": 3,
"yOffset": 0, "yOffset": 0,
"boxPos": { "boxPos": {
"x": "0.494", "x": "0.5",
"y": "0.739" "y": "0.739"
} }
}, },
"dow": { "dow": {
"font": "6x8", "font": "6x8",
"fontSize": 3, "fontSize": 3,
"outline": 1, "outline": 2,
"color": "bgH", "color": "bgH",
"outlineColor": "fg", "outlineColor": "fg",
"border": "#f0f", "border": "#f0f",
@ -27,7 +27,7 @@
"xOffset": 2, "xOffset": 2,
"yOffset": 0, "yOffset": 0,
"boxPos": { "boxPos": {
"x": "0.421", "x": "0.5",
"y": "0.201" "y": "0.201"
}, },
"short": false "short": false
@ -35,7 +35,7 @@
"date": { "date": {
"font": "6x8", "font": "6x8",
"fontSize": 2, "fontSize": 2,
"outline": 1, "outline": 2,
"color": "bgH", "color": "bgH",
"outlineColor": "fg", "outlineColor": "fg",
"border": "#f0f", "border": "#f0f",
@ -44,7 +44,7 @@
"xOffset": 1, "xOffset": 1,
"yOffset": 0, "yOffset": 0,
"boxPos": { "boxPos": {
"x": "0.454", "x": "0.5",
"y": "0.074" "y": "0.074"
}, },
"shortMonth": false, "shortMonth": false,
@ -62,7 +62,7 @@
"xOffset": 2, "xOffset": 2,
"yOffset": 1, "yOffset": 1,
"boxPos": { "boxPos": {
"x": "0.494", "x": "0.5",
"y": "0.926" "y": "0.926"
}, },
"prefix": "Steps: " "prefix": "Steps: "
@ -79,7 +79,7 @@
"xOffset": 2, "xOffset": 2,
"yOffset": 2, "yOffset": 2,
"boxPos": { "boxPos": {
"x": "0.805", "x": "0.8",
"y": "0.427" "y": "0.427"
}, },
"suffix": "%" "suffix": "%"

View File

@ -1,7 +1,7 @@
{ {
"id": "boxclk", "id": "boxclk",
"name": "Box Clock", "name": "Box Clock",
"version": "0.09", "version": "0.10",
"description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background",
"icon": "app.png", "icon": "app.png",
"dependencies" : { "clockbg":"module" }, "dependencies" : { "clockbg":"module" },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -91,4 +91,4 @@
}); });
E.showMenu(menu); E.showMenu(menu);
}); })

View File

@ -720,7 +720,7 @@ const hook = (enable: boolean) => {
// --- intervals --- // --- intervals ---
const setIntervals = ( const setIntervals = (
locked: boolean = Bangle.isLocked(), locked: ShortBoolean = Bangle.isLocked(),
connected: boolean = NRF.getSecurityStatus().connected, connected: boolean = NRF.getSecurityStatus().connected,
) => { ) => {
changeInterval( changeInterval(

View File

@ -14,4 +14,4 @@
} }
}) })
}; };
}) // must not have a semi-colon! })

View File

@ -371,4 +371,4 @@
}; };
E.showMenu(buildMainMenu()); E.showMenu(buildMainMenu());
}); })

3
apps/bttfclock/ChangeLog Normal file
View File

@ -0,0 +1,3 @@
0.01: Back to the future clock first version.
0.02: Added more icons to the status field. Made it posible to custemize the step goal thrue the Health Tracking app.
0.03: Added charging screen.

40
apps/bttfclock/README.md Normal file
View File

@ -0,0 +1,40 @@
# Back to the future Clock
![](screenshot.png)
A watchface inspierd by <a target="_blank" href="https://apps.garmin.com/apps/d181bcf9-5421-42a5-b460-863e5e76d798">this garmin watchface</a>.<br/>
## Todo
- Improving quality of Fonts.
- More status icons.
## Functionalities
- Current time
- Current day and month
- Battery
- Steps
- Step goal can be set white the <a target="_blank" href="https://github.com/espruino/BangleApps/tree/master/apps/health">Health Tracking</a> app defult is 10000
- Bluetooth connected icon
- Alarm icon
- Notification icon
## Screenshots
Clock:<br/>
![](screenshot.png)<br/>
Clock when charging:<br/>
![](screenshotCharging.png)<br/>
## Usage
Install it and enjoy
## Links
### code ispired by
advCasioBangleClock <a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock">https://github.com/dotgreg/advCasioBangleClock</a>
93dub <a target="_blank" href="https://github.com/espruino/BangleApps/tree/master/apps/93dub">https://github.com/espruino/BangleApps/tree/master/apps/93dub</a>
### Creator
<a target="_blank" href="https://github.com/NoobEjby">https://github.com/NoobEjby</a>

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwyEAhN0AMF1AIl2AKAXFH8Jbny4BKu5j/LZWWJ4W4AIXYhPZAJAPDzBnIMc3GjABFLZBZB25XDgP5gUaAIU7AJUbgP6AIRnB3JlEMYw/KLbAjIGIJbBWIJZDrcCvkCzkC3oBMzsCvsCrkCnhnDZomXH6BfU9HkAIIfGLoP4gU6LYRZB7sC/sDj0DnwBCrwBEBIYPBj0C7xnCMoZjD3I/QLqwhJLYpJCKIOege/gffAIX/AIwJBB4IBBv8Dn5lEZYNcH6ZfPDo4hHLYl+I4X/gkfgk/glfAJgPBAIMfM4WfgdfMYW+H6ZffW4W+LI1/gmfAKATBAIIZBMYLNBMYM/L8bfPToJbYMpZjD74/TL5ZhLBIv9vJdCLbpjJn4tBH58JupfTAJP+oJhCLsYBCFIItBH55fOBoIBBywbHF4YBDMMotJLZGYhN3L6GWhO3gUagWcgcdgeegkfGYvvmxhhEIIlBRY0fgefgcegV+gUcgO6L4V2MJhfD7ECrcC3sDr0D/8Er43FAIZhdEplfHIMDr8C70CnsB7bBCL5YJBAIO3hP5gV7gX9XocEv47JTIhdYcYaDIGoLBCgX+gV9gP8hO5YIRfLNoO4gUagWcb4MD78En4/LfYxdWDp0/HoMDn8Cz0CjkB3RfOy0J7ECncC3sDrzjBc4JDPUJBdMbYZ7Or49BgdfgW+gU9gPbhOYWYRfNrcC7pfPI4oBDMJoVVL6DBHA4OXhPZL4l+gkfgl/VKKrDJZLVDLqQBBv5fE70CrpfS/JfXJ4oBDJ4oNNL9C/FrwhBL6JTHL5ZdSAINfL4m+gU9L4WZL6E7gW9L4lfHKRVC982AIIFBA44jTL/ZhFL4pdWL4ffgc/gWeL4mYL5gLB3ECjcCzsDnwhBgk/HqzDID61/HIMD38Dj5fCjsB3cJvBfPgP7gV9gX+gYnBj4pCL+sfHoMC/8Cv0B/pfSy8B7UCrkC70DcYP/c4JfX/1BAIJfYHIQ9BgW/gVegP8gOaL5wBBy8J3MCjcCzsDjzBZL7i9EgcfgWegUdgPchOZhN3KohfFYIu4gP7gVdYInfgk/MKZfaFoM/GoMDn69E/sB3a9ML4+XgPagUcgV9gX+MK5ZB982AIJfSFIIxB/41BgX/gV+XocBzS9CL54BBCYOYgPbgU9cIQrBv5hTL6y7DGIRdCz8Cny9KL5bBFMIO5gP8MIyRB38Ej6XBMZZfSXIQlBRYK7ELoNeLomZXqDBHCoLBB3RhISoOfS4LFCMZBZB/1BAIJfILYa5DEoJdGgUdgPcgOaXqhhHPIJhB3ZhEv0CSoKZBYoJjDY4JlDr5fIBYS3ELYO/EIUfFIJdMIYJdTMIp5BDoN4MIkdGIQ1BMY4HBMoRfHK4QBBCIK3CLYm/RYU+gP9LoW6Loi9DLqZhL3QtBMIU+HIQ9BIIJNBn5LCr/GjABFBYQPBn4VBW4l/EoS5BLoO8LsRhLzTFCMYrHBMoOfLY4BHLIlfDoUeXIgrBzJdkMI5jDzDFCMYQ/BMoZVF9HkAIJfGLIf+LY2aLZBdhMIrFGhOZY4pdJMJMB7sB3gbC3UJvJduMI5jHvD7BLpZhHW4hbLLtJjPu5fTLIhb3MZxPDMJINFLIRb9MZhhJLoxZFLf4BIKooBJJ/5jbI/4B/AP4B/AP4BLA"))

308
apps/bttfclock/app.js Normal file
View File

@ -0,0 +1,308 @@
require("Font8x16").add(Graphics);
require("Font7x11Numeric7Seg").add(Graphics);
require("Font5x7Numeric7Seg").add(Graphics);
require("Font4x5").add(Graphics);
const timeTextY = 4;
const timeDataY = timeTextY+19;
const DateTextY = 48;
const DateDataY = DateTextY+19;
const stepGoalBatTextY = 100;
const stepGoalBatdataY = stepGoalBatTextY+19;
const statusTextY = 140;
const statusDataY = statusTextY+19;
let stepGoal = (require("Storage").readJSON("health.json",1)||10000).stepGoal;
let steps = 0;
let alarmStatus = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on);
let charching = Bangle.isCharging();
let chargeAniFrame= 0;
const chargeAni =[require("heatshrink").decompress(atob("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")),
require("heatshrink").decompress(atob("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")),
require("heatshrink").decompress(atob("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")),
require("heatshrink").decompress(atob("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")),
require("heatshrink").decompress(atob("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"))
];
const bluetoothOnIcon = require("heatshrink").decompress(atob("iEQwYROg3AAokYAgUMg0DAoUBwwFDgE2CIYdHAogREDoopFGoodGABI="));
const bluetoothOffIcon = require("heatshrink").decompress(atob("iEQwYLIgwFF4ADBgYFBjAKCsEGBAIABhgFEgOA7AdDmApKmwpCC4OGFIYjFGoVgIIkMEZAAD"));
const alarmIcon = require("heatshrink").decompress(atob("iEQyBC/AA3/8ABBB7INHA4YLLDqIHVApJRJCZodNCJ4dPHqqPJGp4RLOaozZT8btLF64hJFJpFbAEYA="));
const notificationIcon = require("heatshrink").decompress(atob("iEQyBC/AB3/8ABBD+4bHEa4VJD6YTNEKIf/D/rTDAJ7jTADo5hK+IA=="));
//the following 2 sections are used from waveclk to schedule minutely updates
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw(time) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
//draw;
//console.log(drawTimeout);
}, time - (Date.now() % time));
}
function getSteps() {
steps = Bangle.getHealthStatus("day").steps;
}
function drawBackground() {
//g.setBgColor(1,1,1);
g.setBgColor('#555555');
g.setColor(1,1,1);
g.clear();
//g.drawImage(imgBg,0,0);
g.reset();
}
function drawBlackBox() {
g.reset();
g.setBgColor(1,0,0);
g.setColor(0,0,0);
//Hour, Min and Sec
g.fillRect(50, timeDataY,50+33,timeDataY+22);
g.fillRect(90, timeDataY,90+33, timeDataY+22);
g.fillRect(128, timeDataY+8,130+24, timeDataY+8+14);
//Day, Month, Day and Year
g.fillRect(9, DateDataY,9+24, DateDataY+15);
g.fillRect(42, DateDataY,42+40, DateDataY+15);
g.fillRect(91, DateDataY,91+24, DateDataY+15);
g.fillRect(124, DateDataY,124+43, DateDataY+15);
//Present day
g.fillRect(60, 86,60+47, 86+7);
//Middle line
g.drawLine(0,95,176,95);
//Step and bat
g.fillRect(3, stepGoalBatdataY-1, 62, stepGoalBatdataY+15);
g.fillRect(121, stepGoalBatdataY-1, 150, stepGoalBatdataY+15);
//Status
g.fillRect(62, statusDataY-1, 62+49, statusDataY+15);
}
function draw(){
let time = 60000;
if(charching){
drawCharging();
time = 500;
}else{
drawWatchface();
}
//console.log(charching);
queueDraw(time);
}
function drawGoal() {
var goal = stepGoal <= steps;
g.reset();
g.setColor(0,0,0);
g.fillRect(84, stepGoalBatdataY-1, 92, stepGoalBatdataY+15);
if (goal){
g.reset();
g.setColor(0,1,0);
g.fillRect(84, stepGoalBatdataY, 92, stepGoalBatdataY+7);
} else {
g.reset();
g.setColor(1,0,0);
g.fillRect(84, stepGoalBatdataY+7, 92, stepGoalBatdataY+14);
}
}
function drawRedkBox() {
g.reset();
g.setBgColor(1,0,0);
g.setColor(1,0,0);
//Hour, Min and Sec
g.fillRect(50, timeTextY,50+33,timeTextY+15);
g.fillRect(90, timeTextY,90+33, timeTextY+15);
g.fillRect(128, timeTextY+8,130+24, timeTextY+8+15);
//Day, Month, Day and Year
g.fillRect(9, DateTextY,9+24, DateTextY+15);
g.fillRect(42, DateTextY,42+40, DateTextY+15);
g.fillRect(91, DateTextY,91+24, DateTextY+15);
g.fillRect(124, DateTextY,124+43, DateTextY+15);
//Step, Goal and Bat
g.fillRect(2, stepGoalBatTextY,2+61, stepGoalBatTextY+15);
g.fillRect(70, stepGoalBatTextY,72+33, stepGoalBatTextY+15);
g.fillRect(120, stepGoalBatTextY,120+31, stepGoalBatTextY+15);
//Status
g.fillRect(62, statusTextY,62+49, statusTextY+15);
}
function drawCharging(){
g.drawImage(chargeAni[chargeAniFrame], 0, 0);
chargeAniFrame+=1;
if(chargeAniFrame>=chargeAni.length){
chargeAniFrame=0;
}
var date = new Date();
var h = date.getHours(), m = date.getMinutes();
if (h<10) {
h = ("0"+h).substr(-2);
}
if (m<10) {
m = ("0"+m).substr(-2);
}
//time
g.reset();
g.setBgColor(0,0,0);
g.setColor(1,0,0);
g.setFont("7x11Numeric7Seg",3);
g.drawString(h, 40, 105);
g.drawString(m, 95, 105);
var bat = E.getBattery();
var batl = bat.toString().length-1;
var batDrawX = 80-(11*batl);
//80 69 58
g.drawString(bat, batDrawX, 20);
//g.drawLine(77,18,94,18);
}
function drawWatchface(){
drawBackground();
getSteps();
drawBlackBox();
drawRedkBox();
drawGoal();
var date = new Date();
var h = date.getHours(), m = date.getMinutes(), s = date.getSeconds();
var d = date.getDate(), y = date.getFullYear();//, w = date.getDay();
if (h<10) {
h = ("0"+h).substr(-2);
}
if (m<10) {
m = ("0"+m).substr(-2);
}
if (s<10) {
s = ("0"+s).substr(-2);
}
if (d<10) {
d = ("0"+d).substr(-2);
}
g.reset();
g.setBgColor(1,0,0);
g.setColor(1,1,1);
//Draw text
g.setFont("8x16");
g.drawString('HOUR', 51, timeTextY+1);
g.drawString('MIN', 96, timeTextY+1);
g.drawString('SEC', 130, timeTextY+9);
g.drawString('DAY', 10, DateTextY+1);
g.drawString('MONTH', 43, DateTextY+1);
g.drawString('DAY', 92, DateTextY+1);
g.drawString(' YEAR ', 125, DateTextY+1);
g.drawString('STEPS', 15, stepGoalBatTextY+1);
g.drawString('GOAL', 72, stepGoalBatTextY+1);
g.drawString(' BAT ', 120, stepGoalBatTextY+1);
g.drawString('STATUS', 64, statusTextY+1);
//time
g.reset();
g.setBgColor(0,0,0);
g.setColor(1,0,0);
g.setFont("5x7Numeric7Seg",2);
g.drawString(s, 131, timeDataY+8);
g.setFont("7x11Numeric7Seg",2);
g.drawString(h, 53, timeDataY);
g.drawString(m, 93, timeDataY);
//Date
g.reset();
g.setBgColor(0,0,0);
g.setColor(0,1,0);
g.setFont("5x7Numeric7Seg",2);
g.drawString(d, 13, DateDataY);
g.drawString(y, 127, DateDataY);
g.setFont("8x16");
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 52, DateDataY);
g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 92, DateDataY);
//status
g.reset();
g.setBgColor(0,0,0);
g.setColor(1,1,0);
g.setFont("5x7Numeric7Seg",2);
var step = steps;
var stepl = steps.toString().length;
var stepdDrawX = 4+(36-(stepl*6))+(4*(6-stepl));
g.drawString(step, stepdDrawX, stepGoalBatdataY);
var bat = E.getBattery();
var batl = bat.toString().length;
var batDrawX = 122+(18-(batl*6))+(4*(3-batl));
g.drawString(bat, batDrawX, stepGoalBatdataY);
//status
var b = bluetoothOffIcon;
if (NRF.getSecurityStatus().connected){
b = bluetoothOnIcon;
}
g.drawImage(b, 62, statusDataY-1);
if (alarmStatus){
g.drawImage(alarmIcon, 78, statusDataY-1);
}
if ((require('Storage').readJSON('messages.json',1)||[]).some(messag=>messag.new==true)){
g.drawImage(notificationIcon, 94, statusDataY-1);
}
g.reset();
g.setBgColor(0,0,0);
g.setColor(1,1,1);
g.setFont("4x5");
g.drawString('Present day', 62, 88);
}
/**
* This watch is mostly dark, it does not make sense to respect the
* light theme as you end up with a white strip at the top for the
* widgets and black watch. So set the colours to the dark theme.
*
*/
g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear();
//draw();
//the following section is from waveclk
Bangle.on('lcdPower',function(on) {
if (on) {
draw(); // draw immediately, queue redraw
console.log(drawTimeout);
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.on('charging', function(charging) {
Bangle.setLCDTimeout(!charging);
Bangle.setLCDPower(1);
charching = charging;
draw();
});
Bangle.setUI("clock");
// Load widgets, but don't show them
Bangle.loadWidgets();
require("widget_utils").swipeOn(); // hide widgets, make them visible with a swipe
g.clear(1);
//queueDraw();
draw();

BIN
apps/bttfclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,17 @@
{
"id": "bttfclock",
"name": "Back To The Future",
"version": "0.03",
"description": "The watch of Marty McFly",
"readme": "README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"bttfclock.app.js","url":"app.js"},
{"name":"bttfclock.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -32,4 +32,5 @@ clkinfo.addInteractive that would cause ReferenceError.
0.30: Use widget_utils 0.30: Use widget_utils
0.31: Use clock_info module as an app 0.31: Use clock_info module as an app
0.32: Make the border of the clock_info box extend all the way to the right of the screen. 0.32: Make the border of the clock_info box extend all the way to the right of the screen.
0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749) 0.33: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)
0.34: Support 12-hour time format

View File

@ -239,11 +239,9 @@ let drawTime = function() {
var y = y1; var y = y1;
var date = new Date(); var date = new Date();
var hours = String(date.getHours()); var timeStr = locale.time(date, 1);
var minutes = date.getMinutes(); if (settings.hideColon)
minutes = minutes < 10 ? String("0") + minutes : minutes; timeStr = timeStr.replace(":", "");
var colon = settings.hideColon ? "" : ":";
var timeStr = hours + colon + minutes;
// Set y coordinates correctly // Set y coordinates correctly
y += parseInt((H - y)/2) + 5; y += parseInt((H - y)/2) + 5;

View File

@ -1,7 +1,7 @@
{ {
"id": "bwclk", "id": "bwclk",
"name": "BW Clock", "name": "BW Clock",
"version": "0.33", "version": "0.34",
"description": "A very minimalistic clock.", "description": "A very minimalistic clock.",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -35,4 +35,5 @@ clkinfo.addInteractive that would cause ReferenceError.
Remove invertion of theme as this doesn'twork very well with fastloading. Remove invertion of theme as this doesn'twork very well with fastloading.
Do an quick inital fillRect on theclock info area. Do an quick inital fillRect on theclock info area.
0.33: Make the border of the clock_info box extend all the way to the right of the screen. 0.33: Make the border of the clock_info box extend all the way to the right of the screen.
0.34: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749) 0.34: Fix issue rendering ClockInfos with for fg+bg color set to the same (#2749)
0.35: Support 12-hour time format

View File

@ -199,11 +199,9 @@ let drawTime = function() {
let y = y1; let y = y1;
let date = new Date(); let date = new Date();
let hours = String(date.getHours()); var timeStr = locale.time(date, 1);
let minutes = date.getMinutes(); if (settings.hideColon)
minutes = minutes < 10 ? String("0") + minutes : minutes; timeStr = timeStr.replace(":", "");
let colon = settings.hideColon ? "" : ":";
let timeStr = hours + colon + minutes;
// Set y coordinates correctly // Set y coordinates correctly
y += parseInt((H - y)/2) + 5; y += parseInt((H - y)/2) + 5;

View File

@ -1,7 +1,7 @@
{ {
"id": "bwclklite", "id": "bwclklite",
"name": "BW Clock Lite", "name": "BW Clock Lite",
"version": "0.34", "version": "0.35",
"description": "A very minimalistic clock. This version of BW Clock is quicker at the cost of the custom font.", "description": "A very minimalistic clock. This version of BW Clock is quicker at the cost of the custom font.",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -6,7 +6,7 @@
"description": "Show the current and upcoming events synchronized from Gadgetbridge", "description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png", "icon": "calclock.png",
"type": "clock", "type": "clock",
"tags": "clock agenda", "tags": "clock,agenda",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [

View File

@ -5,3 +5,4 @@
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2) 0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
0.06: Bangle.js 2: Exit with a short press of the physical button 0.06: Bangle.js 2: Exit with a short press of the physical button
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen 0.07: Bangle.js 2: Exit by pressing upper left corner of the screen
0.08: truncate long numbers (and append '...' to displayed value)

View File

@ -10,9 +10,9 @@
g.clear(); g.clear();
require("Font7x11Numeric7Seg").add(Graphics); require("Font7x11Numeric7Seg").add(Graphics);
var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R'; var DEFAULT_SELECTION_NUMBERS = '5';
var RIGHT_MARGIN = 20;
var RESULT_HEIGHT = 40; var RESULT_HEIGHT = 40;
var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14);
var COLORS = { var COLORS = {
// [normal, selected] // [normal, selected]
DEFAULT: ['#7F8183', '#A6A6A7'], DEFAULT: ['#7F8183', '#A6A6A7'],
@ -88,28 +88,11 @@ function prepareScreen(screen, grid, defaultColor) {
} }
function drawKey(name, k, selected) { function drawKey(name, k, selected) {
var rMargin = 0;
var bMargin = 0;
var color = k.color || COLORS.DEFAULT; var color = k.color || COLORS.DEFAULT;
g.setColor(color[selected ? 1 : 0]); g.setColor(color[selected ? 1 : 0]);
g.setFont('Vector', 20).setFontAlign(0,0); g.setFont('Vector', 20).setFontAlign(0,0);
g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]);
g.setColor(-1); g.setColor(-1);
// correct margins to center the texts
if (name == '0') {
rMargin = (RIGHT_MARGIN * 2) - 7;
} else if (name === '/') {
rMargin = 5;
} else if (name === '*') {
bMargin = 5;
rMargin = 3;
} else if (name === '-') {
rMargin = 3;
} else if (name === 'R' || name === 'N') {
rMargin = k.val === 'C' ? 0 : -9;
} else if (name === '%') {
rMargin = -3;
}
g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2); g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2);
} }
@ -138,29 +121,21 @@ function drawGlobal() {
screen[k] = specials[k]; screen[k] = specials[k];
} }
drawKeys(); drawKeys();
var selected = DEFAULT_SELECTION_NUMBERS;
var prevSelected = DEFAULT_SELECTION_NUMBERS;
} }
function drawNumbers() { function drawNumbers() {
screen = numbers; screen = numbers;
screenColor = COLORS.DEFAULT; screenColor = COLORS.DEFAULT;
drawKeys(); drawKeys();
var selected = DEFAULT_SELECTION_NUMBERS;
var prevSelected = DEFAULT_SELECTION_NUMBERS;
} }
function drawOperators() { function drawOperators() {
screen = operators; screen = operators;
screenColor =COLORS.OPERATOR; screenColor =COLORS.OPERATOR;
drawKeys(); drawKeys();
var selected = DEFAULT_SELECTION_OPERATORS;
var prevSelected = DEFAULT_SELECTION_OPERATORS;
} }
function drawSpecials() { function drawSpecials() {
screen = specials; screen = specials;
screenColor = COLORS.SPECIAL; screenColor = COLORS.SPECIAL;
drawKeys(); drawKeys();
var selected = DEFAULT_SELECTION_SPECIALS;
var prevSelected = DEFAULT_SELECTION_SPECIALS;
} }
function getIntWithPrecision(x) { function getIntWithPrecision(x) {
@ -218,8 +193,6 @@ function doMath(x, y, operator) {
} }
function displayOutput(num) { function displayOutput(num) {
var len;
var minusMarge = 0;
g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1);
g.setColor(-1); g.setColor(-1);
if (num === Infinity || num === -Infinity || isNaN(num)) { if (num === Infinity || num === -Infinity || isNaN(num)) {
@ -230,9 +203,7 @@ function displayOutput(num) {
num = '-INFINITY'; num = '-INFINITY';
} else { } else {
num = 'NOT A NUMBER'; num = 'NOT A NUMBER';
minusMarge = -25;
} }
len = (num + '').length;
currNumber = null; currNumber = null;
results = null; results = null;
isDecimal = false; isDecimal = false;
@ -261,6 +232,9 @@ function displayOutput(num) {
num = num.toString(); num = num.toString();
num = num.replace("-","- "); // fix padding for '-' num = num.replace("-","- "); // fix padding for '-'
g.setFont('7x11Numeric7Seg', 2); g.setFont('7x11Numeric7Seg', 2);
if (num.length > RESULT_MAX_LEN) {
num = num.substr(0, RESULT_MAX_LEN - 1)+'...';
}
} }
g.setFontAlign(1,0); g.setFontAlign(1,0);
g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2);

View File

@ -2,7 +2,7 @@
"id": "calculator", "id": "calculator",
"name": "Calculator", "name": "Calculator",
"shortName": "Calculator", "shortName": "Calculator",
"version": "0.07", "version": "0.08",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png", "icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}], "screenshots": [{"url":"screenshot_calculator.png"}],

View File

@ -19,3 +19,5 @@
Display Widgets in menus Display Widgets in menus
0.17: Load holidays before events so the latter is not overpainted 0.17: Load holidays before events so the latter is not overpainted
0.18: Minor code improvements 0.18: Minor code improvements
0.19: Read events synchronized from Gadgetbridge
0.20: Correct start time of all-day events synchronized from Gadgetbridge

Some files were not shown because too many files have changed in this diff Show More