diff --git a/apps.json b/apps.json index 7e7616b22..a0fcf0c79 100644 --- a/apps.json +++ b/apps.json @@ -120,7 +120,7 @@ "id": "welcome", "name": "Welcome", "icon": "app.png", - "version":"0.08", + "version":"0.09", "description": "Appears at first boot and explains how to use Bangle.js", "tags": "start,welcome", "allow_emulator": true, @@ -859,7 +859,7 @@ "id": "ncstart", "name": "NCEU Startup", "icon": "start.png", - "version":"0.05", + "version":"0.06", "description": "NodeConfEU 2019 'First Start' Sequence", "tags": "start,welcome", "storage": [ @@ -2376,5 +2376,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} + ] } ] \ No newline at end of file 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 diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog index 522633f7b..152fdc9d1 100644 --- a/apps/ncstart/ChangeLog +++ b/apps/ncstart/ChangeLog @@ -6,3 +6,4 @@ Don't run again when settings app is updated (or absent) Add "Run Now" option to settings 0.05: Don't overwrite existing settings on app update +0.06: Allow welcome to run after a fresh install diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js index 094033094..62ac962f6 100644 --- a/apps/ncstart/boot.js +++ b/apps/ncstart/boot.js @@ -1,11 +1,8 @@ (function() { - let s = require('Storage').readJSON('ncstart.json', 1) - || require('Storage').readJSON('setting.json', 1) - || {welcomed: true} // do NOT run if global settings are also absent - if (!s.welcomed && require('Storage').read('ncstart.app.js')) { + let s = require('Storage').readJSON('ncstart.json', 1) || {}; + if (!s.welcomed) { setTimeout(() => { - s.welcomed = true - require('Storage').write('ncstart.json', s) + require('Storage').write('ncstart.json', {welcomed: true}) load('ncstart.app.js') }) } diff --git a/apps/welcome/ChangeLog b/apps/welcome/ChangeLog index a377fc81e..9545dbbfa 100644 --- a/apps/welcome/ChangeLog +++ b/apps/welcome/ChangeLog @@ -8,3 +8,6 @@ Don't run again when settings app is updated (or absent) Add "Run Now" option to settings 0.08: Don't overwrite existing settings on app update +0.09: Allow welcome to run after a fresh install + More useful app menu + BTN2 now goes to menu on release diff --git a/apps/welcome/app.js b/apps/welcome/app.js index a32a6e56f..b4c79ddaa 100644 --- a/apps/welcome/app.js +++ b/apps/welcome/app.js @@ -285,7 +285,7 @@ setWatch(()=>{ if (sceneNumber == scenes.length-1) { load(); } -}, BTN2, {repeat:true,edge:"rising"}); +}, BTN2, {repeat:true,edge:"falling"}); setWatch(()=>move(-1), BTN1, {repeat:true}); (function migrateSettings(){ diff --git a/apps/welcome/boot.js b/apps/welcome/boot.js index f6ba6d2d6..4e3a12231 100644 --- a/apps/welcome/boot.js +++ b/apps/welcome/boot.js @@ -1,11 +1,8 @@ (function() { - let s = require('Storage').readJSON('welcome.json', 1) - || require('Storage').readJSON('setting.json', 1) - || {welcomed: true} // do NOT run if global settings are also absent - if (!s.welcomed && require('Storage').read('welcome.app.js')) { + let s = require('Storage').readJSON('welcome.json', 1) || {}; + if (!s.welcomed) { setTimeout(() => { - s.welcomed = true - require('Storage').write('welcome.json', {welcomed: "yes"}) + require('Storage').write('welcome.json', {welcomed: true}) load('welcome.app.js') }) } diff --git a/apps/welcome/settings.js b/apps/welcome/settings.js index 20c2e9b13..f269f238e 100644 --- a/apps/welcome/settings.js +++ b/apps/welcome/settings.js @@ -3,12 +3,16 @@ || require('Storage').readJSON('setting.json', 1) || {} E.showMenu({ '': { 'title': 'Welcome App' }, - 'Run on Next Boot': { + 'Run next boot': { value: !settings.welcomed, - format: v => v ? 'OK' : 'No', + format: v => v ? 'Yes' : 'No', onchange: v => require('Storage').write('welcome.json', {welcomed: !v}), }, 'Run Now': () => load('welcome.app.js'), + 'Turn off & run next': () => { + require('Storage').write('welcome.json', {welcomed: false}); + Bangle.off(); + }, '< Back': back, }) }) diff --git a/js/espruinotools.js b/js/espruinotools.js index 8e266f267..0e8df02cb 100644 --- a/js/espruinotools.js +++ b/js/espruinotools.js @@ -124,7 +124,7 @@ Espruino.Core.Status = { hasProgress : function() { return false; }, incrementProgress : function(amt) {} }; -var acorn = (function(){ var exports={}; +var acorn = (function(){ var exports={};var module={}; (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) :