diff --git a/.gitignore b/.gitignore index abc3e9bd1..be33fbc90 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules package-lock.json .DS_Store *.js.bak +appdates.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1552747..e5cd6aef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,7 @@ Changed for individual apps are listed in `apps/appname/ChangeLog` * Added espruinotools.js for pretokenisation * Included image and compression tools in repo * Added better upload of large files (incl. compression) +* URL fetch is now async +* Adding '#search' after the URL (when not the name of a 'filter' chip) will set up search for that term +* If `bin/pre-publish.sh` has been run and recent.csv created, add 'Sort By' chip +* New 'espruinotools' which fixes pretokenise issue when ID follows ID (fix #416) diff --git a/README.md b/README.md index a45647daf..04854c99e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Bangle.js App Loader (and Apps) * Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps) * Try the **development version** at [github.io](https://espruino.github.io/BangleApps/) -**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By +**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By submitting code to this repository you confirm that you are happy with it being MIT licensed, and that it is not licensed in another way that would make this impossible. @@ -203,7 +203,7 @@ and which gives information about the app for the Launcher. // added by BangleApps loader on upload - lists all files // that belong to the app so it can be deleted "data":"appid.data.json,appid.data?.json;appidStorageFile,appidStorageFile*" - // added by BangleApps loader on upload - lists files that + // added by BangleApps loader on upload - lists files that // the app might write, so they can be deleted on uninstall // typically these files are not uploaded, but created by the app // these can include '*' or '?' wildcards @@ -251,7 +251,7 @@ and which gives information about the app for the Launcher. "storageFile":true // if supplied, file is treated as storageFile }, {"wildcard":"appid.data.*" // wildcard of filenames used in storage - }, // this is mutually exclusive with using "name" + }, // this is mutually exclusive with using "name" ], "sortorder" : 0, // optional - choose where in the list this goes. // this should only really be used to put system @@ -341,9 +341,12 @@ See [apps/gpsrec/interface.html](the GPS Recorder) for a full example. Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings". To do so, the app needs to include a `settings.js` file, containing a single function that handles configuring the app. -When the app settings are opened, this function is called with one +When the app settings are opened, this function is called with one argument, `back`: a callback to return to the settings menu. +Usually it will save any information in `app.json` where `app` is the name +of your app - so you should change the example accordingly. + Example `settings.js` ```js // make sure to enclose the function in parentheses @@ -352,7 +355,7 @@ Example `settings.js` function save(key, value) { settings[key] = value; require('Storage').write('app.json',settings); - } + } const appMenu = { '': {'title': 'App Settings'}, '< Back': back, diff --git a/apps.json b/apps.json index 0c63a9848..04c2f637d 100644 --- a/apps.json +++ b/apps.json @@ -2,7 +2,7 @@ { "id": "boot", "name": "Bootloader", "icon": "bootloader.png", - "version":"0.16", + "version":"0.17", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "tags": "tool,system", "type":"bootloader", @@ -78,7 +78,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, @@ -108,7 +108,7 @@ { "id": "mclock", "name": "Morphing Clock", "icon": "clock-morphing.png", - "version":"0.05", + "version":"0.06", "description": "7 segment clock that morphs between minutes and hours", "tags": "clock", "type":"clock", @@ -168,7 +168,7 @@ "name": "Image background clock", "shortName":"Image Clock", "icon": "app.png", - "version":"0.05", + "version":"0.06", "description": "A clock with an image as a background", "tags": "clock", "type" : "clock", @@ -589,7 +589,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": [ @@ -1272,7 +1272,7 @@ "name": "Battery Chart", "shortName":"Battery Chart", "icon": "app.png", - "version":"0.09", + "version":"0.10", "readme": "README.md", "description": "A widget and an app for recording and visualizing battery percentage over time.", "tags": "app,widget,battery,time,record,chart,tool", @@ -1421,7 +1421,7 @@ "id": "metronome", "name": "Metronome", "icon": "metronome_icon.png", - "version": "0.04", + "version": "0.05", "readme": "README.md", "description": "Makes the watch blinking and vibrating with a given rate", "tags": "tool", @@ -1435,7 +1435,8 @@ "name": "metronome.img", "url": "metronome-icon.js", "evaluate": true - } + }, + {"name":"metronome.settings.js","url":"settings.js"} ] }, { "id": "blackjack", @@ -1469,7 +1470,7 @@ "name": "Round clock with seconds, minutes and date", "shortName":"Round Clock", "icon": "app.png", - "version":"0.02", + "version":"0.03", "description": "Designed round clock with ticks for minutes and seconds and heart rate indication", "tags": "clock", "type": "clock", @@ -1572,7 +1573,7 @@ "id": "largeclock", "name": "Large Clock", "icon": "largeclock.png", - "version": "0.02", + "version": "0.03", "description": "A readable and informational digital watch, with date, seconds and moon phase", "readme": "README.md", "tags": "clock", @@ -1591,12 +1592,10 @@ { "name": "largeclock.settings.js", "url": "settings.js" - }, - { - "name": "largeclock.json", - "url": "largeclock.json", - "evaluate": true } + ], + "data": [ + {"name":"largeclock.json"} ] }, { "id": "smtswch", @@ -1617,6 +1616,18 @@ {"name":"switch-off.img","url":"switch-off.js","evaluate":true} ] }, + { "id": "miplant", + "name": "Xiaomi Plant Sensor", + "shortName":"Mi Plant", + "icon": "app.png", + "version":"0.01", + "description": "Reads and displays data from Xiaomi bluetooth plant moisture sensors", + "tags": "xiaomi,mi,plant,ble,bluetooth", + "storage": [ + {"name":"miplant.app.js","url":"app.js"}, + {"name":"miplant.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "simpletimer", "name": "Timer", @@ -1699,7 +1710,7 @@ "version": "0.01", "description": "A clock for time travellers. The light pie segment shows the minutes, the black circle, the hour. The dial itself reads 'time' just in case you forget.", "tags": "clock", - "readme": "README.md", + "readme": "README.md", "type": "clock", "allow_emulator":true, "storage": [ @@ -1708,7 +1719,46 @@ { "name": "gallifr.settings.js", "url": "settings.js" } ], "data": [ - {"name":"app.json"} + {"name":"gallifr.json"} ] + }, + { "id": "rndmclk", + "name": "Random Clock Loader", + "icon": "rndmclk.png", + "version":"0.02", + "description": "Load a different clock whenever the LCD is switched on.", + "readme": "README.md", + "tags": "widget,clock", + "type":"widget", + "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} + ] + }, + { + "id": "jbm8b", + "name": "Magic 8 Ball", + "shortName": "Magic 8 Ball", + "icon": "app.png", + "description": "A simple fortune telling app", + "tags": "game", + "storage": [ + { "name": "jbm8b.app.js", "url": "app.js" }, + { "name": "jbm8b.img", "url": "app-icon.js", "evaluate": true } + ], + "version": "0.03" } -] \ No newline at end of file +] diff --git a/apps/_example_app/add_to_apps.json b/apps/_example_app/add_to_apps.json index bb0377b66..1585ab73d 100644 --- a/apps/_example_app/add_to_apps.json +++ b/apps/_example_app/add_to_apps.json @@ -11,4 +11,4 @@ {"name":"7chname.app.js","url":"app.js"}, {"name":"7chname.img","url":"app-icon.js","evaluate":true} ] -} +} \ No newline at end of file diff --git a/apps/batchart/ChangeLog b/apps/batchart/ChangeLog index 66b40fbbf..31c386684 100644 --- a/apps/batchart/ChangeLog +++ b/apps/batchart/ChangeLog @@ -6,4 +6,5 @@ 0.06: Fixes widget events and charting of component states 0.07: Improve logging and charting of component states and add widget icon 0.08: Fix for Home button in the app and README added. -0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state \ No newline at end of file +0.09: Fix failing dismissal of Gadgetbridge notifications, record (coarse) bluetooth state +0.10: Remove widget icon and improve listener and setInterval handling for widget (might help with https://github.com/espruino/BangleApps/issues/381) \ No newline at end of file diff --git a/apps/batchart/widget.js b/apps/batchart/widget.js index 96f8b4b25..4a116c990 100644 --- a/apps/batchart/widget.js +++ b/apps/batchart/widget.js @@ -1,6 +1,7 @@ (() => { + let recordingInterval = null; const Storage = require("Storage"); - + const switchableConsumers = { none: 0, lcd: 1, @@ -14,53 +15,44 @@ const recordingInterval10Min = 60 * 10 * 1000; const recordingInterval1Min = 60 * 1000; //For testing const recordingInterval10S = 10 * 1000; //For testing - var recordingInterval = null; var compassEventReceived = false; var gpsEventReceived = false; var hrmEventReceived = false; - // draw your widget function draw() { - let x = this.x; - let y = this.y; - - g.setColor(0, 1, 0); - g.fillPoly([x + 5, y, x + 5, y + 4, x + 1, y + 4, x + 1, y + 20, x + 18, y + 20, x + 18, y + 4, x + 13, y + 4, x + 13, y], true); - - g.setColor(0, 0, 0); - g.drawPoly([x + 5, y + 6, x + 8, y + 12, x + 13, y + 12, x + 16, y + 18], false); - - g.reset(); + // void } - function onMag() { + function batteryChartOnMag() { compassEventReceived = true; // Stop handling events when no longer necessarry - Bangle.removeListener("mag", onMag); + Bangle.removeListener("mag", batteryChartOnMag); } - function onGps() { + function batterChartOnGps() { gpsEventReceived = true; - Bangle.removeListener("GPS", onGps); + Bangle.removeListener("GPS", batterChartOnGps); } - function onHrm() { + function batteryChartOnHrm() { hrmEventReceived = true; - Bangle.removeListener("HRM", onHrm); + Bangle.removeListener("HRM", batteryChartOnHrm); } function getEnabledConsumersValue() { // Wait for an event from each of the devices to see if they are switched on var enabledConsumers = switchableConsumers.none; - Bangle.on('mag', onMag); - Bangle.on('GPS', onGps); - Bangle.on('HRM', onHrm); + Bangle.on('mag', batteryChartOnMag); + Bangle.on('GPS', batterChartOnGps); + Bangle.on('HRM', batteryChartOnHrm); // Wait two seconds, that should be enough for each of the events to get raised once setTimeout(() => { - Bangle.removeAllListeners(); + Bangle.removeListener('mag', batteryChartOnMag); + Bangle.removeListener('GPS', batterChartOnGps); + Bangle.removeListener('HRM', batteryChartOnHrm); }, 2000); if (Bangle.isLCDOn()) @@ -112,14 +104,20 @@ } function reload() { - WIDGETS["batchart"].width = 24; + console.log("Reloading BatteryChart widget"); + WIDGETS["batchart"].width = 0; + + if (recordingInterval) { + clearInterval(recordingInterval); + recordingInterval = null; + } recordingInterval = setInterval(logBatteryData, recordingInterval10Min); } // add the widget WIDGETS["batchart"] = { - area: "tl", width: 24, draw: draw, reload: reload + area: "tl", width: 0, draw: draw, reload: reload }; reload(); diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index c12654e97..c157c6705 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -15,3 +15,4 @@ 0.14: Move welcome loaders to *.boot.js 0.15: Added BLE HID option for Joystick and bare Keyboard 0.16: Detect out of memory errors and draw them onto the bottom of the screen in red +0.17: Don't modify beep/buzz behaviour if firmware does it automatically diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js index 9463df27c..38423362d 100644 --- a/apps/boot/boot0.js +++ b/apps/boot/boot0.js @@ -21,20 +21,22 @@ if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth // Don't disconnect if something is already connected to us if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep(); // Set time, vibrate, beep, etc -if (!s.vibrate) Bangle.buzz=Promise.resolve; -if (s.beep===false) Bangle.beep=Promise.resolve; -else if (s.beep=="vib") Bangle.beep = function (time, freq) { - return new Promise(function(resolve) { - if ((0|freq)<=0) freq=4000; - if ((0|time)<=0) time=200; - if (time>5000) time=5000; - analogWrite(D13,0.1,{freq:freq}); - setTimeout(function() { - digitalWrite(D13,0); - resolve(); - }, time); - }); -}; +if (!Bangle.F_BEEPSET) { + if (!s.vibrate) Bangle.buzz=Promise.resolve; + if (s.beep===false) Bangle.beep=Promise.resolve; + else if (s.beep=="vib") Bangle.beep = function (time, freq) { + return new Promise(function(resolve) { + if ((0|freq)<=0) freq=4000; + if ((0|time)<=0) time=200; + if (time>5000) time=5000; + analogWrite(D13,0.1,{freq:freq}); + setTimeout(function() { + digitalWrite(D13,0); + resolve(); + }, time); + }); + }; +} Bangle.setLCDTimeout(s.timeout); if (!s.timeout) Bangle.setLCDPower(1); E.setTimeZone(s.timezone); 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/files/files.js b/apps/files/files.js index 6ccbe80df..ab259d6df 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -201,7 +201,7 @@ function showSortAppsManually() { function setSortorder(app, val) { app = store.readJSON(app.id + '.info', 1); app.sortorder = val; - store.writeJSON(app.id + '.info', app); + store.write(app.id + '.info', JSON.stringify(app)); } function getAppsList() { diff --git a/apps/gallifr/app.js b/apps/gallifr/app.js index b775d247f..8948393d5 100644 --- a/apps/gallifr/app.js +++ b/apps/gallifr/app.js @@ -10,7 +10,7 @@ const cirRad = 2*Math.PI; const proportion = 0.3; // relative size of hour hand const thickness = 4; // thickness of decorative lines // retrieve settings from menu -let settings = require('Storage').readJSON('app.json',1)||{}; +let settings = require('Storage').readJSON('gallifr.json',1)||{}; const decoration = !settings.decoration; const widgets = !settings.widgets; if (widgets) { @@ -133,21 +133,21 @@ const drawDecoration = () => { drawSegment(params); params = { fromX: 0.4, - fromY: 0.2, + fromY: 0.2, toX: 0.6, toY: 0.1 }; drawThickLine(params); params = { fromX: -0.2, - fromY: -0.05, + fromY: -0.05, toX: -0.7, toY: -0.7 }; drawThickLine(params); params = { fromX: -0.3, - fromY: 0.05, + fromY: 0.05, toX: -0.95, toY: -0.3 }; @@ -166,7 +166,7 @@ const drawMinuteHand = () => { break; case "blue": g.setColor(0,0,1); - break; + break; case "80s": g.setColor(1,0,0); break; @@ -203,7 +203,7 @@ const drawClockFace = () => { break; case "blue": g.setColor(0,0.3,0.8); - break; + break; case "80s": g.setColor(1,1,1); break; diff --git a/apps/gallifr/settings.js b/apps/gallifr/settings.js index 78e7e516d..bf6aae846 100644 --- a/apps/gallifr/settings.js +++ b/apps/gallifr/settings.js @@ -1,11 +1,11 @@ // make sure to enclose the function in parentheses (function (back) { - let settings = require('Storage').readJSON('app.json',1)||{}; + let settings = require('Storage').readJSON('gallifr.json',1)||{}; let colours = ["green","red","blue","80s"]; let onoff = ["on","off"]; function save(key, value) { settings[key] = value; - require('Storage').write('app.json',settings); + require('Storage').writeJSON('gallifr.json',settings); } const appMenu = { '': {'title': 'Clock Settings'}, @@ -21,13 +21,13 @@ min:0,max:1, format: m => onoff[m], onchange: m => {save('widgets', m)} - }, + }, 'Decoration': { value: 0|settings['decoration'], min:0,max:1, format: m => onoff[m], onchange: m => {save('decoration', m)} - } + } }; E.showMenu(appMenu) - }) \ No newline at end of file + }) diff --git a/apps/jbm8b/ChangeLog b/apps/jbm8b/ChangeLog new file mode 100644 index 000000000..80d7de1d6 --- /dev/null +++ b/apps/jbm8b/ChangeLog @@ -0,0 +1,3 @@ +0.01: First working version +0.02: Added delay in replying for dramatic effect +0.03: Fixed apps.json entry diff --git a/apps/jbm8b/app-icon.js b/apps/jbm8b/app-icon.js new file mode 100644 index 000000000..09bf032a6 --- /dev/null +++ b/apps/jbm8b/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhBC/AGMrq2B1gAEwNWlYthq2s64AKGYIydFpoAEGLUrFqIADqxcXFqhiDFymBFy7GCF1owTRjCSVlYudeiGsF7/XlaNqSKBeP1mBwJxQMBReO1gaEleBMDBLN1hAC1hhBAoIwNCwQAGlZINqxvFGAIXOSBAXQN4hPBC5yQIVBxfBCAgvQSBC+NFAYRDMwJHOF654DqxkBYooALF6+sbIhkEF8Z3CRIWBR6AvXFAzvQF6wnIYQJgNd5AWNdoLoGBBAvPO5pfYH4IvUUwS/GVBzXBYCpHCq2s1mBDwKOWDwRgNPAwVVMCRLCwIABCZ6OJJSAATLxZgRACJeLAAMrFz9WFxiRgRpoADwIub1guQGDmsXhqSfRiL0G1jqkMRYxRwKLUGK2sFryVEq2B1gAEwNWFkIA/AH4A/AH4AQ")) \ No newline at end of file diff --git a/apps/jbm8b/app.js b/apps/jbm8b/app.js new file mode 100644 index 000000000..53baa32e3 --- /dev/null +++ b/apps/jbm8b/app.js @@ -0,0 +1,80 @@ +const affirmative = [ + 'It is\ncertain.', + 'It is\ndicededly\nso.', + 'Without\na doubt.', + 'Yes\ndefinitely.', + 'You may\nrely\non it.', + 'As I see,\nit yes.', + 'Most\nlikely.', + 'Outlook\ngood.', + 'Yes.', + 'Signs point\nto yes.' +]; +const nonCommittal = [ + 'Reply hazy,\ntry again.', + 'Ask again\nlater.', + 'Better not\ntell you\nnow.', + 'Cannot\npredict\nnow.', + 'Concentrate\nand\nask again.' +]; +const negative = [ + 'Don\'t\ncount on it.', + 'My reply\nis no.', + 'My sources\nsay no.', + 'Outlook\nis not\nso\ngood.', + 'Very\ndoubtful.' +]; + +const title = 'Magic 8 Ball'; + +const answers = [affirmative, nonCommittal, negative]; + +function getRandomArbitrary(min, max) { + return Math.random() * (max - min) + min; +} + +function predict() { + // affirmative, negative or non-committal + let max = answers.length; + const a = Math.floor(getRandomArbitrary(0, max)); + // sets max compared to answer category + max = answers[a].length; + const b = Math.floor(getRandomArbitrary(0, max)); + // get the answer + const response = answers[a][b]; + return response; +} + +function draw(msg) { + // console.log(msg); + g.clear(); + E.showMessage(msg, title); +} + +function reply(button) { + const theButton = (typeof button === 'undefined' || isNaN(button)) ? 1 : button; + const timer = Math.floor(getRandomArbitrary(0, theButton) * 1000); + // Thinking... + draw('...'); + setTimeout('draw(predict());', timer); +} + +function ask() { + draw('Ask me a\nYes or No\nquestion\nand\ntouch the\nscreen'); +} + +g.clear(); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); +ask(); + +// Event Handlers + +Bangle.on('touch', (button) => reply(button)); + +setWatch(ask, BTN1, { repeat: true, edge: "falling" }); +setWatch(reply, BTN3, { repeat: true, edge: "falling" }); + +// Back to launcher +setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); \ No newline at end of file diff --git a/apps/jbm8b/app.png b/apps/jbm8b/app.png new file mode 100644 index 000000000..24c3013de Binary files /dev/null and b/apps/jbm8b/app.png differ diff --git a/apps/largeclock/ChangeLog b/apps/largeclock/ChangeLog index c45f61d7e..fe44e5078 100644 --- a/apps/largeclock/ChangeLog +++ b/apps/largeclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: Init 0.02: fix 3/4 moon orientation +0.03: Change `largeclock.json` to 'data' file to allow settings to be preserved diff --git a/apps/largeclock/largeclock.js b/apps/largeclock/largeclock.js index e118793cb..9975775fb 100644 --- a/apps/largeclock/largeclock.js +++ b/apps/largeclock/largeclock.js @@ -9,10 +9,8 @@ const moonX = 215; const moonY = 50; const settings = require("Storage").readJSON("largeclock.json", 1); -const BTN1app = settings.BTN1; -const BTN3app = settings.BTN3; -console.log("BTN1app", BTN1app); -console.log("BTN3app", BTN3app); +const BTN1app = settings.BTN1 || ""; +const BTN3app = settings.BTN3 || ""; function drawMoon(d) { const BLACK = 0, @@ -174,14 +172,14 @@ Bangle.setLCDMode(); // Show launcher when middle button pressed clearWatch(); setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" }); -setWatch( +if (BTN1app) setWatch( function() { load(BTN1app); }, BTN1, { repeat: false, edge: "rising" } ); -setWatch( +if (BTN3app) setWatch( function() { load(BTN3app); }, diff --git a/apps/largeclock/settings.js b/apps/largeclock/settings.js index 3901747b8..a33f3c438 100644 --- a/apps/largeclock/settings.js +++ b/apps/largeclock/settings.js @@ -34,7 +34,7 @@ function onchange(v) { settings[btn] = v; - s.write("largeclock.json", settings); + s.writeJSON("largeclock.json", settings); } const btnMenu = { diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog index 98566f277..cca1b6e6b 100644 --- a/apps/mclock/ChangeLog +++ b/apps/mclock/ChangeLog @@ -3,3 +3,4 @@ 0.04: Improve performance, attempt to remove occasional glitch when LCD on (fix #279) 0.05: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast Fix issue where first digit could get stuck going from "2x:xx" to " x:xx" (fix #365) +0.06: Support 12 hour time diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js index 8a2c62e28..32048cd60 100644 --- a/apps/mclock/clock-morphing.js +++ b/apps/mclock/clock-morphing.js @@ -1,3 +1,4 @@ +var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]; var locale = require("locale"); var CHARW = 34; // how tall are digits? var CHARP = 2; // how chunky are digits? @@ -146,7 +147,7 @@ function drawDigits(lastText,thisText,n) { x+=s+p+7; } } -function drawSeconds() { +function drawEverythingElse() { var x = (CHARW + CHARP + 6)*5; var y = Y + 2*CHARW + CHARP; var d = new Date(); @@ -154,6 +155,8 @@ function drawSeconds() { g.setFont("6x8"); g.setFontAlign(-1,-1); g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true); + // meridian + if (is12Hour) g.drawString((d.getHours() < 12) ? "AM" : "PM", x, Y + 4, true); // date g.setFontAlign(0,-1); var date = locale.date(d,false); @@ -164,13 +167,15 @@ function drawSeconds() { function showTime() { if (animInterval) return; // in animation - quit var d = new Date(); - var t = (" "+d.getHours()).substr(-2)+":"+ + var hours = d.getHours(); + if (is12Hour) hours = ((hours + 11) % 12) + 1; + var t = (" "+hours).substr(-2)+":"+ ("0"+d.getMinutes()).substr(-2); var l = lastTime; // same - don't animate if (t==l || l=="-----") { drawDigits(l,t,0); - drawSeconds(); + drawEverythingElse(); lastTime = t; return; } diff --git a/apps/metronome/ChangeLog b/apps/metronome/ChangeLog index 25628660e..909d6b983 100644 --- a/apps/metronome/ChangeLog +++ b/apps/metronome/ChangeLog @@ -2,3 +2,4 @@ 0.02: Watch vibrates with every beat 0.03: Uses mean of three time intervalls to calculate bmp 0.04: App shows instructions, Widgets remain visible, color changed +0.05: Buzz intensity and beats per bar can be changed via settings-app diff --git a/apps/metronome/README.md b/apps/metronome/README.md index 1bb9a893c..f67b4adf1 100644 --- a/apps/metronome/README.md +++ b/apps/metronome/README.md @@ -8,6 +8,7 @@ This metronome makes your watch blink and vibrate with a given rate. * Use `BTN1` to increase the bmp value by one. * Use `BTN3` to decrease the bmp value by one. * You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`. +* Intensity of buzzing and the beats per bar (default 4) can be changed with the settings-app. The first beat per bar will be marked in red. ## Attributions diff --git a/apps/metronome/metronome.js b/apps/metronome/metronome.js index c41305f77..add6fee16 100644 --- a/apps/metronome/metronome.js +++ b/apps/metronome/metronome.js @@ -6,31 +6,40 @@ var tindex=0; //index to iterate through time_diffs Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app +const storage = require("Storage"); +const SETTINGS_FILE = 'metronome.settings.json'; + +//return setting +function setting(key) { + //define default settings + const DEFAULTS = { + 'beatsperbar': 4, + 'buzzintens': 0.75, + }; + if (!settings) { loadSettings(); } + return (key in settings) ? settings[key] : DEFAULTS[key]; +} + +//load settings +let settings; + +function loadSettings() { + settings = storage.readJSON(SETTINGS_FILE, 1) || {}; +} + function changecolor() { - const maxColors = 2; - const colors = { - 0: { value: 0xFFFF, name: "White" }, - // 1: { value: 0x000F, name: "Navy" }, - // 2: { value: 0x03E0, name: "DarkGreen" }, - // 3: { value: 0x03EF, name: "DarkCyan" }, - // 4: { value: 0x7800, name: "Maroon" }, - // 5: { value: 0x780F, name: "Purple" }, - // 6: { value: 0x7BE0, name: "Olive" }, - // 7: { value: 0xC618, name: "LightGray" }, - // 8: { value: 0x7BEF, name: "DarkGrey" }, - // 9: { value: 0x001F, name: "Blue" }, - // 10: { value: 0x07E0, name: "Green" }, - // 11: { value: 0x07FF, name: "Cyan" }, - 1: { value: 0xF800, name: "Red" }, - // 13: { value: 0xF81F, name: "Magenta" }, - // 14: { value: 0xFFE0, name: "Yellow" }, - // 15: { value: 0xFFFF, name: "White" }, - // 16: { value: 0xFD20, name: "Orange" }, - // 17: { value: 0xAFE5, name: "GreenYellow" }, - // 18: { value: 0xF81F, name: "Pink" }, + const colors = { + 0: { value: 0xF800, name: "Red" }, + 1: { value: 0xFFFF, name: "White" }, + 2: { value: 0x9492, name: "gray" }, + 3: { value: 0xFFFF, name: "White" }, + 4: { value: 0x9492, name: "gray" }, + 5: { value: 0xFFFF, name: "White" }, + 6: { value: 0x9492, name: "gray" }, + 7: { value: 0xFFFF, name: "White" }, }; g.setColor(colors[cindex].value); - if (cindex == maxColors-1) { + if (cindex == setting('beatsperbar')-1) { cindex = 0; } else { @@ -42,11 +51,16 @@ function changecolor() { function updateScreen() { g.clearRect(0, 50, 250, 150); changecolor(); - Bangle.buzz(50, 0.75); + try { + Bangle.buzz(50, setting('buzzintens')); + } + catch(err) { + } g.setFont("Vector",48); g.drawString(Math.floor(bpm)+"bpm", 5, 60); } + Bangle.on('touch', function(button) { // setting bpm by tapping the screen. Uses the mean time difference between several tappings. if (tindex < time_diffs.length) { diff --git a/apps/metronome/settings.js b/apps/metronome/settings.js new file mode 100644 index 000000000..1dd4d92df --- /dev/null +++ b/apps/metronome/settings.js @@ -0,0 +1,48 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const SETTINGS_FILE = 'metronome.settings.json'; + + // initialize with default settings... + let s = { + 'beatsperbar': 4, + 'buzzintens': 0.75, + }; + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage'); + const saved = storage.readJSON(SETTINGS_FILE, 1) || {}; + for (const key in saved) { + s[key] = saved[key]; + } + + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function(value) { + s[key] = value; + storage.write(SETTINGS_FILE, s); + }; + } + + const menu = { + '': { 'title': 'Metronome' }, + '< Back': back, + 'beats per bar': { + value: s.beatsperbar, + min: 1, + max: 8, + step: 1, + onchange: save('beatsperbar'), + }, + 'buzz intensity': { + value: s.buzzintens, + min: 0.0, + max: 1.0, + step: 0.25, + onchange: save('buzzintens'), + }, + }; + E.showMenu(menu); +}); diff --git a/apps/miplant/ChangeLog b/apps/miplant/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/miplant/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/miplant/app-icon.js b/apps/miplant/app-icon.js new file mode 100644 index 000000000..bf800ea4c --- /dev/null +++ b/apps/miplant/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+4AA/AH4A/AAPQ64AQ44ZKBYwvc6/QF9wwFF9XXF73I4IAH44/F54vdCpYQESAYvm5Avm44ADA4Yvmc44v/F/4v/F/4v/F/4v+zmjv4ABF9GrwoACrYvn2WGFwgABSh4AV0QtDFwYvmFwlhF9wuDF9QuEF82jFw4vmMIQuFv4vuEDOi0d/WgV/0YNGBIN/LrSvCABAkECAI4GAChKBF5QABCIYEDADKpCsIvJLQQ0EGoShJMBwACSJYvEB5GiMCYxJF4LuCLorTLF6IxGQILuBMYgAGC4QwP0YwHYwQbCF4K1CL4lhCohfQMBBiCFQQvEAgIsFAATxWSQyPDAYYSIHgRgZE4IsBF4QGCCI6NRMBboFXgTTIFyYwIPYWcGAb2CCI2iF6qwBD4aqERYQABIQ+cFywALLwgAqXoYvrSAQROA==")) diff --git a/apps/miplant/app.js b/apps/miplant/app.js new file mode 100644 index 000000000..336fddc15 --- /dev/null +++ b/apps/miplant/app.js @@ -0,0 +1,74 @@ + +function getImgHum() { + return require("heatshrink").decompress(atob("jUoxH+AEtlsoYYDS4ZYDAYaVDLAYFDSQYHDSIZYDBIaPDLAYLDRoZYDBoaLDLAYPDRIZYDCIaHDLAYTDQoZYDCoaDDOQYXAA+JxIYX1utDSwYBAAIzYGiwZUTgpODQpzPGGgY3OdI4aRDIIaMDJIYCDIztDGRwaJP5oaWDAwaRDBAbOC5YcKB5I=")); +} +function getImgTemp() { + return require("heatshrink").decompress(atob("iUqxH+AA2sAAQLHCBASMCAoSLCPOBAAQRfI/5Hn3YACy4ACCL4ADCL5H/I/AQHCRAQJCQwQLCQgQNCQYRQCB4A/ADaPjYqTpSCRYQGCZALFA")); +} +function getImgFert() { + return require("heatshrink").decompress(atob("kklxH+AC+FwtbDbAfFAAVbEbgiGEbYiHEbQiEsIjiEQYjeEQiPdEQrXdEdKnTAAJsMD6QlJFZAAIGAIkPEaIkCrdhEaR9MT4gkLFAyjMYoojNUZ4jFEoxrGEBCJDEZSWEEZdhCwpsKJQiJFAgYgGEQwjLD4QjFCRD+KCAylGQ4gjXVhAiPEhAKDJIwiQEowIEEQo2GERgAKEYwAcEUQkDEL9VAAgHFETgAIDJwePEZwdTE5ggdMJt6AAQEEqwRMABYQDAAwkBF5AkKEBQAPEUR6ESAQicJIX+A==")); +} + +var deviceInfo = {}; + +function parseDevice(device) { + var d = new DataView(device.serviceData["fe95"]); + var frame = d.getUint16(0,true); + var offset = 5; + if (frame&16) offset+=6; // mac address + if (frame&32) offset+=1; // capabilitities + if (frame&64) { // event + var l = d.getUint8(offset+2); + var code = d.getUint16(offset,true); + if (!deviceInfo[device.id]) deviceInfo[device.id]={id:device.id}; + event = deviceInfo[device.id]; + switch (code) { + case 0x1004: event.temperature = d.getInt16(offset+3,true)/10; break; + case 0x1006: event.humidity = d.getInt16(offset+3)/10; break; + case 0x100D: + event.temperature = d.getInt16(offset+3,true)/10; + event.humidity = d.getInt16(offset+5)/10; break; + case 0x1008: event.moisture = d.getUint8(offset+3); break; + case 0x1009: event.fertility = d.getUint16(offset+3,true)/10; break; + // case 0x1007: break; // 3 bytes? got 84,0,0 or 68,0,0 + default: event.code = code; + event.raw = new Uint8Array(d.buffer, offset+3, l); + break; + } + //print(event); + show(event); + } +} + +/* +eg. { + "id": "c4:7c:8d:6a:ac:79 public", + "temperature": 16.6, "code": 4103, + "raw": new Uint8Array([246, 0, 0]), + "moisture": 46, "fertility": 20.8 } +*/ +function show(event) { + g.reset().setFont("6x8"); + var y = 45 + 50*Object.keys(deviceInfo).indexOf(event.id); + + g.drawString(event.id.substr(0,17),0,y); + g.drawImage(getImgHum(),0,y+15); + g.setFont("6x8",2); + var t = (event.moisture===undefined) ? "?" : event.moisture; + g.drawString((t+" ").substr(0,3),35,y+25,true); + g.drawImage(getImgFert(),80,y+15); + t = Math.round(event.fertility) || "?"; + g.drawString((t+" ").substr(0,3), 120, y+25, true); + g.drawImage(getImgTemp(),160,y+15); + t = Math.round(event.temperature) || "?"; + g.drawString((t+" ").substr(0,3), 180, y+25, true); + g.flip(); +} + +g.clear(); +g.setFont("6x8",2).setFontAlign(0,-1).drawString("Scanning...",120,24); + +Bangle.loadWidgets() +Bangle.drawWidgets() + +NRF.setScan(parseDevice, { filters: [{serviceData:{"fe95":{}}}], timeout: 2000 }); diff --git a/apps/miplant/app.png b/apps/miplant/app.png new file mode 100644 index 000000000..d73d3d79a Binary files /dev/null and b/apps/miplant/app.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/rndmclk/ChangeLog b/apps/rndmclk/ChangeLog new file mode 100644 index 000000000..2d387a04b --- /dev/null +++ b/apps/rndmclk/ChangeLog @@ -0,0 +1,2 @@ +0.01: New widget +0.02: Less invasive, change default clock setting instead of directly loading the new clock (no longer breaks Gadgetbridge notifications) diff --git a/apps/rndmclk/README.md b/apps/rndmclk/README.md new file mode 100644 index 000000000..86138e0e7 --- /dev/null +++ b/apps/rndmclk/README.md @@ -0,0 +1,6 @@ +# Summary +Random Clock is a widget that will randomly show one of the installed watch faces each time the LCD is turned on. + +# How it works +Everytime the LCD is turned off, the widget randomly changes the clock. When you long press BTN 3 the next time, +you might (or might not, it's random after all) see another watch face. \ No newline at end of file diff --git a/apps/rndmclk/rndmclk.png b/apps/rndmclk/rndmclk.png new file mode 100644 index 000000000..9519b8d09 Binary files /dev/null and b/apps/rndmclk/rndmclk.png differ diff --git a/apps/rndmclk/widget.js b/apps/rndmclk/widget.js new file mode 100644 index 000000000..566d8eed5 --- /dev/null +++ b/apps/rndmclk/widget.js @@ -0,0 +1,35 @@ +(() => { + let currentClock = ""; + + /** + * Random value between zero (inclusive) and max (exclusive) + * @param {int} max + */ + function getRandomInt(max) { + return Math.floor(Math.random() * Math.floor(max)); + } + + function loadRandomClock() { + // Find available clock apps (same way as in the bootloader) + var clockApps = require("Storage").list(/\.info$/).map(app => require("Storage").readJSON(app, 1) || {}).filter(app => app.type == "clock").sort((a, b) => a.sortorder - b.sortorder); + + if (clockApps && clockApps.length > 0) { + var clockIndex = getRandomInt(clockApps.length); + + // Only update the file if the clock really change to be nice to the FLASH mem + if (clockApps[clockIndex].src != currentClock) { + currentClock = clockApps[clockIndex].src; + settings = require("Storage").readJSON('setting.json', 1); + settings.clock = clockApps[clockIndex].src; + require("Storage").write('setting.json', settings); + } + } + } + + Bangle.on('lcdPower', (on) => { + if (!on) { + loadRandomClock(); + } + }); + +})(); \ No newline at end of file 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/bin/pre-publish.sh b/bin/pre-publish.sh new file mode 100755 index 000000000..ee73968d7 --- /dev/null +++ b/bin/pre-publish.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +cd `dirname $0`/.. +nodejs bin/sanitycheck.js || exit 1 + +echo "Sanity check passed." + +echo "Finding app dates..." + +# Create list of: +# appid,created_time,modified_time +cd apps +for appfolder in *; do + echo "$appfolder,$(git log --follow --format=%ai -- $appfolder | tail -n 1),$(git log --follow --format=%ai -- $appfolder | head -n 1)" ; +done | grep -v _example_ | grep -v unknown.png > ../appdates.csv +cd .. + +echo "Ready to publish" diff --git a/index.html b/index.html index c7c262557..f3ca261b5 100644 --- a/index.html +++ b/index.html @@ -40,6 +40,12 @@ .chip { cursor: pointer; } + .filter-nav { + display: inline-block; + } + .sort-nav { + float: right; + } .tile-content { position: relative; } .link-github { position:absolute; @@ -88,17 +94,26 @@
-
- - - - - - - - +
+
+ + + + + + + + +
+
-
+ +
diff --git a/js/appinfo.js b/js/appinfo.js index b4d0b5426..ad2611d19 100644 --- a/js/appinfo.js +++ b/js/appinfo.js @@ -1,10 +1,13 @@ +if (typeof btoa==="undefined") + function btoa(d) { return Buffer.from(d).toString('base64'); } + // Converts a string into most efficient way to send to Espruino (either json, base64, or compressed base64) function toJS(txt) { var json = JSON.stringify(txt); var b64 = "atob("+JSON.stringify(btoa(json))+")"; var js = b64.length < json.length ? b64 : json; - if (heatshrink) { + if (typeof heatshrink !== "undefined") { var ua = new Uint8Array(txt.length); for (var i=0;i=0) { @@ -4132,12 +4135,12 @@ Object.defineProperty(exports, '__esModule', { value: true }); if (ch=="x" || ch=="X") chRange="0123456789ABCDEFabcdef"; s+=ch; nextCh(); - } + } } while (isIn(chRange,ch) || ch==".") { s+=ch; nextCh(); - } + } } else if (isIn(chQuotes,ch)) { // STRING type = "STRING"; var q = ch; @@ -4507,8 +4510,8 @@ Object.defineProperty(exports, '__esModule', { value: true }); fileLoader.click(); } - /* Save a file with a save file dialog. callback(savedFileName) only called in chrome app case when we knopw the filename*/ - function fileSaveDialog(data, filename, callback) { + // Save a file with a save file dialog + function fileSaveDialog(data, filename) { function errorHandler() { Espruino.Core.Notifications.error("Error Saving", true); } @@ -4524,7 +4527,6 @@ Object.defineProperty(exports, '__esModule', { value: true }); writer.onwriteend = function(e) { writer.onwriteend = function(e) { console.log('FileWriter: complete'); - if (callback) callback(writableFileEntry.name); }; console.log('FileWriter: writing'); writer.write(blob); @@ -4535,8 +4537,10 @@ Object.defineProperty(exports, '__esModule', { value: true }); }, errorHandler); }); } else { + var rawdata = new Uint8Array(data.length); + for (var i=0;i { - const minified = generated.code; - console.log('rollup: '+minified.length+' bytes'); - - // FIXME: needs warnings? - Espruino.Core.Notifications.info('Rollup no errors. Bundling ' + code.length + ' bytes to ' + minified.length + ' bytes'); - callback(minified); - }) - .catch(err => { - console.log('rollup:error', err); - Espruino.Core.Notifications.error("Rollup errors - Bundling failed: " + String(err).trim()); - callback(code); - }); - } Espruino.Core.Modules = { init : init @@ -6436,18 +6433,18 @@ To add a new serial device, you must add an object to This Source Code is subject to the terms of the Mozilla Public License, v2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - + ------------------------------------------------------------------ Try and get any URLS that are from GitHub ------------------------------------------------------------------ **/ "use strict"; (function(){ - + function init() { - Espruino.addProcessor("getURL", getGitHub); + Espruino.addProcessor("getURL", getGitHub); } - + function getGitHub(data, callback) { var match = data.url.match(/^https?:\/\/github.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.*)$/); if (match) { @@ -6457,7 +6454,7 @@ To add a new serial device, you must add an object to branch : match[3], path : match[4] }; - + var url = "https://raw.githubusercontent.com/"+git.owner+"/"+git.repo+"/"+git.branch+"/"+git.path; console.log("Found GitHub", JSON.stringify(git)); callback({url: url}); @@ -6484,6 +6481,7 @@ To add a new serial device, you must add an object to (function(){ if (typeof acorn == "undefined") { console.log("pretokenise: needs acorn, disabling."); + return; } function init() { @@ -6587,11 +6585,12 @@ To add a new serial device, you must add an object to var tp = "?"; if (tk.type.label=="template" || tk.type.label=="string") tp="STRING"; if (tk.type.label=="num") tp="NUMBER"; - if (tk.type.keyword) tp="ID"; + if (tk.type.keyword || tk.type.label=="name") tp="ID"; + if (tp=="?" && tk.start+1==tk.end) tp="CHAR"; return { startIdx : tk.start, endIdx : tk.end, - str : code.substr(tk.start, tk.end), + str : code.substring(tk.start, tk.end), type : tp }; }}; @@ -6802,5 +6801,7 @@ Espruino.transform = function(code, options) { }); }; +if ("undefined"==typeof document) Espruino.init(); if ("undefined"!=typeof module) module.exports = Espruino; + diff --git a/js/index.js b/js/index.js index 7b896b782..031391f52 100644 --- a/js/index.js +++ b/js/index.js @@ -1,5 +1,6 @@ var appJSON = []; // List of apps and info from apps.json var appsInstalled = []; // list of app JSON +var appSortInfo = {}; // list of data to sort by, from appdates.csv { created, modified } var files = []; // list of files on Bangle var DEFAULTSETTINGS = { pretokenise : true, @@ -19,6 +20,19 @@ httpGet("apps.json").then(apps=>{ refreshFilter(); }); +httpGet("appdates.csv").then(csv=>{ + document.querySelector(".sort-nav").classList.remove("hidden"); + csv.split("\n").forEach(line=>{ + var l = line.split(","); + appSortInfo[l[0]] = { + created : Date.parse(l[1]), + modified : Date.parse(l[2]) + }; + }); +}).catch(err=>{ + console.log("No recent.csv - app sort disabled"); +}); + // =========================================== Top Navigation function showChangeLog(appid) { var app = appNameToApp(appid); @@ -182,11 +196,12 @@ function showTab(tabname) { // =========================================== Library -var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value); +var chips = Array.from(document.querySelectorAll('.filter-nav .chip')).map(chip => chip.attributes.filterid.value); var hash = window.location.hash ? window.location.hash.slice(1) : ''; var activeFilter = !!~chips.indexOf(hash) ? hash : ''; -var currentSearch = ''; +var activeSort = ''; +var currentSearch = activeFilter ? '' : hash; function refreshFilter(){ var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); @@ -194,6 +209,12 @@ function refreshFilter(){ if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active'); else filtersContainer.querySelector('.chip[filterid]').classList.add('active'); } +function refreshSort(){ + var sortContainer = document.querySelector("#librarycontainer .sort-nav"); + sortContainer.querySelector('.active').classList.remove('active'); + if(activeSort) sortContainer.querySelector('.chip[sortid="'+activeSort+'"]').classList.add('active'); + else sortContainer.querySelector('.chip[sortid]').classList.add('active'); +} function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; @@ -202,7 +223,7 @@ function refreshLibrary() { if (activeFilter) { if ( activeFilter == "favourites" ) { visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length)); - }else{ + } else { visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); } } @@ -211,6 +232,13 @@ function refreshLibrary() { visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch)); } + if (activeSort) { + visibleApps = visibleApps.slice(); // clone the array so sort doesn't mess with original + if (activeSort=="created" || activeSort=="modified") { + visibleApps = visibleApps.sort((a,b) => appSortInfo[b.id][activeSort] - appSortInfo[a.id][activeSort]); + } else throw new Error("Unknown sort type "+activeSort); + } + panelbody.innerHTML = visibleApps.map((app,idx) => { var appInstalled = appsInstalled.find(a=>a.id==app.id); var version = getVersionInfo(app, appInstalled); @@ -580,12 +608,22 @@ filtersContainer.addEventListener('click', ({ target }) => { }); var librarySearchInput = document.querySelector("#searchform input"); - +librarySearchInput.value = currentSearch; librarySearchInput.addEventListener('input', evt => { currentSearch = evt.target.value.toLowerCase(); refreshLibrary(); }); +var sortContainer = document.querySelector("#librarycontainer .sort-nav"); +sortContainer.addEventListener('click', ({ target }) => { + if (target.classList.contains('active')) return; + + activeSort = target.getAttribute('sortid') || ''; + refreshSort(); + refreshLibrary(); + window.location.hash = activeFilter; +}); + // =========================================== About if (window.location.host=="banglejs.com") { diff --git a/js/utils.js b/js/utils.js index 53eeb1868..2b6a6e4c3 100644 --- a/js/utils.js +++ b/js/utils.js @@ -37,7 +37,10 @@ function httpGet(url) { }); oReq.addEventListener("error", () => reject()); oReq.addEventListener("abort", () => reject()); - oReq.open("GET", url); + oReq.open("GET", url, true); + oReq.onerror = function () { + reject("HTTP Request failed"); + }; oReq.send(); }); }