diff --git a/apps.json b/apps.json index 7d4bf20cf..42d732f47 100644 --- a/apps.json +++ b/apps.json @@ -1732,5 +1732,19 @@ "storage": [ {"name":"rndmclk.wid.js","url":"widget.js"} ] + }, + { "id": "dotmatrixclock", + "name": "Dotmatrix Clock", + "icon": "dotmatrixclock.png", + "version":"0.01", + "description": "A clear white-on-blue dotmatrix simulated clock", + "tags": "clock,dotmatrix,retro", + "type": "clock", + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"dotmatrixclock.app.js","url":"app.js"}, + {"name":"dotmatrixclock.img","url":"dotmatrixclock-icon.js","evaluate":true} + ] } ] diff --git a/apps/dotmatrixclock/ChangeLog b/apps/dotmatrixclock/ChangeLog new file mode 100644 index 000000000..7ab9e14a9 --- /dev/null +++ b/apps/dotmatrixclock/ChangeLog @@ -0,0 +1 @@ +0.01: Create dotmatrix clock app diff --git a/apps/dotmatrixclock/README.md b/apps/dotmatrixclock/README.md new file mode 100644 index 000000000..3af48efc6 --- /dev/null +++ b/apps/dotmatrixclock/README.md @@ -0,0 +1,28 @@ +# Dotmatrix clock + +A clock face simulating the classic dotmatrix displays. Shows time, date, compass, and heart rate. + +![](dotmatrix-clock-screen-shot.png) + +## Features + +* Easy to read digits +* Simulated white-on-blue dotmatrix display +* Compass +* Heart rate monitor +* Multiple colour palletes, swipe to change + +## Usage + +### Sensor readings + +When the display is activated by 'flipping' the watch up, the compass and heart sensors will be activated automatically, but if +you activate the LCD through a button press, then the sensors will remain off until you press button-1. + +### Colours + +The display defaults to blue, but you can change this to orange by swiping the screen + +## Requests + +If you have any feature requests, please send an email to the author paulcockrell@gmail.com` diff --git a/apps/dotmatrixclock/app.js b/apps/dotmatrixclock/app.js new file mode 100755 index 000000000..94c628b1b --- /dev/null +++ b/apps/dotmatrixclock/app.js @@ -0,0 +1,354 @@ +/** + * BangleJS DotMatrixCLOCK + * + * + Original Author: Paul Cockrell https://github.com/paulcockrell + * + Created: May 2020 + */ +const storage = require('Storage'); +const settings = (storage.readJSON('setting.json', 1) || {}); +const is12Hour = settings["12hour"] || false; +const timeout = settings.timeout || 20; + +const font7x7 = { + "empty": "00000000", + "0": "3E61514945433E", + "1": "1808080808081C", + "2": "7E01013E40407F", + "3": "7E01013E01017E", + "4": "4141417F010101", + "5": "7F40407E01017E", + "6": "3E40407E41413E", + "7": "3F010202040408", + "8": "3E41413E41413E", + "9": "3E41413F01013E", +}; + +const font5x5 = { + "empty": "00000000", + "-": "0000FF0000", + "0": "0E1915130E", + "1": "0C0404040E", + "2": "1E010E101F", + "3": "1E010E011E", + "4": "11111F0101", + "5": "1F101E011E", + "6": "0E101E110E", + "7": "1F01020408", + "8": "0E110E110E", + "9": "0E110F010E", + "A": "040A0E1111", + "B": "1E111E111E", + "C": "0F1010100F", + "D": "1E1111111E", + "E": "1F101E101F", + "F": "1F101E1010", + "G": "0F1013110E", + "H": "11111F1111", + "I": "0E0404040E", + "J": "1F0404140C", + "L": "101010101F", + "M": "111B151111", + "N": "1119151311", + "O": "0E1111110E", + "P": "1E111E1010", + "R": "1E111E1111", + "S": "0F100E011E", + "T": "1F04040404", + "U": "111111110E", + "V": "1111110A04", + "W": "111115150A", + "Y": "110A040404", +}; + +// Char renderer +const COLORS = { + blue: { + BG: "#0297fe", + DARK: "#3b3ce8", + LIGHT: "#E9ffff", + }, + orange: { + BG: "#f7b336", + DARK: "#ac721e", + LIGHT: "#f6fc0f", + } +}; + +let selectedColor = "blue"; +let displayTimeoutRef, sensorTimeoutRef; + +// Example +// binToHex(["0111110", "1000000", "1000000", "1111110", "1000001", "1000001", "0111110"]) +function binToHex(bins) { + return bins.map(bin => ("00" + (parseInt(bin, 2).toString(16))).substr(-2).toUpperCase()).join(""); +} + +// Example +// hexToBin("3E40407E41413E") +function hexToBin(hexStr) { + const regEx = new RegExp("..", "g"); + const bin = hexStr + .replace(regEx, el => el + '_') + .slice(0, -1) + .split('_') + .map(hex => ("00000000" + (parseInt(hex, 16)).toString(2)).substr(-8)); + + return bin; +} + +function drawPixel(opts) { + g.setColor(opts.color); + g.fillRect(opts.x, opts.y, opts.x + opts.w, opts.y + opts.h); +} + +function drawGrid(pos, dims, charAsBin, opts) { + const defaultOpts = { + pxlW: 5, + pxlH: 5, + gap: 1, + offColor: COLORS[selectedColor].DARK, + onColor: COLORS[selectedColor].LIGHT + }; + const pxl = Object.assign({}, defaultOpts, opts); + + for (let rowY = 0; rowY < dims.rows; rowY++) { + const y = pos.y + ((pxl.pxlH + pxl.gap) * rowY); + + for (let colX = 7; colX > (7 - dims.cols); colX--) { + const x = pos.x + ((pxl.pxlW + pxl.gap) * colX); + const color = (charAsBin && parseInt(charAsBin[rowY][colX])) ? pxl.onColor : pxl.offColor; + + drawPixel({ + x: x, + y: y, + w: pxl.pxlW, + h: pxl.pxlH, + color: color, + }); + } + } +} + +function drawFont(str, font, x, y) { + let fontMap, rows, cols; + + switch(font) { + case "7x7": + fontMap = font7x7; + rows = cols = 7; + break; + case "5x5": + fontMap = font5x5; + rows = cols = 5; + break; + default: + throw "Unknown font type: " + font; + } + + const pxlW = 2; + const pxlH = 2; + const gap = 2; + const gutter = 3; + const charArr = str.split(""); + const gridWidthTotal = (rows * (pxlW + gap)) + gutter; + for (let i = 0; i < charArr.length; i++) { + const charAsBin = fontMap.hasOwnProperty(charArr[i])? + hexToBin(fontMap[charArr[i]]): + fontMap.empty; + + drawGrid( + {x: x + (i * gridWidthTotal), y: y}, + {rows: rows, cols: cols}, + charAsBin, + {pxlW: pxlW, pxlH: pxlH, gap: gap} + ); + } +} + +function drawTitles() { + g.setColor("#ffffff"); + g.setFont("6x8"); + g.drawString("COMPASS", 52, 49); + g.drawString("HEART", 122, 49); + g.drawString("TIME", 52, 94); + g.drawString("DATE", 52, 144); +} + +function drawCompass(lastHeading) { + const directions = [ + 'N', + 'NE', + 'E', + 'SE', + 'S', + 'SW', + 'W', + 'NW' + ]; + const cps = Bangle.getCompass(); + let angle = cps.heading; + let heading = angle? + directions[Math.round(((angle %= 360) < 0 ? angle + 360 : angle) / 45) % 8]: + "-- "; + + heading = (heading + " ").slice(0, 3); + if (lastHeading != heading) drawFont(heading, "5x5", 40, 67); + setTimeout(drawCompass.bind(null, heading), 1000 * 2); +} + +function drawHeart(hrm) { + drawFont((" " + (hrm ? hrm.bpm : "---")).slice(-3), "5x5", 109, 67); +} + +function drawTime(lastHrs, lastMns, toggle) { + const date = new Date(); + const h = date.getHours(); + const hrs = ("00" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2); + const mns = ("00" + date.getMinutes()).substr(-2); + + if (lastHrs != hrs) { + drawFont(hrs, "7x7", 48, 109); + } + if (lastMns != mns) { + drawFont(mns, "7x7", 124, 109); + } + + const color = toggle? COLORS[selectedColor].LIGHT : COLORS[selectedColor].DARK; + + // This should toggle on/off per second + drawPixel({ + color: color, + x: 118, y: 118, + w: 2, h: 2, + }); + drawPixel({ + color: color, + x: 118, y: 125, + w: 2, h: 2, + }); + + setTimeout(drawTime.bind(null, hrs, mns, !toggle), 1000); +} + +function drawDate(lastDate) { + const locale = require('locale'); + const date = new Date(); + + if (lastDate != date.toISOString().split('T')[0]) { + const dow = locale.dow(date, 1).toUpperCase(); + const dayNum = ("00" + date.getDate()).slice(-2); + const mon = locale.month(date).toUpperCase().slice(0, 3); + const yr = date.getFullYear().toString().slice(-2); + drawFont(dow + " " + dayNum, "5x5", 40, 159); + drawFont(mon + " " + yr, "5x5", 40, 189); + } + + setTimeout(drawDate.bind(null, date.toISOString().split('T')), 1000 * 60); +} + +function setSensors(state) { + // Already reading sensors and trying to activate sensors, do nothing + if (sensorTimeoutRef && state === 1) return; + + // If we are activating the sensors, turn them off again in one minute + if (state === 1) { + sensorTimeoutRef = setTimeout(() => { setSensors(0); }, 1000 * 60); + } else { + if (sensorTimeoutRef) { + clearInterval(sensorTimeoutRef); + sensorTimeoutRef = null; + } + // Bit nasty, but we only redraw the heart value on sensor callback + // but we want to blank out when sensor is off, but no callback for + // that so force redraw here + drawHeart(); + } + + Bangle.setHRMPower(state); + Bangle.setCompassPower(state); +} + +function drawScreen() { + g.setBgColor(COLORS[selectedColor].BG); + g.clearRect(0, 24, g.getWidth(), g.getHeight()); + + // Draw components + drawTitles(); + drawCompass(); + drawHeart(); + drawTime(); + drawDate(); +} + +function clearTimers(){ + if (displayTimeoutRef) { + clearInterval(displayTimeoutRef); + displayTimeoutRef = null; + } + + if (sensorTimeoutRef) { + clearInterval(sensorTimeoutRef); + sensorTimeoutRef = null; + } +} + +function resetDisplayTimeout() { + if (displayTimeoutRef) clearInterval(displayTimeoutRef); + Bangle.setLCDPower(true); + + displayTimeoutRef = setTimeout(() => { + if (Bangle.isLCDOn()) Bangle.setLCDPower(false); + clearTimers(); + }, 1000 * timeout); +} + +// Turn sensors on +setSensors(1); + +// Reset screen +g.clear(); + +// Load and draw widgets +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// Draw screen +drawScreen(); +resetDisplayTimeout(); + +// Setup callbacks +Bangle.on('swipe', (sDir) => { + selectedColor = selectedColor === "blue" ? "orange" : "blue"; + resetDisplayTimeout(); + drawScreen(); +}); + +Bangle.on('HRM', drawHeart); + +setWatch(() => { + setSensors(1); + resetDisplayTimeout(); +}, BTN1, {repeat: true, edge: "falling"}); + +setWatch(() => { + setSensors(0); + clearTimers(); + Bangle.setLCDMode(); + Bangle.showLauncher(); +}, BTN2, {repeat: false, edge: "falling"}); + +Bangle.on('lcdPower', (on) => { + if(on) { + resetDisplayTimeout(); + } else { + clearTimers(); + setSensors(0); + } +}); + +Bangle.on('faceUp', (up) => { + if (up && !Bangle.isLCDOn()) { + setSensors(1); + resetDisplayTimeout(); + } +}); \ No newline at end of file diff --git a/apps/dotmatrixclock/dotmatrix-clock-screen-shot.png b/apps/dotmatrixclock/dotmatrix-clock-screen-shot.png new file mode 100755 index 000000000..e6218f4c9 Binary files /dev/null and b/apps/dotmatrixclock/dotmatrix-clock-screen-shot.png differ diff --git a/apps/dotmatrixclock/dotmatrixclock-icon.js b/apps/dotmatrixclock/dotmatrixclock-icon.js new file mode 100644 index 000000000..8773839e1 --- /dev/null +++ b/apps/dotmatrixclock/dotmatrixclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AAmBAEgrFAAZelFo+s1krAEcAE4IuFHIIJBAEXXE4KSEF84nCF4WBgErBYoAfEoaTBF42zF8PXF5QNBi4AgMIYv/F9nX64CDAw4ACl8vBIgGGF/4AOEgKPfI4xfoF96P/R/6PdACAv/F/4v/F/4v/F8HX68Xl8vAwIDCBIQADBIQQDBoQQDF/4AOGQqPbLAxmGL5gGDF/4AfF/6PRBIQQDSwwv/ABwoCR7xYGMwxfhF94AeF/4vr1nXBoIAf64mCF4gJEF8IkCF4YABFYQLDAEItBwIuCF9InBF4iSBwMrAEgnBFwgACXsIADFo4ABqwAkFQg=")) diff --git a/apps/dotmatrixclock/dotmatrixclock.png b/apps/dotmatrixclock/dotmatrixclock.png new file mode 100755 index 000000000..ab2637520 Binary files /dev/null and b/apps/dotmatrixclock/dotmatrixclock.png differ