New version, redesigned and refactored

A bit of re-design and refactor before making things configurable.
Removed flicker.
master
Marc Englund 2022-01-25 22:19:34 +02:00
parent 33fb825117
commit bcfb9167bd
5 changed files with 258 additions and 155 deletions

View File

@ -1,2 +1,3 @@
0.01: Hello Ruuvi Watch! 0.01: Hello Ruuvi Watch!
0.02: Clear gfx on startup. 0.02: Clear gfx on startup.
0.03: Improve design and code, reduce flicker.

View File

@ -2,8 +2,10 @@
Watch the status of [RuuviTags](https://ruuvi.com) in range. Watch the status of [RuuviTags](https://ruuvi.com) in range.
![Ruuvi Watch in action](/BangleApps/apps/ruuviwatch/ruuviwatch-in-action.jpg)
- Id - Id
- Temperature (°C) - Temperature (°C)
- Humidity (%) - Humidity (%)
- Pressure (hPa) - Pressure (hPa)
- Battery voltage - Battery voltage
@ -18,8 +20,7 @@ Watch the status of [RuuviTags](https://ruuvi.com) in range.
## Todo / ideas ## Todo / ideas
- Settings for scan frequency, units
- Allow to "name" known devices - Allow to "name" known devices
- Prevent flicker when updating
- Include more data - Include more data
- Support older Ruuvi protocols - Support older Ruuvi protocols

View File

@ -2,8 +2,8 @@
"name": "Ruuvi Watch", "name": "Ruuvi Watch",
"shortName":"Ruuvi Watch", "shortName":"Ruuvi Watch",
"icon": "ruuviwatch.png", "icon": "ruuviwatch.png",
"version":"0.02", "version":"0.03",
"description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). For RuuviTags using the v5 format.",
"readme":"README.md", "readme":"README.md",
"tags": "bluetooth", "tags": "bluetooth",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -1,16 +1,37 @@
require("Storage").write("ruuviwatch.info", { require("Storage").write("ruuviwatch.info", {
"id":"ruuviwatch", id: "ruuviwatch",
"name":"Ruuvi Watch", name: "Ruuvi Watch",
"src":"ruuviwatch.app.js", src: "ruuviwatch.app.js",
"icon":"ruuviwatch.img" icon: "ruuviwatch.img",
}); });
const lookup = {}; const lookup = {};
const ruuvis = []; const ruuvis = [];
let current = 0; let current = 0;
let scanning = false;
const SCAN_FREQ = 1000 * 30;
// Fonts
const FONT_L = "Vector:60";
const FONT_M = "Vector:20";
const FONT_S = "Vector:16";
// "layout"
const CENTER = g.getWidth() / 2;
const MIDDLE = g.getHeight() / 2;
const PAGING_Y = 25;
const NAME_Y = PAGING_Y + 25;
const TEMP_Y = MIDDLE;
const HUMID_PRESSURE_Y = MIDDLE + 50;
const VOLT_Y = g.getHeight() - 15;
const SCANNING_Y = VOLT_Y - 25;
function int2Hex(str) { function int2Hex(str) {
return ('0' + str.toString(16).toUpperCase()).slice(-2); return ("0" + str.toString(16).toUpperCase()).slice(-2);
} }
function p(data) { function p(data) {
@ -24,8 +45,11 @@ require("Storage").write("ruuviwatch.info",{
} }
robject.temperature = temperature / 200.0; robject.temperature = temperature / 200.0;
robject.humidity = (((data[OFFSET+3] & 0xff) << 8) | (data[OFFSET+4] & 0xff)) / 400.0; robject.humidity =
robject.pressure = ((((data[OFFSET+5] & 0xff) << 8) | (data[OFFSET+6] & 0xff)) + 50000) / 100.0; (((data[OFFSET + 3] & 0xff) << 8) | (data[OFFSET + 4] & 0xff)) / 400.0;
robject.pressure =
((((data[OFFSET + 5] & 0xff) << 8) | (data[OFFSET + 6] & 0xff)) + 50000) /
100.0;
let accelerationX = (data[OFFSET + 7] << 8) | (data[OFFSET + 8] & 0xff); let accelerationX = (data[OFFSET + 7] << 8) | (data[OFFSET + 8] & 0xff);
if (accelerationX > 32767) accelerationX -= 65536; // two's complement if (accelerationX > 32767) accelerationX -= 65536; // two's complement
@ -39,11 +63,13 @@ require("Storage").write("ruuviwatch.info",{
if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement
robject.accelerationZ = accelerationZ / 1000.0; robject.accelerationZ = accelerationZ / 1000.0;
const powerInfo = ((data[OFFSET+13] & 0xff) << 8) | (data[OFFSET+14] & 0xff); const powerInfo =
((data[OFFSET + 13] & 0xff) << 8) | (data[OFFSET + 14] & 0xff);
robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0; robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0;
robject.txPower = (powerInfo & 0b11111) * 2 - 40; robject.txPower = (powerInfo & 0b11111) * 2 - 40;
robject.movementCounter = data[OFFSET + 15] & 0xff; robject.movementCounter = data[OFFSET + 15] & 0xff;
robject.measurementSequenceNumber = ((data[OFFSET+16] & 0xff) << 8) | (data[OFFSET+17] & 0xff); robject.measurementSequenceNumber =
((data[OFFSET + 16] & 0xff) << 8) | (data[OFFSET + 17] & 0xff);
robject.mac = [ robject.mac = [
int2Hex(data[OFFSET + 18]), int2Hex(data[OFFSET + 18]),
@ -51,59 +77,111 @@ require("Storage").write("ruuviwatch.info",{
int2Hex(data[OFFSET + 20]), int2Hex(data[OFFSET + 20]),
int2Hex(data[OFFSET + 21]), int2Hex(data[OFFSET + 21]),
int2Hex(data[OFFSET + 22]), int2Hex(data[OFFSET + 22]),
int2Hex(data[OFFSET+23]) int2Hex(data[OFFSET + 23]),
].join(':'); ].join(":");
robject.name = "Ruuvi " + int2Hex(data[OFFSET+22]) + int2Hex(data[OFFSET+23]); robject.name =
"Ruuvi " + int2Hex(data[OFFSET + 22]) + int2Hex(data[OFFSET + 23]);
return robject; return robject;
} }
function getAge(created) { function drawAge() {
const ruuvi = ruuvis[current];
const created = ruuvi.time;
const now = new Date().getTime(); const now = new Date().getTime();
const ago = ((now - created) / 1000).toFixed(0); const agoMs = now - created;
return ago > 0 ? ago + "s ago" : "now"; let age = "";
if (agoMs > SCAN_FREQ) {
const agoS = agoMs / 1000;
// not seen since last scan; indicate age
if (agoS < 60) {
// seconds
age = agoS.toFixed(0) + "s ago";
} else if (agoS < 60 * 60) {
// minutes
age = (agoS / 60).toFixed(0) + "m ago";
} else {
// hours
age = (agoS / 60 / 60).toFixed(0) + "h ago";
}
}
if (agoMs > 5 * SCAN_FREQ) {
// old
g.setColor("#ff0000");
}
g.setFont(FONT_S);
g.drawString(age, CENTER, SCANNING_Y);
}
function redrawAge() {
const originalColor = g.getColor();
g.clearRect(0, SCANNING_Y - 10, g.getWidth(), SCANNING_Y + 10);
g.setFont(FONT_S);
g.setColor("#666666");
if (scanning) {
g.drawString("Scanning...", CENTER, SCANNING_Y);
} else if (ruuvis.length > 0 && ruuvis[current]) {
drawAge();
} else {
g.drawString("No tags in sight", CENTER, SCANNING_Y);
}
g.setColor(originalColor);
} }
function redraw() { function redraw() {
g.clear();
g.setColor("#ffffff");
if (ruuvis.length > 0 && ruuvis[current]) { if (ruuvis.length > 0 && ruuvis[current]) {
const ruuvi = ruuvis[current]; const ruuvi = ruuvis[current];
g.clear();
g.setFontAlign(0,0); // page
g.setFont("Vector",12); g.setFont(FONT_S);
g.drawString(" (" + (current+1) + "/" + ruuvis.length + ")", g.getWidth()/2, 10); g.drawString(
g.setFont("Vector",20); " (" + (current + 1) + "/" + ruuvis.length + ")",
g.drawString(ruuvi.name, g.getWidth()/2, 30); CENTER,
g.setFont("Vector",12); PAGING_Y
const age = getAge(ruuvi.time); );
if(age > (5*60)) {
g.setColor("#ff0000"); // name
} else if (age > 60) { g.setFont(FONT_M);
g.setColor("#f39c12"); g.drawString(ruuvi.name, CENTER, NAME_Y);
// age
redrawAge();
// temp
g.setFont(FONT_L);
g.drawString(ruuvi.temperature.toFixed(2) + "°c", CENTER, TEMP_Y);
// humid & pressure
g.setFont(FONT_M);
g.drawString(
ruuvi.humidity.toFixed(2) + "% " + ruuvi.pressure.toFixed(2) + "hPa ",
CENTER,
HUMID_PRESSURE_Y
);
// battery
g.setFont(FONT_S);
g.drawString(ruuvi.battery + "v", CENTER, VOLT_Y);
} else { } else {
g.setColor("#2ecc71"); // no ruuvis
} g.drawImage(
g.drawString(age, g.getWidth()/2, 50); require("Storage").read("ruuviwatch.img"),
g.setColor("#ffffff"); CENTER - 24,
g.setFont("Vector",60); MIDDLE - 24
g.drawString(ruuvi.temperature.toFixed(2) + "°c", g.getWidth()/2, g.getHeight()/2); );
g.setFontAlign(0,1);
g.setFont("Vector",20);
g.drawString(ruuvi.humidity + "% " + ruuvi.pressure + "hPa ", g.getWidth()/2, g.getHeight()-30);
g.setFont("Vector",12);
g.drawString(ruuvi.battery + "v", g.getWidth()/2, g.getHeight()-10);
} else {
g.clear();
g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24);
g.setFontAlign(0,0);
g.setFont("Vector",16);
g.drawString("Looking for Ruuvi...", g.getWidth()/2, g.getHeight()/2 + 50);
} }
} }
function scan() { function scan() {
NRF.findDevices(function(devices) { if (scanning) return;
scanning = true;
NRF.findDevices(
function (devices) {
let foundNew = false; let foundNew = false;
devices.forEach(device => { devices.forEach((device) => {
const data = p(device.data); const data = p(device.data);
data.time = new Date().getTime(); data.time = new Date().getTime();
const idx = lookup[data.name]; const idx = lookup[data.name];
@ -114,38 +192,61 @@ require("Storage").write("ruuviwatch.info",{
foundNew = true; foundNew = true;
} }
}); });
scanning = false;
redraw(); redraw();
if (foundNew) { if (foundNew) {
Bangle.buzz(); Bangle.buzz();
g.flip(); }
},
{ timeout: 2000, filters: [{ manufacturerData: { 0x0499: {} } }] }
);
} }
}, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] }); // START
} // Button 1 pages up
setWatch(
g.clear(); () => {
g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24);
var drawInterval = setInterval(redraw, 1000);
var scanInterval = setInterval(scan, 10000);
setWatch(() => {
current--; current--;
if (current < 0) { if (current < 0) {
current = ruuvis.length - 1; current = ruuvis.length - 1;
} }
redraw(); redraw();
}, BTN1, {repeat:true}); },
BTN1,
setWatch(() => { { repeat: true }
);
// button triggers scan
setWatch(
() => {
scan(); scan();
}, BTN2, {repeat:true}); },
BTN2,
setWatch(() => { { repeat: true }
);
// button 3 pages down
setWatch(
() => {
current++; current++;
if (current >= ruuvis.length) { if (current >= ruuvis.length) {
current = 0; current = 0;
} }
redraw(); redraw();
}, BTN3, {repeat:true}); },
BTN3,
{ repeat: true }
);
g.setFontAlign(0, 0);
g.clear();
g.drawImage(
require("Storage").read("ruuviwatch.img"),
CENTER - 24,
MIDDLE - 24
);
g.setFont(FONT_M);
g.drawString("Ruuvi Watch", CENTER, HUMID_PRESSURE_Y);
var ageInterval = setInterval(redrawAge, 1000);
var scanInterval = setInterval(scan, SCAN_FREQ);
scan(); scan();