From 2c2141c9ec6beef3590c4082399e5dafe5421d53 Mon Sep 17 00:00:00 2001 From: Marc Englund Date: Thu, 6 Jan 2022 21:20:14 +0200 Subject: [PATCH 1/3] Initial version of Ruuvi Watch Added Ruuvi Watch, an app to watch the status of RuuviTags in range. --- apps.json | 13 +++ apps/ruuviwatch/ChangeLog | 1 + apps/ruuviwatch/README.md | 25 +++++ apps/ruuviwatch/ruuviwatch.app-icon.js | 1 + apps/ruuviwatch/ruuviwatch.app.js | 150 +++++++++++++++++++++++++ apps/ruuviwatch/ruuviwatch.png | Bin 0 -> 692 bytes 6 files changed, 190 insertions(+) create mode 100644 apps/ruuviwatch/ChangeLog create mode 100644 apps/ruuviwatch/README.md create mode 100644 apps/ruuviwatch/ruuviwatch.app-icon.js create mode 100644 apps/ruuviwatch/ruuviwatch.app.js create mode 100644 apps/ruuviwatch/ruuviwatch.png diff --git a/apps.json b/apps.json index 833c3505e..15654f26a 100644 --- a/apps.json +++ b/apps.json @@ -5448,5 +5448,18 @@ {"name":"flipper.app.js","url":"flipper.app.js"}, {"name":"flipper.img","url":"flipper.icon.js","evaluate":true} ] + }, + { "id": "ruuviwatch", + "name": "Ruuvi Watch", + "shortName":"Ruuvi Watch", + "icon": "ruuviwatch.png", + "version":"1.00", + "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", + "readme":"README.md", + "tags": "bluetooth", + "storage": [ + {"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"}, + {"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true} + ] } ] diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog new file mode 100644 index 000000000..8a8ec68d5 --- /dev/null +++ b/apps/ruuviwatch/ChangeLog @@ -0,0 +1 @@ +1.00: Hello Ruuvi Watch! \ No newline at end of file diff --git a/apps/ruuviwatch/README.md b/apps/ruuviwatch/README.md new file mode 100644 index 000000000..bf4358267 --- /dev/null +++ b/apps/ruuviwatch/README.md @@ -0,0 +1,25 @@ +# Ruuvi Watch + +Watch the status of [RuuviTags](https://ruuvi.com) in range. + + - Id + - Temperature (°C) + - Humidity (%) + - Pressure (hPa) + - Battery voltage + + Also shows how "fresh" the data is (age of reading). + + ## Usage + + - Scans for devices when launched and every N seconds. + - Page trough devices with BTN1/BTN3. + - Trigger scan with BTN2. + +## Todo / ideas + + - Allow to "name" known devices + - Prevent flicker when updating + - Include more data + - Support older Ruuvi protocols + diff --git a/apps/ruuviwatch/ruuviwatch.app-icon.js b/apps/ruuviwatch/ruuviwatch.app-icon.js new file mode 100644 index 000000000..7ed27ef6c --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA")) \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js new file mode 100644 index 000000000..9f5e03b4c --- /dev/null +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -0,0 +1,150 @@ +require("Storage").write("ruuviwatch.info",{ + "id":"ruuviwatch", + "name":"Ruuvi Watch", + "src":"ruuviwatch.app.js", + "icon":"ruuviwatch.img" + }); + + const lookup = {}; + const ruuvis = []; + let current = 0; + + function int2Hex (str) { + return ('0' + str.toString(16).toUpperCase()).slice(-2); + } + + function p(data) { + const OFFSET = 7; // 0-4 header, 5-6 Ruuvi id + const robject = {}; + robject.version = data[OFFSET]; + + let temperature = (data[OFFSET+1] << 8) | (data[OFFSET+2] & 0xff); + if (temperature > 32767) { + temperature -= 65534; + } + robject.temperature = temperature / 200.0; + + robject.humidity = (((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); + if (accelerationX > 32767) accelerationX -= 65536; // two's complement + robject.accelerationX = accelerationX / 1000.0; + + let accelerationY = (data[OFFSET+9] << 8) | (data[OFFSET+10] & 0xff); + if (accelerationY > 32767) accelerationY -= 65536; // two's complement + robject.accelerationY = accelerationY / 1000.0; + + let accelerationZ = (data[OFFSET+11] << 8) | (data[OFFSET+12] & 0xff); + if (accelerationZ > 32767) accelerationZ -= 65536; // two's complement + robject.accelerationZ = accelerationZ / 1000.0; + + const powerInfo = ((data[OFFSET+13] & 0xff) << 8) | (data[OFFSET+14] & 0xff); + robject.battery = ((powerInfo >>> 5) + 1600) / 1000.0; + robject.txPower = (powerInfo & 0b11111) * 2 - 40; + robject.movementCounter = data[OFFSET+15] & 0xff; + robject.measurementSequenceNumber = ((data[OFFSET+16] & 0xff) << 8) | (data[OFFSET+17] & 0xff); + + robject.mac = [ + int2Hex(data[OFFSET+18]), + int2Hex(data[OFFSET+19]), + int2Hex(data[OFFSET+20]), + int2Hex(data[OFFSET+21]), + int2Hex(data[OFFSET+22]), + int2Hex(data[OFFSET+23]) + ].join(':'); + + robject.name = "Ruuvi " + int2Hex(data[OFFSET+22]) + int2Hex(data[OFFSET+23]); + return robject; + } + + function getAge(created) { + const now = new Date().getTime(); + const ago = ((now - created) / 1000).toFixed(0); + return ago > 0 ? ago + "s ago" : "now"; + } + + function redraw() { + if (ruuvis.length > 0 && ruuvis[current]) { + const ruuvi = ruuvis[current]; + g.clear(); + g.setFontAlign(0,0); + g.setFont("Vector",12); + g.drawString(" (" + (current+1) + "/" + ruuvis.length + ")", g.getWidth()/2, 10); + g.setFont("Vector",20); + g.drawString(ruuvi.name, g.getWidth()/2, 30); + g.setFont("Vector",12); + const age = getAge(ruuvi.time); + if(age > (5*60)) { + g.setColor("#ff0000"); + } else if (age > 60) { + g.setColor("#f39c12"); + } else { + g.setColor("#2ecc71"); + } + g.drawString(age, g.getWidth()/2, 50); + g.setColor("#ffffff"); + g.setFont("Vector",60); + 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() { + NRF.findDevices(function(devices) { + let foundNew = false; + devices.forEach(device => { + const data = p(device.data); + data.time = new Date().getTime(); + const idx = lookup[data.name]; + if (idx !== undefined) { + ruuvis[idx] = data; + } else { + lookup[data.name] = ruuvis.push(data)-1; + foundNew = true; + } + }); + redraw(); + if (foundNew) { + Bangle.buzz(); + g.flip(); + } + + }, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] }); + } + + 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--; + if (current < 0) { + current = ruuvis.length-1; + } + redraw(); + }, BTN1, {repeat:true}); + + setWatch(() => { + scan(); + }, BTN2, {repeat:true}); + + setWatch(() => { + current++; + if (current >= ruuvis.length) { + current = 0; + } + redraw(); + }, BTN3, {repeat:true}); + + scan(); \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.png b/apps/ruuviwatch/ruuviwatch.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5d0954952c77bbe18348240ff7313dcc1e8915 GIT binary patch literal 692 zcmV;l0!#ggP)!VNEuuo9P$>Ll4zvRRj_#*&Ry+IU%mlmf9PGcdKZnENFMS7G;?`N`?20^m``($M zufPX(Heet6Pwj{cc2*`f1=EhsnLZNCRUab>49*^$RhZYr(%**v$3&+M?M(=NjU;X} z^?WfNBb}&eFGAHg#@xgiZ4(maIMG%h&-adY#Obv{ob~HMNZwDQ-wI#5sH`8Y9+CO& zb-_>5{E=`@jgWk$^f!>XfLY|huL3`%8qyMiFDo%2b9X?TG13VxzOH!S7nQp5cREL; zxkUUc@I^eP0&`ni_LhcuK5MWMjhboylpD0A6+qq^mvjjoZg)CKjYoBF<3W4qnG|}1 z%7+oRqy+Pvw}@X8SQOe;<%9RbK9B-zM4|t*a2chBCq_Sc5<)-UImU$cnek~Tqqm+q z5v0R<`iO3w)Bfh<=km00ik<^t=4b{+(3H*rLn#F3E8_Q!S+)4+#xJ z9|`1j!BujC{-kc{vq1b=B$okS{KWW(b#J5Mu9U+ruhX88jOumT*+2I}{X&I8p-?FN abY20-1jwJ!GdeE-0000 Date: Thu, 6 Jan 2022 21:27:40 +0200 Subject: [PATCH 2/3] Add "supports" for Ruuvi Watch Add the "supports" section to apps.json for Ruuvi Watch. --- apps.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps.json b/apps.json index 15654f26a..f86d3bcf8 100644 --- a/apps.json +++ b/apps.json @@ -5457,6 +5457,7 @@ "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", "readme":"README.md", "tags": "bluetooth", + "supports": ["BANGLEJS"], "storage": [ {"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"}, {"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true} From 28bb2f73e0d9293c717f080889d7f8f946c2ebae Mon Sep 17 00:00:00 2001 From: Marc Englund Date: Thu, 6 Jan 2022 21:42:43 +0200 Subject: [PATCH 3/3] Visual fixes Make apploader logo black. Clear gfx before drawing logo on startup. --- apps.json | 2 +- apps/ruuviwatch/ChangeLog | 3 ++- apps/ruuviwatch/ruuviwatch.app.js | 1 + apps/ruuviwatch/ruuviwatch.png | Bin 692 -> 665 bytes 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps.json b/apps.json index f86d3bcf8..6761dd47e 100644 --- a/apps.json +++ b/apps.json @@ -5453,7 +5453,7 @@ "name": "Ruuvi Watch", "shortName":"Ruuvi Watch", "icon": "ruuviwatch.png", - "version":"1.00", + "version":"1.01", "description": "Keep an eye on RuuviTag devices (https://ruuvi.com). Only shows RuuviTags using the v5 format.", "readme":"README.md", "tags": "bluetooth", diff --git a/apps/ruuviwatch/ChangeLog b/apps/ruuviwatch/ChangeLog index 8a8ec68d5..ebde871fa 100644 --- a/apps/ruuviwatch/ChangeLog +++ b/apps/ruuviwatch/ChangeLog @@ -1 +1,2 @@ -1.00: Hello Ruuvi Watch! \ No newline at end of file +1.00: Hello Ruuvi Watch! +1.01: Clear gfx on startup. \ No newline at end of file diff --git a/apps/ruuviwatch/ruuviwatch.app.js b/apps/ruuviwatch/ruuviwatch.app.js index 9f5e03b4c..46218a323 100644 --- a/apps/ruuviwatch/ruuviwatch.app.js +++ b/apps/ruuviwatch/ruuviwatch.app.js @@ -123,6 +123,7 @@ require("Storage").write("ruuviwatch.info",{ }, {timeout : 2000, filters : [{ manufacturerData:{0x0499:{}} }] }); } + g.clear(); g.drawImage(require("Storage").read("ruuviwatch.img"), g.getWidth()/2-24, g.getHeight()/2-24); var drawInterval = setInterval(redraw, 1000); diff --git a/apps/ruuviwatch/ruuviwatch.png b/apps/ruuviwatch/ruuviwatch.png index 3a5d0954952c77bbe18348240ff7313dcc1e8915..3737a7e8cb955bc7747861ed1eba36682f99885e 100644 GIT binary patch delta 590 zcmV-U0TLBDAveCk^&%HVS#^jDn$zM7sUQo?^paMm!hfAnRwqvYoz)hXvrrKB zy8qqh!{AQU(BL!K^}olUtd8IpvBr#SnTcE`HKw6(37GP~!wgi6^8@Ck#vRTD6};O< z8-cCHb!J_)8u<732J|*fszeDo_?6IJfj_8w(1#qoe`-sWh(O^s6Di%n*D_s2iir8e z$y2z^KuqQb3V($A{@!E10-eMnH1Uy`D5a%sAiKP@$H$r&-y(9-8fj4bIHjA_h&a^a z8?r?j4P?O&+ag3KwTUV3n4%x=`%K^zLQ8GTXix_kUlSFg#cc;8*%8hwgfU1D{uJv9ct&i1iuJLB@Up2FPY$LOrU0hs5~|bmrs)46)}C_ znz7k4^VFDu9phYls61%!TW^+sWCLyw8t|=9Ar1}xS>}xCe^p5o{Fj!?VJRR910 delta 617 zcmV-v0+#)m1+)c_R)0xJL_t(|0qvOWZR0QuhM5gAf;Kphpb@-*Z-Zuoc!PKYcZ1#r zz72dgfxAJv!CMJd(7+`n%6311>H&!Si1?DCJ}sg`p-?FNWDc|g0FLgba#lP0<;(=T z@f_^Gvp4rj?S4r63kT} zBMA)79-LK}*TmA_hX2Pzrw#2*2!4$uZZq|KF&-nGsA(@k)i}o7#2IZ966QG3Rv^#! zj&{W9wL+Zr>q1E0Pov)oU%RNRAFUpd`R#SVPt*L7a88Yoe5Ld^khy?aXmWFvgYp@ZGnrZ)( z8?>brK;9acbO{}9cRERpM|E%GL3`+#6ncZohY`1=1oND?h+h*}6xvqhgZINekOFK( zq5rgS8Ks6NMn8EHLO9aumStOSMU;M=Q zh;?tH;;xj#F0a#`kc{ee+Sxz%Lj6L8LZMJ7{B&Lc$OOor(K9+P00000NkvXXu0mjf Dtw1bu