diff --git a/apps.json b/apps.json index 01fa1c5e2..4a594b996 100644 --- a/apps.json +++ b/apps.json @@ -3135,7 +3135,7 @@ { "id": "kitchen", "name": "Kitchen Combo", "icon": "kitchen.png", - "version":"0.10", + "version":"0.11", "description": "Combination of the Stepo, Walkersclock, Arrow and Waypointer apps into a multiclock format. 'Everything but the kitchen sink'. Requires firmware v2.08.167 or later", "tags": "tool,outdoors,gps", "type":"clock", @@ -3146,7 +3146,6 @@ {"name":"stepo.kit.js","url":"stepo.kit.js"}, {"name":"gps.kit.js","url":"gps.kit.js"}, {"name":"digi.kit.js","url":"digi.kit.js"}, - {"name":"heart.kit.js","url":"heart.kit.js"}, {"name":"swatch.kit.js","url":"swatch.kit.js"}, {"name":"compass.kit.js","url":"compass.kit.js"}, {"name":"kitchen.img","url":"kitchen.icon.js","evaluate":true} @@ -3273,5 +3272,20 @@ {"name":"mysticdock.settings.js","url":"mystic-dock-settings.js"}, {"name":"mysticdock.img","url":"mystic-dock-icon.js","evaluate":true} ] +}, +{ "id": "mysticclock", + "name": "Mystic Clock", + "icon": "mystic-clock.png", + "version":"1.00", + "description": "A retro-inspired watchface featuring time, date, and an interactive data display line.", + "tags": "clock", + "type":"clock", + "readme": "README.md", + "allow_emulator":true, + "storage": [ + {"name":"mysticclock.app.js","url":"mystic-clock-app.js"}, + {"name":"mysticclock.settings.js","url":"mystic-clock-settings.js"}, + {"name":"mysticclock.img","url":"mystic-clock-icon.js","evaluate":true} + ] } ] diff --git a/apps/arrow/README.md b/apps/arrow/README.md index 3b439711c..4b77dbc42 100644 --- a/apps/arrow/README.md +++ b/apps/arrow/README.md @@ -36,6 +36,10 @@ charge. *BTN3* - invokes calibration ( can be cancelled if pressed accidentally) +## Issues +* detect when calibration data is missing + ## Acknowledgement This app is based in the work done by [jeffmer](https://github.com/jeffmer/JeffsBangleAppsDev) + diff --git a/apps/kitchen/ChangeLog b/apps/kitchen/ChangeLog index 71548ec30..ea42a3f11 100644 --- a/apps/kitchen/ChangeLog +++ b/apps/kitchen/ChangeLog @@ -8,3 +8,4 @@ 0.08: Improved error handling for missing firmware features, added template app.kit.js 0.09: Added heart rate monitor app 0.10: Converted Stepo to use direct screen writes, added a Trip Counter feature to stepo +0.11: Detect when waypoints.json is not present, error E-WPT diff --git a/apps/kitchen/README.md b/apps/kitchen/README.md index a829a39b0..502ec1a7f 100644 --- a/apps/kitchen/README.md +++ b/apps/kitchen/README.md @@ -76,6 +76,9 @@ The following buttons depend on which face is currently in use ## Heart ![](screenshot_heart.jpg) - A simple heart rate monitor, at present the app is just showing the raw value from HRM.bpm +- This is an experimental app and not installed by default. The + heart.kit.js file can be uploaded via the Espruino IDE if you want + to try it out. Then reload the App. - BTN1, long press, turn heart rate monitor on / off ## Waypointer @@ -226,12 +229,12 @@ I have settled on directly writing to the screen using the Graphics object (g.) for the compass App. This creates a bit of flicker when the arrow moves but is more reliable than using the ArrayBuffer. -v0.09: Since adding the heart rate monitor I have noticed that I can -sometimes can a memory error when switch through the Apps back to the -Stepo App. I think this can be cured by statically allocating the -ArrayBuffer for stepo rather than using new everytime you switch back -into the stepo watch face. The problem is that the bangle memory -management / defragmentation is quite slow to run. +v0.09: Since adding the heart rate monitor I have sometimes observed +a low memory error when switching through the Apps back to the Stepo +App. I think this can be cured by statically allocating the +ArrayBuffer for stepo rather than using 'new' everytime you switch +back into the stepo watch face. The problem is that the bangle +memory management / defragmentation is quite slow to run. v0.10: Revisited having a display buffer for the stepo part of the App. Now use direct screen writing as it means less memory allocation and @@ -241,9 +244,13 @@ reduces chance of getting a memory error on switching watch faces. The following error codes will be displayed if one of the dependancies is not met. -* E-STEPS - no pedomintor widget has been installed, please install the widpedom or the activepedom widgets -* E-CALIB - no compass calibration data was found, see 'Compass Calibration' -* E-FW - require firmware 2v08.187 or later to detect gps and compass power status +* E-STEPS - no pedomintor widget has been installed, please install + the widpedom or the activepedom widgets +* E-CALIB - no compass calibration data was found, see 'Compass + Calibration' +* E-FW - require firmware 2v08.187 or later to detect gps and compass + power status +* E-WPT - missing waypoints.json file ### Issues / Future enhancements @@ -254,3 +261,5 @@ The following error codes will be displayed if one of the dependancies is not me seconds after the LCD goes off. At present I just rely on using the GPSSetup app and set the GPS power mode that I want. * Add a small graph to the heart rate monitor app +* Add a facility to call the Arrow calibration process +* Maybe create waypoints.json file if missing diff --git a/apps/kitchen/annex.js b/apps/kitchen/annex.js new file mode 100644 index 000000000..d789f5d0c --- /dev/null +++ b/apps/kitchen/annex.js @@ -0,0 +1,29 @@ +// annexed code that might be worth keeping + +/***************************************************************************** + +Screen Buffer Object that can be shared between faces + +Making into a Class like this means we allocate the memory once +and avoid fragmenting the memory when we switch in and out of faces + +******************************************************************************/ + +function BUF() { + this.pal4color = new Uint16Array([0x0000,0xFFFF,0x7BEF,0xAFE5],0,2); // b,w,grey,greenyellow + this.pal4red = new Uint16Array([0x0000,0xFFFF,0xF800,0xAFE5],0,2); // b,w,red,greenyellow + this.buf = Graphics.createArrayBuffer(120,120,2,{msb:true}); +} + +BUF.prototype.flip = function(x,y) { + g.drawImage({width:120,height:120,bpp:2, buffer:this.buf.buffer, palette:this.pal4color}, x, y); + this.buf.clear(); +} + +BUF.prototype.flip_red = function(x,y) { + g.drawImage({width:120,height:120,bpp:2, buffer:this.buf.buffer, palette:this.pal4red}, x, y); + this.buf.clear(); +} + +let bufObj = new BUF(); + diff --git a/apps/kitchen/compass.kit.js b/apps/kitchen/compass.kit.js index 530ba021c..b20cdce2c 100644 --- a/apps/kitchen/compass.kit.js +++ b/apps/kitchen/compass.kit.js @@ -17,13 +17,12 @@ } function init(gps,sw, hrm) { - showMem("compass init() START"); gpsObject = gps; intervalRefSec = undefined; bearing = 0; // always point north if GPS is off heading = 0; oldHeading = 0; - previous = {hding:"-", bs:"-", dst:"-", wp_name:"-", course:999}; + resetPrevious(); loc = require("locale"); CALIBDATA = require("Storage").readJSON("magnav.json",1)||null; getWaypoint(); @@ -34,12 +33,9 @@ */ if (!Bangle.isCompassOn()) Bangle.setCompassPower(1); gps.determineGPSState(); - - showMem("compass init() END"); } function freeResources() { - showMem("compass freeResources() START"); gpsObject = undefined; intervalRefSec = undefined; previous = undefined; @@ -50,7 +46,6 @@ CALIBDATA = undefined; wp = undefined; if (Bangle.isCompassOn !== undefined && Bangle.isCompassOn()) Bangle.setCompassPower(0); - showMem("compass freeResources() END"); } function startTimer() { @@ -67,12 +62,6 @@ if (Bangle.isCompassOn !== undefined && Bangle.isCompassOn()) Bangle.setCompassPower(0); } - function showMem(msg) { - var val = process.memory(); - var str = msg + " " + Math.round(val.usage*100/val.total) + "%"; - log_debug(str); - } - function onButtonShort(btn) { log_debug("onButtonShort()"); if (gpsObject.getState() !== gpsObject.GPS_RUNNING) return; @@ -206,12 +195,12 @@ drawCompass(dir, 0xFFC0); // yellow oldHeading = dir; } - + if (gpsObject.getState() === gpsObject.GPS_RUNNING) { drawGPSData(); } else { drawCompassHeading(); - } + } } // only used when acting as compass with GPS off diff --git a/apps/kitchen/kitchen.app.js b/apps/kitchen/kitchen.app.js index c3f7bd74d..fcaa048bb 100644 --- a/apps/kitchen/kitchen.app.js +++ b/apps/kitchen/kitchen.app.js @@ -33,10 +33,10 @@ function nextFace(){ // when you feel the buzzer you know you have done a long press function longPressCheck() { Bangle.buzz(); - debug_log("long PressCheck() buzz"); + debug_log("BUZZ, long press"); if (pressTimer) { clearInterval(pressTimer); - debug_log("clear pressTimer 2"); + debug_log("CLEAR pressTimer 2"); pressTimer = undefined; } } @@ -48,10 +48,10 @@ function buttonPressed(btn) { } else { firstPress = getTime(); if (pressTimer) { - debug_log("clear pressTimer 1"); + debug_log("CLEAR pressTimer 1"); clearInterval(pressTimer); } - debug_log("set pressTimer 1"); + debug_log("SET pressTimer 1"); pressTimer = setInterval(longPressCheck, 1500); } } @@ -60,7 +60,7 @@ function buttonPressed(btn) { function buttonReleased(btn) { var dur = getTime() - firstPress; if (pressTimer) { - debug_log("clear pressTimer 3"); + debug_log("CLEAR pressTimer 3"); clearInterval(pressTimer); pressTimer = undefined; } @@ -256,7 +256,7 @@ GPS.prototype.processFix = function(fix) { this.gpsState = this.GPS_RUNNING; if (!this.last_fix.fix && !(require("Storage").readJSON("setting.json", 1) || {}).quiet) { Bangle.buzz(); // buzz on first position - debug_log("GPS fix buzz"); + debug_log("BUZZ - gps fix"); } this.last_fix = fix; } @@ -303,7 +303,7 @@ GPS.prototype.getWPdistance = function() { //log_debug(this.last_fix); //log_debug(this.wp_current); - if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return 0; else return this.calcDistance(this.last_fix, this.wp_current); @@ -313,14 +313,14 @@ GPS.prototype.getWPbearing = function() { //log_debug(this.last_fix); //log_debug(this.wp_current); - if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return 0; else return this.calcBearing(this.last_fix, this.wp_current); } GPS.prototype.loadFirstWaypoint = function() { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; this.wp_index = 0; this.wp_current = waypoints[this.wp_index]; log_debug(this.wp_current); @@ -332,7 +332,7 @@ GPS.prototype.getCurrentWaypoint = function() { } GPS.prototype.waypointHasLocation = function() { - if (this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) + if (this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE" || this.wp_current.lat === undefined || this.wp_current.lat === 0) return false; else return true; @@ -340,12 +340,12 @@ GPS.prototype.waypointHasLocation = function() { GPS.prototype.markWaypoint = function() { - if(this.wp_current.name === "NONE") + if(this.wp_current.name === "E-WPT" || this.wp_current.name === "NONE") return; log_debug("GPS::markWaypoint()"); - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; this.wp_current = waypoints[this.wp_index]; if (this.waypointHasLocation()) { @@ -360,7 +360,7 @@ GPS.prototype.markWaypoint = function() { } GPS.prototype.nextWaypoint = function(inc) { - var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; + var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"E-WPT"}]; this.wp_index+=inc; if (this.wp_index>=waypoints.length) this.wp_index=0; if (this.wp_index<0) this.wp_index = waypoints.length-1; @@ -731,14 +731,14 @@ function TRIP() { TRIP.prototype.resetTrip = function(steps) { this.tripStart = (0 + steps); - console.log("resetTrip starting=" + this.tripStart); + log_debug("resetTrip starting=" + this.tripStart); } TRIP.prototype.getTrip = function(steps) { let tripSteps = (0 + steps) - this.tripStart; - console.log("getTrip steps=" + steps); - console.log("getTrip tripStart=" + this.tripStart); - console.log("getTrip=" + tripSteps); + log_debug("getTrip steps=" + steps); + log_debug("getTrip tripStart=" + this.tripStart); + log_debug("getTrip=" + tripSteps); return tripSteps; } @@ -758,7 +758,6 @@ Debug Object ******************************************************************************/ -/* function DEBUG() { this.logfile = require("Storage").open("debug.log","a"); } @@ -770,7 +769,6 @@ DEBUG.prototype.log = function(msg) { } debugObj = new DEBUG(); -*/ function debug_log(m) { //debugObj.log(m); diff --git a/apps/mysticclock/ChangeLog b/apps/mysticclock/ChangeLog new file mode 100644 index 000000000..34fe53627 --- /dev/null +++ b/apps/mysticclock/ChangeLog @@ -0,0 +1 @@ +1.00: First published version. diff --git a/apps/mysticclock/README.md b/apps/mysticclock/README.md new file mode 100644 index 000000000..fd5bbb431 --- /dev/null +++ b/apps/mysticclock/README.md @@ -0,0 +1,40 @@ +# Mystic Clock for Bangle.js + +A retro-inspired watchface featuring time, date, and an interactive data display line. + +## Features + +- 24 or 12-hour time (adjustable via the Settings menu) +- Variable colors (also in the Settings) +- Interactive data display line (use upper and lower watch-buttons to rotate between values) +- Cover watch screen with your hand to put it to sleep (the watch, not your hand) +- International localization of date (which can be disabled via the Settings if memory becomes an issue) + +The interactive line rotates between the following items: + +- Current time zone +- Battery charge level +- Device ID (derived from the last 4 of the MAC) +- Memory usage +- Firmware version + + +## Inspirations + +- [CLI Clock](https://github.com/espruino/BangleApps/tree/master/apps/cliock) +- [Dev Clock](https://github.com/espruino/BangleApps/tree/master/apps/dclock) +- [Digital Clock](https://github.com/espruino/BangleApps/tree/master/apps/digiclock) +- [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock) +- [Simplest Clock](https://github.com/espruino/BangleApps/tree/master/apps/simplest) + +Icon adapted from [Public Domain Vectors](https://publicdomainvectors.org/en/free-clipart/Digital-clock-display-vector-image/10845.html). + + +## Changelog + +- 1.00: First published version. (June 2021) + + +## Author + +Eric Wooodward https://itsericwoodward.com/ diff --git a/apps/mysticclock/mystic-clock-app.js b/apps/mysticclock/mystic-clock-app.js new file mode 100644 index 000000000..22c2c8982 --- /dev/null +++ b/apps/mysticclock/mystic-clock-app.js @@ -0,0 +1,215 @@ +/** + * Mystic Clock for Bangle.js + * + * + Original Author: Eric Wooodward https://itsericwoodward.com/ + * + see README.md for details + */ + +/* jshint esversion: 6 */ + +const timeFontSize = 6; +const dataFontSize = 2; +const font = "6x8"; + +const xyCenter = g.getWidth() / 2; + +const yposTime = 75; +const yposDate = 125; +const yposSymbol = 160; +const yposInfo = 220; + +const settings = require('Storage').readJSON('mysticclock.json', 1) || {}; +const colors = ['white', 'blue', 'green', 'purple', 'red', 'teal', 'other']; +const color = settings.color ? colors[settings.color] : 0; + +const infoData = { + '*GMT_MODE': { + calc: () => (new Date()).toString().split(" ")[5], + }, + BATT_MODE: { + calc: () => `BATT: ${E.getBattery()}%`, + }, + ID_MODE: { + calc: () => { + const val = NRF.getAddress().split(":"); + return `ID: ${val[4]}${val[5]}`; + }, + }, + MEM_MODE: { + calc: () => { + const val = process.memory(); + return `MEM: ${Math.round(val.usage * 100 / val.total)}%`; + }, + }, + VER_MODE: { + calc: () => `FW: ${process.env.VERSION}`, + }, +}; +const infoList = Object.keys(infoData).sort(); +let infoMode = infoList[0]; + +function setColor() { + const colorCommands = { + white: () => g.setColor(1, 1, 1), + blue: () => g.setColor(0, 0, 1), + green: () => g.setColor(0, 1, 0), + purple: () => g.setColor(1, 0, 1), + red: () => g.setColor(1, 0, 0), + teal: () => g.setColor(0, 1, 1), + other: () => g.setColor(1, 1, 0) + }; + + // default if value unknown + if (!color || !colorCommands[color]) return colorCommands.white(); + return colorCommands[color](); +} + +function getLocale() { + return require('locale'); +} + +function drawClock() { + + // default draw styles + g.reset(); + + // drawSting centered + g.setFontAlign(0, 0); + + // setup color + setColor(); + + // get date + const d = new Date(); + const dLocal = d.toString().split(" "); + + const useLocale = !settings.useLocale; + + const minutes = (`0${d.getMinutes()}`).substr(-2); + const seconds = (`0${d.getSeconds()}`).substr(-2); + + let hours = (`0${d.getHours()}`).substr(-2); + let meridian = ""; + + if (settings.use12Hour) { + hours = parseInt(hours, 10); + meridian = 'AM'; + if (hours === 0) { + hours = 12; + } + else if (hours >= 12) { + meridian = 'PM'; + if (hours > 12) hours -= 12; + } + hours = (' ' + hours).substr(-2); + } + + g.setFont(font, timeFontSize); + g.drawString(`${hours}${(d.getSeconds() % 2) ? ' ' : ':'}${minutes}`, xyCenter - 15, yposTime, true); + g.setFont(font, dataFontSize); + + if (settings.use12Hour) { + g.drawString(seconds, xyCenter + 97, yposTime - 10, true); + g.drawString(meridian, xyCenter + 97, yposTime + 10, true); + } + else { + g.drawString(seconds, xyCenter + 97, yposTime + 10, true); + } + + // draw DoW, name of month, date, year + g.setFont(font, dataFontSize); + g.drawString([ + useLocale ? getLocale().dow(d, 1) : dLocal[0], + useLocale ? getLocale().month(d, 1) : dLocal[1], + d.getDate(), + d.getFullYear() + ].join(" "), xyCenter, yposDate, true); + +} + +function drawInfo() { + if (infoData[infoMode] && infoData[infoMode].calc) { + // clear info + g.setColor(0, 0, 0); + g.fillRect(0, yposInfo - 8, 239, yposInfo + 25); + + // draw info + g.setFont(font, dataFontSize); + setColor(); + g.drawString((infoData[infoMode].calc()), xyCenter, yposInfo, true); + } +} + +function drawImage() { + setColor(); + g.drawPoly([xyCenter - 100, yposSymbol, xyCenter + 100, yposSymbol, xyCenter, yposSymbol + 30], true); +} + +function drawAll() { + drawClock(); + drawInfo(); + drawImage(); +} + +function nextInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === infoList.length - 1) infoMode = infoList[0]; + else infoMode = infoList[idx + 1]; + } +} + +function prevInfo() { + let idx = infoList.indexOf(infoMode); + if (idx > -1) { + if (idx === 0) infoMode = infoList[infoList.length - 1]; + else infoMode = infoList[idx - 1]; + } +} + + +let secondInterval; + +// handle LCD power state change +Bangle.on('lcdPower', on => { + + // stop running when screen turns off + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + + // start running + if (on) { + secondInterval = setInterval(drawAll, 1000); + drawAll(); // draw immediately + } +}); + +// cover screen to put it to sleep +Bangle.on('touch', (button) => { + if (button === 3 && Bangle.isLCDOn()) Bangle.setLCDPower(false); +}); + +// clean app screen +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// if screen already active, draw now and start interval +if (Bangle.isLCDOn()) { + secondInterval = setInterval(drawAll, 1000); + drawAll(); // draw immediately +} + +// show launcher when middle button pressed +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); + +// rotate through info when the buttons are pressed +setWatch(() => { + nextInfo(); + drawAll(); +}, BTN3, { repeat: true }); + +setWatch(() => { + prevInfo(); + drawAll(); +}, BTN1, { repeat: true }); diff --git a/apps/mysticclock/mystic-clock-icon.js b/apps/mysticclock/mystic-clock-icon.js new file mode 100644 index 000000000..7415fccd5 --- /dev/null +++ b/apps/mysticclock/mystic-clock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBIf4A/AH4A/AH4A/AH4ALs1msADCA4MGAgQDBBYIAGg93u92s4DBuEAAYN3swDCC5AhBuwMBg4XBuwEBs4dCC49nHgNwCQREBCYNnEYYXHHQQvBAAJZBAgRPEC5IOCu0GM4YLCuCGDAAREBHwtnJ41gDQIXEOAQvBDoZ7CuwjCWwimTJgLCFZojWEbwbWIAH4A/AH4A/AH4A/AH4AFA")) diff --git a/apps/mysticclock/mystic-clock-settings.js b/apps/mysticclock/mystic-clock-settings.js new file mode 100644 index 000000000..2fa0c49c5 --- /dev/null +++ b/apps/mysticclock/mystic-clock-settings.js @@ -0,0 +1,41 @@ +// make sure to enclose the function in parentheses +(function (back) { + + const settings = require('Storage').readJSON('mysticclock.json',1)||{}; + const colors = ['White', 'Blue', 'Green', 'Purple', 'Red', 'Teal', 'Yellow']; + const offon = ['Off','On']; + const onoff = ['On','Off']; + + function save(key, value) { + settings[key] = value; + require('Storage').writeJSON('mysticclock.json',settings); + } + + const appMenu = { + '': {'title': 'Clock Settings'}, + '< Back': back, + 'Color': { + value: 0|settings['color'], + min:0, + max:6, + format: m => colors[m], + onchange: m => {save('color', m)} + }, + '12 Hour Clock': { + value: 0|settings['use12Hour'], + min:0, + max:1, + format: m => offon[m], + onchange: m => {save('use12Hour', m)} + }, + 'Use Locale': { + value: 0|settings['useLocale'], + min:0, + max:1, + format: m => onoff[m], + onchange: m => {save('useLocale', m)} + } + }; + E.showMenu(appMenu) + +}) diff --git a/apps/mysticclock/mystic-clock.png b/apps/mysticclock/mystic-clock.png new file mode 100644 index 000000000..915e2ee32 Binary files /dev/null and b/apps/mysticclock/mystic-clock.png differ