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 000000000..db489f8c1 Binary files /dev/null and b/apps/bluetoothdock/app.png differ diff --git a/apps/bluetoothdock/boot.js b/apps/bluetoothdock/boot.js new file mode 100644 index 000000000..93d5fe63f --- /dev/null +++ b/apps/bluetoothdock/boot.js @@ -0,0 +1 @@ +Bangle.on("charging", isCharging => { if (isCharging) load("bluetoothdock.app.js"); });