forked from FOSS/BangleApps
add new bluetoothdock app
parent
24c84f9ba6
commit
5fc5b83102
14
apps.json
14
apps.json
|
@ -460,6 +460,20 @@
|
||||||
{"name":"chargeanim.img","url":"app-icon.js","evaluate":true}
|
{"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",
|
{ "id": "widbat",
|
||||||
"name": "Battery Level Widget",
|
"name": "Battery Level Widget",
|
||||||
"icon": "widget.png",
|
"icon": "widget.png",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: New App!
|
|
@ -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)
|
|
@ -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}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwggNK93gEikO93uC6gWBF6ECkQuVkUikAuVAAIuVAAIuGGZgXDlwuDhWkpWqAARHLkQpChWql3kC4YYHmQXDSQWq0Xu8QXE0AWEgYWESQIuC90qlQwJFwoABFwnyGBBdEC4guC1X/GBAXIVYJdC/4wFUw4XFFYX/GApIDC5BJBC4YwEC6QwEC5pHD+YwE0IXMGAX//U/GAgXNU4X60YwEU5YABnQXC0RhEFxkv+YXCl5iBF4gXKLQM6IgIuBGoIXCIxOqlRaBRoIABFwYXBUheqGAIACFwYXKBoYwBFwwXGVoQuDGAguEC4MzCwUQC4UKBwmvFw2qgczmUikAWCC4OikUzAAQvH+YXCCwcAmQVDC4YwFBIIVEgA3BAALADR48zmAWEh4VBPAS/DAIQXKJwIlDd4f6AgQXIIoSPCFwWqC4IFDL4YAFmAXCFIYXB0RhBKQRvDAAa/Dl4oCC4Mv//ya4gWFC4eiLAQUBFwgXBAA8Bc4qnCFwehC5EAC5AuD0AXRFwYXTFweqwAXJPAQXDFwh2JC5AuE0QXKJAouFLxQwGFwhGLPJAuPMI4uQDBAKD"))
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
|
@ -0,0 +1 @@
|
||||||
|
Bangle.on("charging", isCharging => { if (isCharging) load("bluetoothdock.app.js"); });
|
Loading…
Reference in New Issue