From 5fc5b8310217d26dcf5f15939c4d7bff2a34ecbf Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Tue, 17 Nov 2020 12:31:56 +0000 Subject: [PATCH] add new bluetoothdock app --- apps.json | 14 +++ apps/bluetoothdock/ChangeLog | 1 + apps/bluetoothdock/README.md | 35 ++++++ apps/bluetoothdock/add_to_apps.json | 15 +++ apps/bluetoothdock/app-icon.js | 1 + apps/bluetoothdock/app.js | 182 ++++++++++++++++++++++++++++ apps/bluetoothdock/app.png | Bin 0 -> 2604 bytes apps/bluetoothdock/boot.js | 1 + 8 files changed, 249 insertions(+) create mode 100644 apps/bluetoothdock/ChangeLog create mode 100644 apps/bluetoothdock/README.md create mode 100644 apps/bluetoothdock/add_to_apps.json create mode 100644 apps/bluetoothdock/app-icon.js create mode 100644 apps/bluetoothdock/app.js create mode 100644 apps/bluetoothdock/app.png create mode 100644 apps/bluetoothdock/boot.js diff --git a/apps.json b/apps.json index ad5200593..116f1488d 100644 --- a/apps.json +++ b/apps.json @@ -460,6 +460,20 @@ {"name":"chargeanim.img","url":"app-icon.js","evaluate":true} ] }, + { "id": "bluetoothdock", + "name": "Bluetooth Dock", + "shortName":"Dock", + "icon": "app.png", + "version":"0.01", + "description": "When charging shows the time, scans Bluetooth for known devices (eg temperature) and shows them on the screen", + "tags": "bluetooth", + "readme": "README.md", + "storage": [ + {"name":"bluetoothdock.app.js","url":"app.js"}, + {"name":"bluetoothdock.boot.js","url":"boot.js"}, + {"name":"bluetoothdock.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "widbat", "name": "Battery Level Widget", "icon": "widget.png", diff --git a/apps/bluetoothdock/ChangeLog b/apps/bluetoothdock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/bluetoothdock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/bluetoothdock/README.md b/apps/bluetoothdock/README.md new file mode 100644 index 000000000..37d6dd463 --- /dev/null +++ b/apps/bluetoothdock/README.md @@ -0,0 +1,35 @@ +# Charging Dock + +When charging shows the time, scans Bluetooth for known devices (eg temperature) and shows them on the screen. + +Rotates by 90 degrees if it detects it is sideways, allowing for use +in a Charging Dock. + +When devices are out of range (eg low water level in a plant) they are +highlighted red. + +Currently supported devices: + +* Mi Flora/other Xiaomi +* Bluetooth 0x1809 (eg. [Espruino Apps](https://espruino.github.io/EspruinoApps/#bletemp)) +* Espruino Manufacturer Data (0x0590) + +In the future it'd be nice to support more types of device in the future! + +## Espruino Devices + +To use your own Espruino device, use code like the following: + +``` +var data = {a:1,t:E.getTemperature()}; +NRF.setAdvertising({},{ + showName:false, + manufacturer:0x0590, + manufacturerData:JSON.stringify(data) +}); +``` + +Currently: + +* `t` is the temperature (if defined) +* `t` is the alert status (1 or 0) diff --git a/apps/bluetoothdock/add_to_apps.json b/apps/bluetoothdock/add_to_apps.json new file mode 100644 index 000000000..cb59dcdbe --- /dev/null +++ b/apps/bluetoothdock/add_to_apps.json @@ -0,0 +1,15 @@ +// Create an entry in apps.json as follows: +{ "id": "bluetoothdock", + "name": "Bluetooth Dock", + "shortName":"Dock", + "icon": "app.png", + "version":"0.01", + "description": "When charging shows the time, scans Bluetooth for known devices (eg temperature) and shows them on the screen", + "tags": "bluetooth", + "readme": "README.md", + "storage": [ + {"name":"bluetoothdock.app.js","url":"app.js"}, + {"name":"bluetoothdock.boot.js","url":"boot.js"}, + {"name":"bluetoothdock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bluetoothdock/app-icon.js b/apps/bluetoothdock/app-icon.js new file mode 100644 index 000000000..06e21d106 --- /dev/null +++ b/apps/bluetoothdock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwggNK93gEikO93uC6gWBF6ECkQuVkUikAuVAAIuVAAIuGGZgXDlwuDhWkpWqAARHLkQpChWql3kC4YYHmQXDSQWq0Xu8QXE0AWEgYWESQIuC90qlQwJFwoABFwnyGBBdEC4guC1X/GBAXIVYJdC/4wFUw4XFFYX/GApIDC5BJBC4YwEC6QwEC5pHD+YwE0IXMGAX//U/GAgXNU4X60YwEU5YABnQXC0RhEFxkv+YXCl5iBF4gXKLQM6IgIuBGoIXCIxOqlRaBRoIABFwYXBUheqGAIACFwYXKBoYwBFwwXGVoQuDGAguEC4MzCwUQC4UKBwmvFw2qgczmUikAWCC4OikUzAAQvH+YXCCwcAmQVDC4YwFBIIVEgA3BAALADR48zmAWEh4VBPAS/DAIQXKJwIlDd4f6AgQXIIoSPCFwWqC4IFDL4YAFmAXCFIYXB0RhBKQRvDAAa/Dl4oCC4Mv//ya4gWFC4eiLAQUBFwgXBAA8Bc4qnCFwehC5EAC5AuD0AXRFwYXTFweqwAXJPAQXDFwh2JC5AuE0QXKJAouFLxQwGFwhGLPJAuPMI4uQDBAKD")) diff --git a/apps/bluetoothdock/app.js b/apps/bluetoothdock/app.js new file mode 100644 index 000000000..bb0ef4682 --- /dev/null +++ b/apps/bluetoothdock/app.js @@ -0,0 +1,182 @@ +var deviceInfo = {}; +if (Bangle.getAccel().x < -0.7) + g.setRotation(3); // assume watch in charge cradle +// Tile sizes +var TILESIZE = 60; +// Tiles along width of screen +var TILEX = 4; + +// Map devices to nice names... +var deviceNames = { + "eb:44:c1:71:2e:89 random" : "Office", + "c4:7c:8d:6a:ac:79 public" : "Peacelily" +}; + +var scanHandlers = [ + { filter : {serviceData:{"fe95":{}}}, // Xiaomi + handler : function(device) { + if (!device.serviceData["fe95"]) return; + 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; + } + }}}, { + filter : {serviceData:{"1809":{}}}, // Standard Bluetooth + handler : function(device) { + if (!device.serviceData["1809"]) return; + var d = new DataView(device.serviceData["1809"]); + if (!deviceInfo[device.id]) deviceInfo[device.id]={id:device.id,name:device.name}; + event = deviceInfo[device.id]; + event.temperature = d.getInt16(0,1)/100; + }}, { + filter : { manufacturerData:{0x0590:{}} }, // Espruino + handler : function(device) { + if (!device.manufacturerData) return; + var j; + try { j = JSON.parse(E.toString(device.manufacturerData)); } + catch (e) { return; } // not JSON + if (!deviceInfo[device.id]) deviceInfo[device.id]={id:device.id,name:device.name}; + event = deviceInfo[device.id]; + if (j.t) event.temperature = j.t; + if (j.a) event.alert = j.a; + }} +]; + +function getImgHum() { + return require("heatshrink").decompress(atob("jUoxH+AEtlsoYYDS4ZYDAYaVDLAYFDSQYHDSIZYDBIaPDLAYLDRoZYDBoaLDLAYPDRIZYDCIaHDLAYTDQoZYDCoaDDOQYXAA+JxIYX1utDSwYBAAIzYGiwZUTgpODQpzPGGgY3OdI4aRDIIaMDJIYCDIztDGRwaJP5oaWDAwaRDBAbOC5YcKB5I=")); +} +function getImgTemp() { + return require("heatshrink").decompress(atob("iUqxH+AA2sAAQLHCBASMCAoSLCPOBAAQRfI/5Hn3YACy4ACCL4ADCL5H/I/AQHCRAQJCQwQLCQgQNCQYRQCB4A/ADaPjYqTpSCRYQGCZALFA")); +} + +function drawAlert(tile,x,y) { + g.setFont("Vector",56).setFontAlign(0,0); + g.drawString("!",x+TILESIZE/2,y+10+TILESIZE/2); +} + +function drawMoisture(tile,x,y) { + g.drawImage(getImgHum(),x+2,y+18); + g.setFont("Vector",28); + g.drawString(tile.device.moisture,x+26,y+12); +} + +function drawTemperature(tile,x,y) { + g.drawImage(getImgTemp(),x+3,y+16); + g.setFont("Vector",30); + var t = Math.round(tile.device.temperature); + g.drawString(t,x+25,y+13); +} + +function getTiles() { + var tiles = []; + Object.keys(deviceInfo).forEach(id=>{ + var dev = deviceInfo[id]; + if (dev.alert) { + tiles.push({ + alert: true, device: dev, + draw: drawAlert + }); + } + if (dev.moisture && dev.moisture<40) { + tiles.push({ + alert: true, device: dev, + draw: drawMoisture + }); + } + if (dev.temperature) { + tiles.push({ + device: dev, + draw: drawTemperature + }); + } + }); + tiles.sort((a,b)=>(b.alert|0)-(a.alert|0)) + return tiles; +} + + +g.clear(); +require("Font7x11Numeric7Seg").add(Graphics); +function drawClock() { + var d = new Date(); + var size = 3; + var x = (g.getWidth()/2) - size*6, + y = size; + g.reset(); + g.setFont("7x11Numeric7Seg",size).setFontAlign(1,-1); + g.drawString(d.getHours(), x, y, true); + g.setFontAlign(-1,-1); + if (d.getSeconds()&1) g.drawString(":", x,y); + g.drawString(("0"+d.getMinutes()).substr(-2),x+size*4,y, true); + // draw seconds + g.setFont("7x11Numeric7Seg",size/2); + g.drawString(("0"+d.getSeconds()).substr(-2),x+size*18,y + size*7, true); + // date + var s = d.toString().split(" ").slice(0,4).join(" "); + g.reset().setFontAlign(0,-1); + g.drawString(s,g.getWidth()/2, y + size*12, true); + // keep screen on + g.flip(); +} +function drawTiles() { + // draw tiles + var tiles = getTiles(); + for (var i=0;i<6;i++) { + var x = (i%TILEX)*TILESIZE; + var y = TILESIZE + TILESIZE*((i/TILEX)|0); + g.reset(); + var tile = tiles[i]; + if (tile && tile.alert) { + g.setBgColor(0.5,0,0); + } + g.clearRect(x,y,x+TILESIZE-1,y+TILESIZE-1); + if (tile) { + g.reset().setFont("6x8"); + var t = deviceNames[tile.device.id]; + if (!t) t = tile.device.name || tile.device.id.substr(0,17); + g.drawString(t,x+2,y+2); + tile.draw(tile, x, y); + if (tile.alert) { + g.setColor(1,1,0); + g.drawRect(x,y,x+TILESIZE-1,y+TILESIZE-1); + } + } + } + g.flip(); // keep forcing display on +} + +setInterval(drawClock, 1000); +setInterval(drawTiles, 10000); +drawClock(); +drawTiles(); + +function parseDevice(dev) { + if (!dev.serviceData) dev.serviceData={}; + scanHandlers.forEach(s=>s.handler(dev)); +} +NRF.setScan(parseDevice, { filters: scanHandlers.map(s=>s.filter), timeout: 2000 }); + +if (Bangle.isCharging()) { + Bangle.on("charging", isCharging => { + if (!isCharging) load(); + }); +} diff --git a/apps/bluetoothdock/app.png b/apps/bluetoothdock/app.png new file mode 100644 index 0000000000000000000000000000000000000000..db489f8c1d4737ff0b223f12880ee8900834c486 GIT binary patch literal 2604 zcmV+{3e)w8P)lD7X~|yDi{C&3Asr` zK~!ko)tPy8RMj2FKljbNWD+1`LP$kcNkT#pf)KG#o1mPw1Y;%2B4v?)+ExoL&;>XL z2dH3=TDQ`gh=)aJ5>iY637{f@y0Iv6iDm^OXoc(xS!Q{+e@rqYVV0Lj>gn&VnLF>@ z_x;@8a)0j*^peSArlm5^lrE2Aq!+DMCYO1p2SBDG5GW=oWy=B~(Ecv~yJi`Na%LHslTiHx<|Y;U(i$m6E_e+coq0PcLy66hb$y1!k#YsHbVHkCOa<#KLi zO-+1ZNtyI=&!?*DH!b~HOJ%23Pd<<&VFzHI7xy~9zI7)64o%^yH<$5NVS=7Dtd@$h z@c5p@+-~JSyLccz)NCd?GF%9O^lsUdh|skC@yWUK zb$g(r_>SY1H8m8KmLWZ0Ncd8*+itJ;&+QQs?I>VeG%1tzu(4|j8xK9gJH_fsLzmLP%~rS zLsQsvco}aOc~01Nh?{O4priJS6^V(t{$7(r5Kpa+BX&%Px67wvu_)gwgt|gRhldN{p)jjzGr>L}w(l-s*Pb#bgvr3B z)p6VyZT57zIsTKlD31VEyLORo+JF#eiS@M88PsM1`hDwlj#qXSk&$y2fWQDF&#aw9 zY>X#QC^x$WxclKX!%yYsFRZGrJ=0x?P_vnsh;X+X-GrDZQtp|K@X^2=c>6q7o96Pw zldC4uKgiEro{qW-VDVy$NincTN;&k)($c8Pn%Yl#;)$5Za5{F(6JnxRxnv%*lEQg7 zCC1~uVWB3TUONdtRcUX#*|Mvcx_YadFqe^60W=0#H-ntm_BF7uq%^X!rZ&I35O!M# zjn$)&QebU1Gbnfxs;W9$^rR6yw0QK@K^^SRvrpW@bzuQ6@h@&EAm>oIr#u~O=r}NY z`I-lTwO#YCvbwr&P~X4;lgTtlkNArh1s{H(Vr%V#t+hW*jn|X^c`bgb!r0NycpejF zMo~nkN;7$47{iA2?Ib??V7a$}6{vNg;Q8cyaN&R8<|+Cou5KzyQ;Adcx^`&T zz|D%mP=Ju<1){30SuTh>5BiZ?mCX$HutfVx!<|_JvB8|eed1D)lv$7Ur$ZjSp))pl^pK&xAg3@dQ;8M zwNwQK1$}E;9ecNB9~fI(*D$bCt_b;3NORT7w6rvvx7iYs57+^NuG0iXu>Hk&hF;&N zlXo6};zW09h11ss1qFSr2LFS=tt~Cq%ICK2x}a%tJkWp;OaJx4lUaIZOGtjB0Yu*b zlftkeCWa0+F>s(C1I#Kx{fqrWMo-)ADUlSmLicGfWZI`hvdSyEtFqq;Zj8_Rn>Ot z8|*YSIaoR`n#5^CFeu)Sq|&p`lKPrYh+?A<@^?~#M6T?vc&GPDw@Bm?fD7NWT+Vy9 zy3c1{G}3DA+)D@nk3MiCNk1Fr8rzmx!s5A+ox0JKd!66sC6A=l>R?-DiPsQPq!eQT z46RKW-4w6n7nN=nh$TRy6wG_(Z{;7K&Tnul{*ed9aM$!FO7tKT7nfaUj(VdnJV zuCa_9&*8D9LpKVLa&IJyW=DA1i7y(3l&>G^=PTRSlg$lo-cHNAAuqDiu=lS6~WKOi>KrFtn9?J17P~ldgj#0WDJSs-nmZ zv5}D%I;==>S24Lq&U4#O1`#tVn9P0We2VYdjn>v%dE9c+Z8xexu@PW=R7B?c=g;pm zHMPtVQX~NrL7GAF2iXGDA~}cPj1YWeH;O}X;o)^xZ%&4jn&#*oZ)R%P%xC zysaCpt+(>{6DK%Z-r(uhnq#r2LX~R_pb5$0p5ERrzM;{^A2yuwCLV#z#bVK}EdZb? zBK!N?-+0-^ighO`F01o?s3M)ux#j>>hQ@4ko+-H%Z}BZ2_^v+eNBepOpdf3G70C-d z5x;WXNs4@n2PvNWC@s#`D*ymi_44z;SKSp~SX!s&^0Oj1;6+`3aP-tZ$ePov2(iGq zi+DF@nLVXfyiICyVcxdeT73S&aV;!9Id{H5?(nuy(GmUiigzGccr53R^e*j0?^^sl z2WJRPdkyD5kv%j~C6Ej9vhO(L=5VWb?f9O=T+Jw=1#AYIx~H4VNb!=eJEGl*|DSlE zqfMQe9W3!nL=jVwj74Yzks<^zgQx~sE#Z=Y6KEo@#mcc02WQlKoAbY6`Wp46%XWnT O0000 { if (isCharging) load("bluetoothdock.app.js"); });