diff --git a/apps.json b/apps.json index 643d68a8e..58f77e2a4 100644 --- a/apps.json +++ b/apps.json @@ -1283,7 +1283,7 @@ "name": "Numerals Clock", "shortName": "Numerals Clock", "icon": "numerals.png", - "version":"0.04", + "version":"0.05", "description": "A simple big numerals clock", "tags": "numerals,clock", "type":"clock", @@ -1578,6 +1578,44 @@ { "name": "largeclock.json", "url": "largeclock.json", + } + ] + }, + { "id": "smtswch", + "name": "Smart Switch", + "shortName":"Smart Switch", + "icon": "app.png", + "version":"0.01", + "description": "Using EspruinoHub, control your smart devices on and off via Bluetooth Low Energy!", + "tags": "bluetooth,btle,smart,switch", + "type": "app", + "readme": "README.md", + "storage": [ + {"name":"smtswch.app.js","url":"app.js"}, + {"name":"smtswch.img","url":"app-icon.js","evaluate":true}, + {"name":"light-on.img","url":"light-on.js","evaluate":true}, + {"name":"light-off.img","url":"light-off.js","evaluate":true}, + {"name":"switch-on.img","url":"switch-on.js","evaluate":true}, + {"name":"switch-off.img","url":"switch-off.js","evaluate":true} + ] + }, + { + "id": "simpletimer", + "name": "Timer", + "icon": "app.png", + "version": "0.01", + "description": "Simple timer, useful when playing board games or cooking", + "tags": "timer", + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { + "name": "simpletimer.app.js", + "url": "app.js" + }, + { + "name": "simpletimer.img", + "url": "app-icon.js", "evaluate": true } ] diff --git a/apps/numerals/ChangeLog b/apps/numerals/ChangeLog index 927c4ff5f..855442377 100644 --- a/apps/numerals/ChangeLog +++ b/apps/numerals/ChangeLog @@ -2,3 +2,4 @@ 0.02: Use BTN2 for settings menu like other clocks 0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting 0.04: Don't overwrite existing settings on app update +0.05: Fix settings issue diff --git a/apps/numerals/numerals.settings.js b/apps/numerals/numerals.settings.js index 2d388525c..1e97271b6 100644 --- a/apps/numerals/numerals.settings.js +++ b/apps/numerals/numerals.settings.js @@ -11,7 +11,8 @@ updateSettings(); } let numeralsSettings = storage.readJSON('numerals.json',1); - if (!numeralsSettings) resetSettings(); + if (!numeralsSettings) resetSettings(); + if (numeralsSettings.menuButton===undefined) numeralsSettings.menuButton=22; let dm = ["fill","frame"]; let col = ["rnd","r/g","y/w","o/c","b/y"]; let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]]; @@ -30,7 +31,7 @@ onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} }, "Menu button": { - value: 1|btn[numeralsSettings.menuButton], + value: btn.findIndex(e=>e[0]==numeralsSettings.menuButton), min:0,max:4, format: v=>btn[v][1], onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();} diff --git a/apps/simpletimer/ChangeLog b/apps/simpletimer/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/simpletimer/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/simpletimer/README.md b/apps/simpletimer/README.md new file mode 100644 index 000000000..9326e510d --- /dev/null +++ b/apps/simpletimer/README.md @@ -0,0 +1,16 @@ +# Timer + +Simple timer, useful when playing board games or cooking + +## Features + +- When the time is up the timer can be reset to starting time, this is useful e.g. for playing board games +- When the countdown is running the timer cannot be adjusted, this prevents accidental time variations +- When the time is up the starting time is shown, as a reminder of the time elapsed + +## How to use it + +- Tap on minutes to increase them one by one +- Tap on seconds to increase them one by one +- Press BTN3 to reset time to 0 +- Press BTN1 to start the timer or reset to the original time diff --git a/apps/simpletimer/app-icon.js b/apps/simpletimer/app-icon.js new file mode 100644 index 000000000..b55486dd1 --- /dev/null +++ b/apps/simpletimer/app-icon.js @@ -0,0 +1,5 @@ +require("heatshrink").decompress( + atob( + "mEwxH+AH4A/AEsxAAQso1eyrgvDrmrw4skAAQuDAAIHBrYABFsQvMGLYtGAAOAFweA2WrF4gwYFxAwEFwIvBwowFsIub64AB6wJF6wJB1mGMTFbrmsEYoADHAwAC1dhGCoTCmJhBEYoAM2RiFF6VbleBF6QABGAguSw2sgAwnCAdhXYIwBqwvT2WFDwYvP1YZCwMAlYwT1ZgORogZEqwwB1iRhBoYmGlcAYiZgOBgWFDIzCBAALESYIYvMw4ZHGCuHF5aOKeYgABYiCQMBYeyDZLzBAAQwO2QvPDhbzCeqAvbGAQQBlYvqeYIvteYMreJ7vaACbvQJxwAP1YvLGAeHF7uHFxYvDwovdwovPSDusRxgvEwwvbwwvNGAmrds4vGsOyFy+ysIvPSLqNPGDwuT/xyEwySS2QuEF6BgEYYL0Q1ZIEFyIwGMQIxM1ZcFFyYwHreFw+rSwmy1eHwoSGFygxJABwtXeo4upMSQtdGZorjAH4A/AF4A==" + ) +) diff --git a/apps/simpletimer/app.js b/apps/simpletimer/app.js new file mode 100644 index 000000000..dadbdb825 --- /dev/null +++ b/apps/simpletimer/app.js @@ -0,0 +1,151 @@ +let counter = 0; +let setValue = 0; +let counterInterval; +let state; + +const DEBOUNCE = 50; + +function buzzAndBeep() { + return Bangle.buzz(1000, 1) + .then(() => Bangle.beep(200, 3000)) + .then(() => setTimeout(buzzAndBeep, 5000)); +} + +function outOfTime() { + g.clearRect(0, 0, 220, 70); + g.setFontAlign(0, 0); + g.setFont("6x8", 3); + g.drawString("Time UP!", 120, 50); + counter = setValue; + buzzAndBeep(); + setInterval(() => { + g.clearRect(0, 70, 220, 160); + setTimeout(draw, 200); + }, 400); + state = "stopped"; +} + +function draw() { + const minutes = Math.floor(counter / 60); + const seconds = Math.floor(counter % 60); + const seconds2Digits = seconds < 10 ? `0${seconds}` : seconds.toString(); + g.clearRect(0, 70, 220, 160); + g.setFontAlign(0, 0); + g.setFont("6x8", 7); + g.drawString( + `${minutes < 10 ? "0" : ""}${minutes}:${seconds2Digits}`, + 120, + 120 + ); +} + +function countDown() { + if (counter <= 0) { + if (counterInterval) { + clearInterval(counterInterval); + counterInterval = undefined; + } + outOfTime(); + return; + } + + counter--; + draw(); +} + +function clearIntervals() { + clearInterval(); + counterInterval = undefined; +} + +function set(delta) { + if (state === "started") return; + counter += delta; + if (state === "unset") { + state = "set"; + } + draw(); + g.flip(); +} + +function startTimer() { + setValue = counter; + countDown(); + counterInterval = setInterval(countDown, 1000); +} + +// unset -> set -> started -> -> stopped -> set +const stateMap = { + set: () => { + state = "started"; + startTimer(); + }, + started: () => { + reset(setValue); + }, + stopped: () => { + reset(setValue); + } +}; + +function changeState() { + if (stateMap[state]) stateMap[state](); +} + +function drawLabels() { + g.clear(); + g.setFontAlign(-1, 0); + g.setFont("6x8", 7); + g.drawString(`+ +`, 35, 180); + g.setFontAlign(0, 0, 3); + g.setFont("6x8", 1); + g.drawString(`reset (re)start`, 230, 120); +} + +function reset(value) { + clearIntervals(); + counter = value; + setValue = value; + drawLabels(); + draw(); + state = value === 0 ? "unset" : "set"; +} + +function addWatch() { + clearWatch(); + setWatch(changeState, BTN1, { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + }); + setWatch( + () => { + reset(0); + }, + BTN3, + { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + } + ); + setWatch( + () => { + set(60); + }, + BTN4, + { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + } + ); + setWatch(() => set(1), BTN5, { + debounce: DEBOUNCE, + repeat: true, + edge: "falling" + }); +} + +reset(0); +addWatch(); diff --git a/apps/simpletimer/app.png b/apps/simpletimer/app.png new file mode 100644 index 000000000..f593a3a8b Binary files /dev/null and b/apps/simpletimer/app.png differ diff --git a/apps/smtswch/ChangeLog b/apps/smtswch/ChangeLog new file mode 100644 index 000000000..6d3bcf353 --- /dev/null +++ b/apps/smtswch/ChangeLog @@ -0,0 +1 @@ +0.01: New App! See the README.MD for details on how to use it. \ No newline at end of file diff --git a/apps/smtswch/README.md b/apps/smtswch/README.md new file mode 100644 index 000000000..3ac6658c9 --- /dev/null +++ b/apps/smtswch/README.md @@ -0,0 +1,72 @@ +# Smart Switch app for BangleJS + +This app allows you to remotely control devices (or anything else you like!) with: + +* [Bangle.js](https://www.espruino.com/Bangle.js) (Hackable JavaScript Smartwatch) +* [EspruinoHub](https://github.com/espruino/EspruinoHub) (Bluetooth Low Energy -> MQTT bridge) +* [Node-RED](https://nodered.org) (Flow-based programming tool) + +![Demo of Smart Switch app in action](https://raw.githubusercontent.com/wdmtech/BangleApps/add-video/apps/smtswch/demo.gif) + +* Swipe right to turn a device ON +* Swipe left to turn a device OFF +* BTN1 (top-right) - Previous device (page) +* BTN3 (bottom-right) - Next device (page) + +> Currently, devices can only be added/removed/changed by editing them in the app's source code. + +# How to use + +First, you'll need a device that supports BLE. + +Install EspruinoHub following the directions at [https://github.com/espruino/EspruinoHub](https://github.com/espruino/EspruinoHub) +Install [Node-RED](https://nodered.org/docs/getting-started) + +## Example Node-RED flow + +Import the following JSON into Node-RED and configure the MQTT IN node to use your EspruinoHub's MQTT instance (default port is 1883): + +```JSON +[{"id":"87c6f73e.f22038","type":"mqtt in","z":"a256522.ca0b0b","name":"⌚️BangleJS data","topic":"/ble/advertise/ec:5a:c1:a7:fc:91/data","qos":"2","datatype":"auto","broker":"b961407a.91beb","x":860,"y":100,"wires":[["c37809de.3fc538"]]},{"id":"c37809de.3fc538","type":"function","z":"a256522.ca0b0b","name":"Set topic, remove quotes","func":"msg.topic = \"any_topic_here\";\nmsg.payload = msg.payload.replace(/['\"]+/g, \"\")\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":100,"wires":[["9019be89.5b6d5"]]},{"id":"9019be89.5b6d5","type":"debug","z":"a256522.ca0b0b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1250,"y":100,"wires":[]},{"id":"b961407a.91beb","type":"mqtt-broker","z":"","name":"","broker":"192.168.1.22","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"hello_there","birthQos":"0","birthPayload":"","closeTopic":"bye_now","closeQos":"0","closePayload":"true","willTopic":"bye_now","willQos":"0","willPayload":"true"}] +``` + +Replace the topic of the MQTT IN node to use the ID of your Bangle.js device, e.g: + +`/ble/advertise/ec:5a:c1:a7:fc:91/data` + +Once you see the MQTT IN node is configured correctly (it says `connected` below the node itself), try swiping in the Smart Switch app, and +you should see some data in the Debug node. + +The possibilities for switching things on and off via Bangle.js are now endless. Have fun! + +# How it works + +This is the code that does the actual [BLE advertising](https://www.espruino.com/BLE%20Advertising) on the watch itself: + +```JS +NRF.setAdvertising({ + 0xFFFF: [currentPage, page.state] +}); +``` + +# Not working? + +If you can't see any data in Node-RED after swiping, check to see if your device is advertising by visiting port 1888 of your EspruinoHub instance: + +You should see something like the following: + +``` +ec:5a:c1:a7:fc:91 - Bangle.js fc91 (RSSI -83) + ffff => {"data":"1,1"} +``` + +# Any comments? + +[Tweet me!](https://twitter.com/BillyWhizzkid) + +# Future + +PRs welcome! + +[ ] Add an HTML GUI for configuring devices inside the Bangle.js App Loader +[ ] Allow enable/disable of buzz/beep on change of device state diff --git a/apps/smtswch/app-icon.js b/apps/smtswch/app-icon.js new file mode 100644 index 000000000..9153bd3ca --- /dev/null +++ b/apps/smtswch/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA")) \ No newline at end of file diff --git a/apps/smtswch/app.js b/apps/smtswch/app.js new file mode 100644 index 000000000..e8491a065 --- /dev/null +++ b/apps/smtswch/app.js @@ -0,0 +1,79 @@ + +// Learn more! +// https://www.espruino.com/Reference#l_NRF_setAdvertising +// https://www.espruino.com/Bangle.js#buttons + +// Initial graphics setup +g.clear(); +g.setFontAlign(0, 0); // center font +// g.setFont("6x8", 8); // bitmap font, 8x magnified +g.setFont("Vector", 40); // vector font, 80px + +// Let the app begin! +const storage = require("Storage"); + +let currentPage = 0; +let pages = [ + { + name: "Downstairs", + icon: "light", + state: false + }, + { + name: "Upstairs", + icon: "switch", + state: false + }]; + +function loadPage(page) { + const icon = page.state ? page.icon + "-on" : page.icon + "-off"; + Bangle.beep(); + g.clear(); + g.setFont("Vector", 10); + g.drawString("prev", g.getWidth() - 25, 20); + g.drawString("next", g.getWidth() - 25, 220); + g.setFont("Vector", 15); + g.drawString(page.name, g.getWidth() / 2, 200); + g.setFont("Vector", 40); + g.drawString(page.state ? "On" : "Off", g.getWidth() / 2, g.getHeight() / 2); + g.drawImage(storage.read(`${icon}.img`), g.getWidth() / 2 - 24, g.getHeight() / 2 - 24 - 50); +} + +function prevPage() { + if (currentPage > 0) { + currentPage--; + loadPage(pages[currentPage]); + } +} + +function nextPage() { + if (currentPage < pages.length - 1) { + currentPage++; + loadPage(pages[currentPage]); + } +} + +function swipe(dir) { + + const page = pages[currentPage]; + + page.state = dir == 1; + + NRF.setAdvertising({ + 0xFFFF: [currentPage, page.state] + }); + + loadPage(page); + + // optional - this keeps the watch LCD lit up + g.flip(); + + Bangle.buzz(); +} + +Bangle.on('swipe', swipe); + +setWatch(prevPage, BTN, {edge: "rising", debounce: 50, repeat: true}); +setWatch(nextPage, BTN3, {edge: "rising", debounce: 50, repeat: true}); + +loadPage(pages[currentPage]); \ No newline at end of file diff --git a/apps/smtswch/app.png b/apps/smtswch/app.png new file mode 100644 index 000000000..9ed00c6b6 Binary files /dev/null and b/apps/smtswch/app.png differ diff --git a/apps/smtswch/light-off.js b/apps/smtswch/light-off.js new file mode 100644 index 000000000..c6e6b7e77 --- /dev/null +++ b/apps/smtswch/light-off.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGeJAAwttGMotLGMQiD1uzAAWtGEgtE64ACF5IwbFwYtESUouGFpowaFywvXDIS7CFyIwXLwouSF6peF1ovrRqowWF4heEstlApIveDolfAAIEGF76OGFYQuMF6+zdo4uOF6+tF49lFwK9KF7AAJLxovUGBiOhF+IwLF5guWF+AwKF5YuYGBQvKFzQwJF5IucGBAvIFzwwHF44ugF+AwFF4wui/2CABQvrr1YAAIvjrwoDAAwvjFhFeR8onDX/4vcXxIvkYA73BR0gACYA4umMI4uoGAouqAH4AK")) \ No newline at end of file diff --git a/apps/smtswch/light-on.js b/apps/smtswch/light-on.js new file mode 100644 index 000000000..a3e7c322f --- /dev/null +++ b/apps/smtswch/light-on.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AT5gAGFtoxlFpYxhFp4xeFyYwaFyowZF9wuXGC4vuFzIwVF9wdK53OApIwYDRHN6gAC5oFFF8QoC5wyIMRAvZ5wkERgJbCBQqPfEoKGGL4S/j5i3GFwS/jK5BnIF6owMW4S8KFygvKSIQDFF85bBF8QwKF54uUF+AwJF5wuWF+AwIF5ouYGBAvMFzQwHF5YucGAwvKFzwwFF5IugAAOCAA1erAABF0X+rwoDAAwvjFhFeMYIvkE4QAHF8a/vwS+JF8jAHe4KOkGAaQFroumAAUrAAQtpGAgusAH4A/AFI=")) \ No newline at end of file diff --git a/apps/smtswch/switch-off.js b/apps/smtswch/switch-off.js new file mode 100644 index 000000000..58e6e94e6 --- /dev/null +++ b/apps/smtswch/switch-off.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AI1gAEFlgAEz2WAAm6ABwuPxGCAAgJC0wwVGJQtIAAWIGIWXF6gxIEAItIMAgABMCowGFyKSGGCulRhQvHegovVLySRGF6QwBLyjyaF4IuQBAaQX3WmF5wIG0ovXXxaZJYDLuMF8SPHRRCPed4mIcwaNJd7YvBAA4uKH4OXF63+/wuHLxi+YF4JgHLxiOXFwJgHLxmmFwYvXGAqNQFzAwELxKMBdjQwJMAwtCRgovRFpDDIAAjqEFyItLGRQWQAH4A/AH4A/AH4A/AH4A/AH4AP")) \ No newline at end of file diff --git a/apps/smtswch/switch-on.js b/apps/smtswch/switch-on.js new file mode 100644 index 000000000..9153bd3ca --- /dev/null +++ b/apps/smtswch/switch-on.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA")) \ No newline at end of file