Merge branch 'master' into release/largeclock

master
Gordon Williams 2020-05-04 08:32:04 +01:00 committed by GitHub
commit cdfeaf6f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 373 additions and 3 deletions

View File

@ -1283,7 +1283,7 @@
"name": "Numerals Clock", "name": "Numerals Clock",
"shortName": "Numerals Clock", "shortName": "Numerals Clock",
"icon": "numerals.png", "icon": "numerals.png",
"version":"0.04", "version":"0.05",
"description": "A simple big numerals clock", "description": "A simple big numerals clock",
"tags": "numerals,clock", "tags": "numerals,clock",
"type":"clock", "type":"clock",
@ -1578,6 +1578,44 @@
{ {
"name": "largeclock.json", "name": "largeclock.json",
"url": "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 "evaluate": true
} }
] ]

View File

@ -2,3 +2,4 @@
0.02: Use BTN2 for settings menu like other clocks 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.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.04: Don't overwrite existing settings on app update
0.05: Fix settings issue

View File

@ -11,7 +11,8 @@
updateSettings(); updateSettings();
} }
let numeralsSettings = storage.readJSON('numerals.json',1); 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 dm = ["fill","frame"];
let col = ["rnd","r/g","y/w","o/c","b/y"]; let col = ["rnd","r/g","y/w","o/c","b/y"];
let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]]; let btn = [[24,"BTN1"],[22,"BTN2"],[23,"BTN3"],[11,"BTN4"],[16,"BTN5"]];
@ -30,7 +31,7 @@
onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();} onchange: v=> { numeralsSettings.drawMode=dm[v]; updateSettings();}
}, },
"Menu button": { "Menu button": {
value: 1|btn[numeralsSettings.menuButton], value: btn.findIndex(e=>e[0]==numeralsSettings.menuButton),
min:0,max:4, min:0,max:4,
format: v=>btn[v][1], format: v=>btn[v][1],
onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();} onchange: v=> { numeralsSettings.menuButton=btn[v][0]; updateSettings();}

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -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

View File

@ -0,0 +1,5 @@
require("heatshrink").decompress(
atob(
"mEwxH+AH4A/AEsxAAQso1eyrgvDrmrw4skAAQuDAAIHBrYABFsQvMGLYtGAAOAFweA2WrF4gwYFxAwEFwIvBwowFsIub64AB6wJF6wJB1mGMTFbrmsEYoADHAwAC1dhGCoTCmJhBEYoAM2RiFF6VbleBF6QABGAguSw2sgAwnCAdhXYIwBqwvT2WFDwYvP1YZCwMAlYwT1ZgORogZEqwwB1iRhBoYmGlcAYiZgOBgWFDIzCBAALESYIYvMw4ZHGCuHF5aOKeYgABYiCQMBYeyDZLzBAAQwO2QvPDhbzCeqAvbGAQQBlYvqeYIvteYMreJ7vaACbvQJxwAP1YvLGAeHF7uHFxYvDwovdwovPSDusRxgvEwwvbwwvNGAmrds4vGsOyFy+ysIvPSLqNPGDwuT/xyEwySS2QuEF6BgEYYL0Q1ZIEFyIwGMQIxM1ZcFFyYwHreFw+rSwmy1eHwoSGFygxJABwtXeo4upMSQtdGZorjAH4A/AF4A=="
)
)

151
apps/simpletimer/app.js Normal file
View File

@ -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();

BIN
apps/simpletimer/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

1
apps/smtswch/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App! See the README.MD for details on how to use it.

72
apps/smtswch/README.md Normal file
View File

@ -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

1
apps/smtswch/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA"))

79
apps/smtswch/app.js Normal file
View File

@ -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]);

BIN
apps/smtswch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGeJAAwttGMotLGMQiD1uzAAWtGEgtE64ACF5IwbFwYtESUouGFpowaFywvXDIS7CFyIwXLwouSF6peF1ovrRqowWF4heEstlApIveDolfAAIEGF76OGFYQuMF6+zdo4uOF6+tF49lFwK9KF7AAJLxovUGBiOhF+IwLF5guWF+AwKF5YuYGBQvKFzQwJF5IucGBAvIFzwwHF44ugF+AwFF4wui/2CABQvrr1YAAIvjrwoDAAwvjFhFeR8onDX/4vcXxIvkYA73BR0gACYA4umMI4uoGAouqAH4AK"))

1
apps/smtswch/light-on.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AT5gAGFtoxlFpYxhFp4xeFyYwaFyowZF9wuXGC4vuFzIwVF9wdK53OApIwYDRHN6gAC5oFFF8QoC5wyIMRAvZ5wkERgJbCBQqPfEoKGGL4S/j5i3GFwS/jK5BnIF6owMW4S8KFygvKSIQDFF85bBF8QwKF54uUF+AwJF5wuWF+AwIF5ouYGBAvMFzQwHF5YucGAwvKFzwwFF5IugAAOCAA1erAABF0X+rwoDAAwvjFhFeMYIvkE4QAHF8a/vwS+JF8jAHe4KOkGAaQFroumAAUrAAQtpGAgusAH4A/AFI="))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4AI1gAEFlgAEz2WAAm6ABwuPxGCAAgJC0wwVGJQtIAAWIGIWXF6gxIEAItIMAgABMCowGFyKSGGCulRhQvHegovVLySRGF6QwBLyjyaF4IuQBAaQX3WmF5wIG0ovXXxaZJYDLuMF8SPHRRCPed4mIcwaNJd7YvBAA4uKH4OXF63+/wuHLxi+YF4JgHLxiOXFwJgHLxmmFwYvXGAqNQFzAwELxKMBdjQwJMAwtCRgovRFpDDIAAjqEFyItLGRQWQAH4A/AH4A/AH4A/AH4A/AH4AP"))

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/AH4A/AH4Ag1gAECyGFAB1bAAmAFooyQFp4uGEoWIwQAEGBgtQFwtcFpAACxAwJFyIvFEAItIMAowFF1IwFF6zqBRhIvIxBetMAYvWdgJeSAAOHFyQvEw5eRBAeIF6+IF5wIHF66+LTJIvlNBaPfRRAved4g0BAASNJd4f+F61cFQYAEFxQ/Bw4vXYAQAFLxms/wABGC2ALyaOBF7BgGLyAweFyIwTF4jyDLxKMBFw4xTGAhhEFpAuKGKQwFeg4ADFxgAZFlgA/AH4A/AH4A/AH4A/AH4AhA"))