mirror of https://github.com/espruino/BangleApps
Merge remote-tracking branch 'upstream/master' into jekyll-apps.json
commit
740f1fdb71
|
@ -1 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Make overriding the HRM event optional
|
||||
Emit BTHRM event for external sensor
|
||||
Add recorder app plugin
|
||||
|
|
|
@ -2,24 +2,43 @@
|
|||
var log = function() {};//print
|
||||
var gatt;
|
||||
var status;
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
|
||||
var origIsHRMOn = Bangle.isHRMOn;
|
||||
|
||||
Bangle.isBTHRMOn = function(){
|
||||
return (status=="searching" || status=="connecting") || (gatt!==undefined);
|
||||
}
|
||||
Bangle.setHRMPower = function(isOn, app) {
|
||||
|
||||
Bangle.isHRMOn = function() {
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
|
||||
print(settings);
|
||||
if (settings.enabled && !settings.replace){
|
||||
return origIsHRMOn();
|
||||
} else if (settings.enabled && settings.replace){
|
||||
return Bangle.isBTHRMOn();
|
||||
}
|
||||
return origIsHRMOn() || Bangle.isBTHRMOn();
|
||||
}
|
||||
|
||||
Bangle.setBTHRMPower = function(isOn, app) {
|
||||
|
||||
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
|
||||
// Do app power handling
|
||||
if (!app) app="?";
|
||||
log("setHRMPower ->", isOn, app);
|
||||
log("setBTHRMPower ->", isOn, app);
|
||||
if (Bangle._PWR===undefined) Bangle._PWR={};
|
||||
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[];
|
||||
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app);
|
||||
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app);
|
||||
isOn = Bangle._PWR.HRM.length;
|
||||
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
|
||||
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
|
||||
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
|
||||
isOn = Bangle._PWR.BTHRM.length;
|
||||
// so now we know if we're really on
|
||||
if (isOn) {
|
||||
log("setHRMPower on", app);
|
||||
if (!Bangle.isHRMOn()) {
|
||||
log("HRM not already on");
|
||||
log("setBTHRMPower on", app);
|
||||
if (!Bangle.isBTHRMOn()) {
|
||||
log("BTHRM not already on");
|
||||
status = "searching";
|
||||
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
|
||||
log("Found device "+device.id);
|
||||
|
@ -49,7 +68,11 @@
|
|||
if (flags&16) {
|
||||
var interval = dv.getUint16(idx,1); // in milliseconds
|
||||
}*/
|
||||
Bangle.emit('HRM',{
|
||||
|
||||
|
||||
var eventName = settings.replace ? "HRM" : "BTHRM";
|
||||
|
||||
Bangle.emit(eventName, {
|
||||
bpm:bpm,
|
||||
confidence:100
|
||||
});
|
||||
|
@ -65,15 +88,27 @@
|
|||
});
|
||||
}
|
||||
} else { // not on
|
||||
log("setHRMPower off", app);
|
||||
log("setBTHRMPower off", app);
|
||||
if (gatt) {
|
||||
log("HRM connected - disconnecting");
|
||||
log("BTHRM connected - disconnecting");
|
||||
status = undefined;
|
||||
try {gatt.disconnect();}catch(e) {
|
||||
log("HRM disconnect error", e);
|
||||
log("BTHRM disconnect error", e);
|
||||
}
|
||||
gatt = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
Bangle.setHRMPower = function(isOn, app) {
|
||||
var settings = require('Storage').readJSON("bthrm.json", true) || {};
|
||||
if (settings.enabled || !isOn){
|
||||
Bangle.setBTHRMPower(isOn, app);
|
||||
}
|
||||
if (settings.enabled && !settings.replace || !isOn){
|
||||
origSetHRMPower(isOn, app);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
var btm = g.getHeight()-1;
|
||||
var eventInt = null;
|
||||
var eventBt = null;
|
||||
var counterInt = 0;
|
||||
var counterBt = 0;
|
||||
|
||||
|
||||
function draw(y, event, type, counter) {
|
||||
var px = g.getWidth()/2;
|
||||
g.reset();
|
||||
g.setFontAlign(0,0);
|
||||
g.clearRect(0,y,g.getWidth(),y+80);
|
||||
if (type == null || event == null || counter == 0) return;
|
||||
var str = event.bpm + "";
|
||||
g.setFontVector(40).drawString(str,px,y+20);
|
||||
str = "Confidence: " + event.confidence;
|
||||
g.setFontVector(12).drawString(str,px,y+50);
|
||||
str = "Event: " + type;
|
||||
g.setFontVector(12).drawString(str,px,y+60);
|
||||
}
|
||||
|
||||
function onBtHrm(e) {
|
||||
print("Event for BT " + JSON.stringify(e));
|
||||
counterBt += 5;
|
||||
eventBt = e;
|
||||
}
|
||||
|
||||
function onHrm(e) {
|
||||
print("Event for Int " + JSON.stringify(e));
|
||||
counterInt += 5;
|
||||
eventInt = e;
|
||||
}
|
||||
|
||||
Bangle.on('BTHRM', onBtHrm);
|
||||
Bangle.on('HRM', onHrm);
|
||||
|
||||
Bangle.setHRMPower(1,'bthrm')
|
||||
Bangle.setBTHRMPower(1,'bthrm')
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
g.reset().setFont("6x8",2).setFontAlign(0,0);
|
||||
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
|
||||
|
||||
function drawInt(){
|
||||
counterInt--;
|
||||
if (counterInt < 0) counterInt = 0;
|
||||
if (counterInt > 5) counterInt = 5;
|
||||
draw(24, eventInt, "HRM", counterInt);
|
||||
}
|
||||
function drawBt(){
|
||||
counterBt--;
|
||||
if (counterBt < 0) counterBt = 0;
|
||||
if (counterBt > 5) counterBt = 5;
|
||||
draw(100, eventBt, "BTHRM", counterBt);
|
||||
}
|
||||
|
||||
var interval = setInterval(drawInt, 1000);
|
||||
var interval = setInterval(drawBt, 1000);
|
|
@ -2,15 +2,18 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "boot",
|
||||
"type": "app",
|
||||
"tags": "health,bluetooth",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"bthrm.app.js","url":"bthrm.js"},
|
||||
{"name":"bthrm.recorder.js","url":"recorder.js"},
|
||||
{"name":"bthrm.boot.js","url":"boot.js"},
|
||||
{"name":"bthrm.img","url":"app-icon.js","evaluate":true}
|
||||
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"bthrm.settings.js","url":"settings.js"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
(function(recorders) {
|
||||
recorders.bthrm = function() {
|
||||
var bpm = 0;
|
||||
function onHRM(h) {
|
||||
bpm = h.bpm;
|
||||
}
|
||||
return {
|
||||
name : "BTHR",
|
||||
fields : ["BT Heartrate"],
|
||||
getValues : () => {
|
||||
result = [bpm];
|
||||
bpm = 0;
|
||||
return result;
|
||||
},
|
||||
start : () => {
|
||||
Bangle.on('BTHRM', onHRM);
|
||||
Bangle.setBTHRMPower(1,"recorder");
|
||||
},
|
||||
stop : () => {
|
||||
Bangle.removeListener('BTHRM', onHRM);
|
||||
Bangle.setBTHRMPower(0,"recorder");
|
||||
},
|
||||
draw : (x,y) => g.setColor(Bangle.isBTHRMOn()?"#00f":"#88f").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
|
||||
};
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
(function(back) {
|
||||
var FILE = "bthrm.json";
|
||||
|
||||
var settings = Object.assign({
|
||||
enabled: true,
|
||||
replace: true,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Bluetooth HRM' },
|
||||
'< Back': back,
|
||||
'Use BT HRM': {
|
||||
value: !!settings.enabled,
|
||||
format: v => settings.enabled ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.enabled = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Use HRM event': {
|
||||
value: !!settings.replace,
|
||||
format: v => settings.replace ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.replace = v;
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
|
@ -1,2 +1,3 @@
|
|||
0.00: Initial check-in.
|
||||
0.01: Add tap-to-decorate feature. Bugfixes.
|
||||
0.02: Add autorotate while charging. Remove autolight. Tweak fonts. Add some haptic feedback on touchscreen operations.
|
||||
|
|
|
@ -20,8 +20,7 @@ you can quickly alter the number of ‘hands’ on the display. When the watch i
|
|||
or down to remove the distraction. There's also a setting that displays the second hand, but only if the watch is perfectly face-to-the-sky,
|
||||
in case you want the ability to check the _exact_ time, hands free, without the impact on battery life this usually entails.
|
||||
|
||||
In some versions of the Bangle.js firmware, the backlight doesn't come on automatically when you twist your wrist. There's currently a workaround
|
||||
for this integrated into the watchface; you can disable it in the menu, if you prefer.
|
||||
While charging the main display automatically rotates so that noon is up. This can be disabled.
|
||||
|
||||
## Limitations
|
||||
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
//
|
||||
// This only works for Bangle 2.
|
||||
|
||||
const isString = x => typeof x === 'string';
|
||||
const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
|
||||
const isString = x => typeof x === 'string',
|
||||
imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/* System integration */
|
||||
|
@ -115,9 +115,9 @@ class RoundOptions extends Options {
|
|||
onchange: x => this.calendric = x,
|
||||
format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x],
|
||||
},
|
||||
'Auto-Illum.': {
|
||||
init: _ => this.autolight,
|
||||
onchange: x => this.autolight = x
|
||||
'Autorotate': {
|
||||
init: _ => this.autorotate,
|
||||
onchange: x => this.autorotate = x
|
||||
},
|
||||
Defaults: _ => {this.reset(); this.interact();}
|
||||
});
|
||||
|
@ -133,7 +133,7 @@ RoundOptions.defaults = {
|
|||
calendric: 5,
|
||||
dayFg: '#fff',
|
||||
nightFg: '#000',
|
||||
autolight: true,
|
||||
autorotate: true,
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -144,29 +144,29 @@ const dec = x => E.toString(heatshrink.decompress(atob(x)));
|
|||
const y10F = [
|
||||
dec(
|
||||
'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' +
|
||||
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' +
|
||||
'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' +
|
||||
'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' +
|
||||
'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' +
|
||||
'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' +
|
||||
'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' +
|
||||
'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' +
|
||||
'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' +
|
||||
'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA='
|
||||
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUcgECCIZlBAY' +
|
||||
'N4CoRUBIoMP8AZBge8MoMB8+B8B4B+E/gf4jw/B/kD4ADBEQMPSYXgoAfBnEwgeA' +
|
||||
'hw7BvEDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMG4ReBn4zBJYKcDH' +
|
||||
'4IABv+AXoSGCv0eAYP/FIMB/4iBTAIJBGIJ6B/yQCb4RDBEQTlBHoIOBn51BwC+B' +
|
||||
'MoWDAwKYBRgKYBCYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+BAY' +
|
||||
'JyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwZgPwnhxBGQJxBGQK5BGQKWDOwUACALlBI' +
|
||||
'YRrB8H///gnI+COwJGBgaUBWgqVDhgDCZYIADFIKAB84eBIwImBXwP8MoPwviYCI' +
|
||||
'AKYBIAKYB4JlCPwJlBS4IdC/IeBFwJlCh6XCY4Q2BLYMIDQN8PIPwg+B4B2B8FwG' +
|
||||
'oN4TgPAnk+MoM+v6tBGQOAZQJ3CQwUAA'
|
||||
), 48, dec('hgAI'), 34
|
||||
];const y1F = [
|
||||
dec(
|
||||
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' +
|
||||
'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' +
|
||||
'+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' +
|
||||
'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' +
|
||||
'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' +
|
||||
'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' +
|
||||
'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' +
|
||||
'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' +
|
||||
'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' +
|
||||
'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' +
|
||||
'BCQQUAA='
|
||||
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnEAgQ' +
|
||||
'FCjkAgwFCh0Ahg1EBoIABgeAFIf/4A1DFQIED/5MDGAYADEQYwDRwgMDhAYEH4Nw' +
|
||||
'AoUeAok/QYl/wAFD/fAHgUD+PgvAFBj/g+E/4EBLAN4j5SCgE8h4EB/AwCAoOAVA' +
|
||||
'PgggeBFoPgQgRLB8E8I4fgXQS/B8KwBMgOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQ' +
|
||||
'JIB/DMDMoJSBboQVBKoIDBSYZOBAAQlCAATpEg/4Xwc/QIZyBwBcBgf//gxBa4Qb' +
|
||||
'Ba4LZDv/4LwRfCGAcBGAYABC4IYCD4QjCR4IFDR4R6BR4QFDMAIFDF4IFBC4IIBA' +
|
||||
'oLEBBYQlBI4IFDR4ZrBR4QFBTgJMDHoaaCdQSmCC4SyCYYJJB+CHBj+Aj8ASYJNB' +
|
||||
'BINwIIOAM4ILDAYN/wABBB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBA' +
|
||||
'gaNCYwgFEbAkAwAFEc4SPCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh' +
|
||||
'4FDMoQ1DK4ZBBMQIDBJYbWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwR' +
|
||||
'CvEBF4IdB+E/OIp9CJgZBCQQUAA='
|
||||
), 48, dec('hgAI'), 48
|
||||
];const y10sF = [
|
||||
dec(
|
||||
|
@ -194,20 +194,20 @@ const y10F = [
|
|||
];const d1F = [
|
||||
dec(
|
||||
'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' +
|
||||
'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' +
|
||||
'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' +
|
||||
'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' +
|
||||
'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' +
|
||||
'4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' +
|
||||
'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' +
|
||||
'4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' +
|
||||
'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' +
|
||||
'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' +
|
||||
'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' +
|
||||
'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' +
|
||||
'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' +
|
||||
'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' +
|
||||
'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA=='
|
||||
'oUHAowRFDoopFGopBFJopZGBgIKCAB5BBgA1CAoMBAokDCIgTCAYRTDAoI6CHgU/' +
|
||||
'Aol/Aog1GAqgAIhgCBn4CBvjZCLIKMBPIJZBcIMB+4lBMoMD84rDg/HL4cPw4FDj' +
|
||||
'5rEnwFEvgFE/AFBaYMB+CJCwED8AFC8EP4CbC/F/wCnC//+H4bbCAoQWBO594EAI' +
|
||||
'TBgBrCAQTtBPQUD4EQQwRHBuEAjwuBCQRdBjxOBLoU8j47DvF/Aofz/IFCgPv4IF' +
|
||||
'Cgf/EYUPg43BFIUPIYIBBjwnCH4N8ZgT9B/jPCj//+AUBE4P/MoQANnwFETgIACg' +
|
||||
'YFEh/uAod/xwQEagQ6B/AFDHIIFCg4hBAoV/JIIFBaAJaDFQgFKFIYFZABN/JAM4' +
|
||||
'KYSvBjhICAoLjBd4IPBEgLvBvDjCAIL1BboITBAoc8AogVBAoZ2BDoTnCFIQuBDI' +
|
||||
'UeFwIlCnguBGIV4BAIhB4PwCgJJB//gEwJbCwDvCM4LuPC4TjD/4cE//4Fwh/BcY' +
|
||||
'K9BIAX/96DCegIJBQYXwTIJxBn+AAoarBAoUBFYIFCgZ9BDoR3CE4LSDE4I1CCoJ' +
|
||||
'BDJARNCZoRZDHAQDBDQYABSIQACSIYABDQjACFobABHIaMDIoQADFwSFCFwIEBAo' +
|
||||
'N4g4EBAoPwUAIABgPnUonfQgIuC/41Dh5tDEwJGEn46EvhdJACCJDv6VDAYPDJIS' +
|
||||
'lB86tCg+B+7HDDALHEnzHEPILpBcILvDAooRFDoopFGopBFJopZFMowAFZgs/VAI' +
|
||||
'7BgAbCAoMB/InBAoMD8CkBAoWALIIgBeoJ7DLYYHDPYZbCNoQLDEoYNBGIQjBgI9' +
|
||||
'BF4Q4BHYcPJ4JHC/5ZC///fwc/OwkPCAQA=='
|
||||
), 48, dec('ikPigAGA'), 48
|
||||
];const dowF = [
|
||||
dec(
|
||||
|
@ -220,10 +220,10 @@ const y10F = [
|
|||
'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' +
|
||||
'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' +
|
||||
'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' +
|
||||
'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' +
|
||||
'//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' +
|
||||
'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' +
|
||||
'p/zC4n5EYj1BAoc//4RDU4IFDA=='
|
||||
'BDAoJsDAo7vhABZuBQYoFDv4FEYgpjDZRgFYGYYpHGoqxDAAMEAokDdwQbBh//DY' +
|
||||
'cf/+ALoU+g/AbIV4gLfDDILrDIIIFD8ARCAoYdBAoodB8EDAoIvBAoI1BHYPwAoQ' +
|
||||
'nBuARBEoVwDoJDCnEB+E/KoMYXYP+Wgd//DGDh4WBgEZMoQABnPnAodz4YQCgHjw' +
|
||||
'IFP+YXE/IjEeoIFDn//CIanBAoY='
|
||||
), 48, dec('kElkMljsljw='), 48
|
||||
];const mF = [
|
||||
dec(
|
||||
|
@ -322,21 +322,20 @@ class Round {
|
|||
this.r = this.xc - this.minR;
|
||||
}
|
||||
|
||||
reset(clear) {this.state = {}; clear && this.g.clear(true);}
|
||||
reset(clear) {this.state = {}; clear == null || this.g.clear(true).setRotation(clear);}
|
||||
|
||||
doIcons(which) {
|
||||
this.state[which] = null;
|
||||
this.render(new Date()); // Not quite right, I think.
|
||||
}
|
||||
|
||||
enhanceUntil(t) {this.enhance = t;}
|
||||
|
||||
pie(f, a0, a1, invert) {
|
||||
if (!invert) return this.pie(f, a1, a0 + 1, true);
|
||||
let t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI);
|
||||
const t0 = Math.tan(a0 * 2 * Math.PI), t1 = Math.tan(a1 * 2 * Math.PI);
|
||||
let i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5);
|
||||
let x = f.getWidth() / 2, y = f.getHeight() / 2;
|
||||
let poly = [
|
||||
const x = f.getWidth() / 2, y = f.getHeight() / 2;
|
||||
const poly = [
|
||||
x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1),
|
||||
y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1),
|
||||
x,
|
||||
|
@ -348,16 +347,17 @@ class Round {
|
|||
for (i0++; i0 <= i1; i0++) poly.push(
|
||||
3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0
|
||||
);
|
||||
f.setColor(0).fillPoly(poly);
|
||||
return f.setColor(0).fillPoly(poly);
|
||||
}
|
||||
|
||||
hand(t, d, c0, r0, c1, r1) {
|
||||
const g = this.g;
|
||||
t *= Math.PI / 30;
|
||||
const r = this.r;
|
||||
const z = 2 * r0 + 1;
|
||||
const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t);
|
||||
const x0 = x - r0, y0 = y - r0;
|
||||
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true});
|
||||
const r = this.r,
|
||||
z = 2 * r0 + 1,
|
||||
x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t),
|
||||
x0 = x - r0, y0 = y - r0;
|
||||
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 4, {msb: true});
|
||||
for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) {
|
||||
d.setPixel(i, j, g.getPixel(x0 + i, y0 + j));
|
||||
}
|
||||
|
@ -366,24 +366,20 @@ class Round {
|
|||
return [d, x0, y0];
|
||||
}
|
||||
|
||||
render(d) {
|
||||
const g = this.g;
|
||||
const b = this.b, bI = this.bI;
|
||||
const c = this.c, cI = this.cI;
|
||||
const e = d < this.enhance;
|
||||
const state = this.state;
|
||||
const options = this.options;
|
||||
const cal = options.calendric;
|
||||
const res = options.resolution;
|
||||
const dow = (e || cal == 1 || cal > 2) && d.getDay();
|
||||
const ts = res < 2 && d.getSeconds();
|
||||
const tm = (e || res < 3) && d.getMinutes() + ts / 60;
|
||||
const th = d.getHours() + d.getMinutes() / 60;
|
||||
const dd = (e || cal > 1) && d.getDate();
|
||||
const dm = (e || cal > 3) && d.getMonth();
|
||||
const dy = (e || cal > 4) && d.getFullYear();
|
||||
const xc = this.xc, yc = this.yc, r = this.r;
|
||||
const dlr = xc * 3/4, dlw = 8, dlhw = 4;
|
||||
render(d, rate) {
|
||||
const g = this.g, b = this.b, bI = this.bI, c = this.c, cI = this.cI,
|
||||
e = d < this.enhance,
|
||||
state = this.state, options = this.options,
|
||||
cal = options.calendric, res = options.resolution,
|
||||
dow = (e || cal === 1 || cal > 2) && d.getDay(),
|
||||
ts = res < 2 && d.getSeconds(),
|
||||
tm = (e || res < 3) && d.getMinutes() + ts / 60,
|
||||
th = d.getHours() + d.getMinutes() / 60,
|
||||
dd = (e || cal > 1) && d.getDate(),
|
||||
dm = (e || cal > 3) && d.getMonth(),
|
||||
dy = (e || cal > 4) && d.getFullYear();
|
||||
const xc = this.xc, yc = this.yc, r = this.r,
|
||||
dlr = xc * 3/4, dlw = 8, dlhw = 4;
|
||||
|
||||
// Restore saveunders for fast-moving, overdrawing indicators.
|
||||
if (state.sd) g.drawImage.apply(g, state.sd);
|
||||
|
@ -397,10 +393,10 @@ class Round {
|
|||
state.dow = dow;
|
||||
}
|
||||
|
||||
const locked = Bangle.isLocked();
|
||||
const charging = Bangle.isCharging();
|
||||
const battery = E.getBattery();
|
||||
const HRMOn = Bangle.isHRMOn();
|
||||
const locked = Bangle.isLocked(),
|
||||
charging = Bangle.isCharging(),
|
||||
battery = E.getBattery(),
|
||||
HRMOn = Bangle.isHRMOn();
|
||||
if (dy !== state.dy ||
|
||||
locked !== state.locked ||
|
||||
charging !== state.charging ||
|
||||
|
@ -463,6 +459,7 @@ class Round {
|
|||
this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) :
|
||||
null;
|
||||
state.sd = ts === +ts ?
|
||||
rate > 1000 ? this.hand(ts, state.sd, g.theme.fg2, this.secR, g.theme.bg, 2) :
|
||||
this.hand(ts, state.sd, g.theme.fg2, this.secR) :
|
||||
null;
|
||||
}
|
||||
|
@ -482,13 +479,23 @@ class Clock {
|
|||
|
||||
this.listeners = {
|
||||
lcdPower: on => on ? this.active() : this.inactive(),
|
||||
charging: () => {face.doIcons('charging'); this.active();},
|
||||
charging: on => {
|
||||
face.doIcons('charging');
|
||||
if (on) {
|
||||
this.listeners.accel =
|
||||
a => this.orientation(a) === this.attitude || this.active();
|
||||
Bangle.on('accel', this.listeners.accel);
|
||||
} else {
|
||||
Bangle.removeListener('accel', this.listeners.accel);
|
||||
delete this.listeners.accel;
|
||||
}
|
||||
this.active();
|
||||
},
|
||||
lock: () => {face.doIcons('locked'); this.active();},
|
||||
faceUp: up => {
|
||||
this.conservative = !up;
|
||||
this.active();
|
||||
},
|
||||
twist: _ => this.options.autolight && Bangle.setLCDPower(true),
|
||||
drag: e => {
|
||||
if (this.t0) {
|
||||
if (e.b) {
|
||||
|
@ -498,20 +505,23 @@ class Clock {
|
|||
if (e.y - this.e0.y < -50) {
|
||||
this.options.resolution > 0 && this.options.resolution--;
|
||||
this.rates.clock = this.timescales[this.options.resolution];
|
||||
this.ack();
|
||||
this.active();
|
||||
} else if (e.y - this.e0.y > 50) {
|
||||
this.options.resolution < this.timescales.length - 1 &&
|
||||
this.options.resolution++;
|
||||
this.rates.clock = this.timescales[this.options.resolution];
|
||||
this.ack();
|
||||
this.active();
|
||||
} else if (this.yX - this.yN < 20) {
|
||||
const now = new Date();
|
||||
if (now - this.t0 < 250) {
|
||||
this.ack();
|
||||
face.enhanceUntil(now + 30000);
|
||||
face.render(now);
|
||||
this.active();
|
||||
} else if (now - this.t0 > 500) {
|
||||
this.stop();
|
||||
this.options.interact();
|
||||
this.ack().then(_ => this.options.interact());
|
||||
}
|
||||
}
|
||||
this.t0 = null;
|
||||
|
@ -524,9 +534,25 @@ class Clock {
|
|||
};
|
||||
}
|
||||
|
||||
ack() {
|
||||
return Bangle.buzz(33);
|
||||
}
|
||||
|
||||
orientation(a) {
|
||||
return Math.abs(a.z) < 0.85 ?
|
||||
Math.abs(a.y) > Math.abs(a.x) ? a.y < 0 ? 0 : 2 : a.x > 0 ? 1 : 3 :
|
||||
0;
|
||||
}
|
||||
|
||||
rotation() {
|
||||
return this.options.autorotate && Bangle.isCharging() ?
|
||||
this.orientation(Bangle.getAccel()) :
|
||||
0;
|
||||
}
|
||||
|
||||
redraw(rate) {
|
||||
const now = this.updated = new Date();
|
||||
if (this.refresh) this.face.reset(true);
|
||||
if (this.refresh) this.face.reset(this.attitude = this.rotation());
|
||||
this.refresh = false;
|
||||
rate = this.face.render(now, rate);
|
||||
if (rate !== this.rates.face) {
|
||||
|
@ -541,13 +567,13 @@ class Clock {
|
|||
this.exception && clearTimeout(this.exception);
|
||||
this.interval && clearInterval(this.interval);
|
||||
this.timeout = this.exception = this.interval = this.rate = null;
|
||||
this.face.reset(false); // Cancel any ongoing background rendering
|
||||
this.face.reset(); // Cancel any ongoing background rendering
|
||||
return this;
|
||||
}
|
||||
|
||||
active() {
|
||||
const prev = this.rate;
|
||||
const now = Date.now();
|
||||
const prev = this.rate,
|
||||
now = Date.now();
|
||||
let rate = Infinity;
|
||||
for (const k in this.rates) {
|
||||
let r = this.rates[k];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "pooqround",
|
||||
"name": "pooq Round watch face",
|
||||
"shortName":"pooq Round",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "A 24 hour analogue watchface with high legibility and a novel style.",
|
||||
"icon": "app.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// pooqRoman resource maker
|
||||
//
|
||||
// Copyright (c) 2021 Stephen P Spackman
|
||||
// Copyright (c) 2021, 2022 Stephen P Spackman
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -147,18 +147,18 @@ res += prepFont('y10', `
|
|||
xxx
|
||||
xxx
|
||||
-2--------------------------------
|
||||
x xx
|
||||
x xxx
|
||||
xx xxx
|
||||
xxxx xxx
|
||||
xxxxx xxx
|
||||
xxxxxxx xxx
|
||||
xxxx xxx xxx
|
||||
xxxx xxxx xxx
|
||||
xxxx xxxx xxx
|
||||
xxxx xxxxxxxx xxxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxxxx
|
||||
xxxx xxxx xxxx
|
||||
xxxx xxxxx xxxx
|
||||
xxxx xxxxxxx xxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxx
|
||||
xxxx xxxxxxxxxx
|
||||
xxxx xxxxxxxxx
|
||||
-3--------------------------------
|
||||
xxx x xxx
|
||||
xxx xx xxx
|
||||
|
@ -270,10 +270,10 @@ res += prepFont('y1', `
|
|||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
-1----------------------------------------------
|
||||
xxx
|
||||
xxx
|
||||
xxx
|
||||
xxx x
|
||||
xxx x
|
||||
xxx x
|
||||
xxx xx
|
||||
xxx xx
|
||||
xxx xxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
@ -282,18 +282,18 @@ res += prepFont('y1', `
|
|||
xxx
|
||||
xxx
|
||||
-2----------------------------------------------
|
||||
x xx
|
||||
x xxx
|
||||
xx xxx
|
||||
xxxx xxx
|
||||
xxxxx xxx
|
||||
xxxxxxx xxx
|
||||
xxxx xxxx xxx
|
||||
xxxx xxxxx xxx
|
||||
xxxx xxxxxxx xxxx
|
||||
xxxx xxxxxxxxxxxxx xxxxxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxx
|
||||
xxxxxx xxx
|
||||
xxxxxxxx xxx
|
||||
xxxx xxxxx xxx
|
||||
xxxx xxxxxx xxxx
|
||||
xxxx xxxxxxxx xxxx
|
||||
xxxx xxxxxxxxxxx xxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxx xxxxxxxxxxxxxxxxx
|
||||
-3----------------------------------------------
|
||||
xxx x xxx
|
||||
xxx xx xxx
|
||||
|
@ -645,12 +645,12 @@ xxxx xxxx
|
|||
-1----------------------------------------------
|
||||
|
||||
|
||||
xxx x
|
||||
xxx xx
|
||||
xxx xxx
|
||||
xxx xxx
|
||||
xxx xxxx
|
||||
xxx xxxx
|
||||
xxx
|
||||
xxx x
|
||||
xxx xx
|
||||
xxx xx
|
||||
xxx xxx
|
||||
xxx xxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
@ -993,9 +993,9 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|||
xxx
|
||||
|
||||
|
||||
xxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxxx
|
||||
xxxxxxxxxxxxxxxxxxx
|
||||
xxxx
|
||||
xxxx
|
||||
xxx
|
||||
|
|
|
@ -2,11 +2,19 @@
|
|||
|
||||
Directly launch apps from the clock screen with custom patterns.
|
||||
|
||||
## Usage
|
||||
## Installation and Usage
|
||||
|
||||
Install Pattern Launcher alongside your main laucher app.
|
||||
_Do not delete that launcher!_
|
||||
Pattern Launcher is designed as an additional app launching utility, not as a replacement for the main launcher.
|
||||
|
||||
In the main launcher, start Pattern Launcher in the app menu to assign the pattern configuration (see below).
|
||||
Note that this actually among the applications, _not_ in the application settings!
|
||||
|
||||
Create patterns and link them to apps in the Pattern Launcher app.
|
||||
|
||||
Then launch the linked apps directly from the clock screen by simply drawing the desired pattern.
|
||||
Note that this does only work in the clock screen, not if other applications run.
|
||||
|
||||
## Add Pattern Screenshots
|
||||
|
||||
|
@ -28,7 +36,8 @@ Then launch the linked apps directly from the clock screen by simply drawing the
|
|||
|
||||
## Detailed Steps
|
||||
|
||||
From the main menu you can:
|
||||
The main menu of Pattern Launcher is accessible from the _application_ starter of the main launcher.
|
||||
From there you can:
|
||||
|
||||
- Add a new pattern and link it to an app (first entry)
|
||||
- To create a new pattern first select "Add Pattern"
|
||||
|
@ -60,6 +69,16 @@ Make sure the watch is unlocked before you start drawing. If this bothers you, y
|
|||
|
||||
Please note that drawing on the clock screen will not visually show the pattern you drew. It will start the app as soon as the pattern was recognized - this might take 1 or 2 seconds! If still nothing happens, that might be a bug, sorry!
|
||||
|
||||
4. Where can I configure the patterns?
|
||||
|
||||
You have to start the "Pattern Launcher" app from the main app launcher's app selection.
|
||||
|
||||
5. Do I have to delete my former app launcher so that Pattern Launcher is the only installed launcher?
|
||||
|
||||
No! Pattern Launcher works alongside another "main" launcher.
|
||||
If you have deleted that one, you do not have a general purpose app launcher any more and cannot access Pattern Launcher's configuration.
|
||||
If you already have deleted your main launcher accidentially, just reinstall it from the app loader.
|
||||
|
||||
## Authors
|
||||
|
||||
Initial creation: [crazysaem](https://github.com/crazysaem)
|
||||
|
@ -67,3 +86,5 @@ Initial creation: [crazysaem](https://github.com/crazysaem)
|
|||
Improve pattern detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/)
|
||||
|
||||
Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/)
|
||||
|
||||
Doc additions: [dirkhillbrecht](http://forum.espruino.com/profiles/182498/)
|
||||
|
|
|
@ -4,3 +4,7 @@
|
|||
0.03: Fix theme and maps/graphing if no GPS
|
||||
0.04: Multiple bugfixes
|
||||
0.05: Add recording for coresensor
|
||||
0.06: Add recording for battery stats
|
||||
Fix execution of other recorders (*.recorder.js)
|
||||
Modified icons and colors for better visibility
|
||||
Only show plotting speed if Latitude is available
|
||||
|
|
|
@ -16,7 +16,8 @@ You can record
|
|||
* **Time** The current time
|
||||
* **GPS** GPS Latitude, Longitude and Altitude
|
||||
* **Steps** Steps counted by the step counter
|
||||
* **HR** Heart rate
|
||||
* **HR** Heart rate and confidence
|
||||
* **BAT** Battery percentage and voltage
|
||||
* **Core** CoreTemp body temperature
|
||||
|
||||
**Note:** It is possible for other apps to record information using this app
|
||||
|
@ -25,4 +26,4 @@ function in `widget.js` for more information.
|
|||
|
||||
## Tips
|
||||
|
||||
When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a grey satellite symbol, which you will see turn red when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
|
||||
When recording GPS, it usually takes several minutes for the watch to get a [GPS fix](https://en.wikipedia.org/wiki/Time_to_first_fix). There is a red satellite symbol, which you will see turn green when you get an actual GPS Fix. You can [upload assistant files](https://banglejs.com/apps/#assisted%20gps%20update) to speed up the time spent on getting a GPS fix.
|
||||
|
|
|
@ -199,9 +199,10 @@ function viewTrack(filename, info) {
|
|||
menu['Plot Alt.'] = function() {
|
||||
plotGraph(info, "Altitude");
|
||||
};
|
||||
menu['Plot Speed'] = function() {
|
||||
plotGraph(info, "Speed");
|
||||
};
|
||||
if (info.fields.includes("Latitude"))
|
||||
menu['Plot Speed'] = function() {
|
||||
plotGraph(info, "Speed");
|
||||
};
|
||||
// TODO: steps, heart rate?
|
||||
menu['Erase'] = function() {
|
||||
E.showPrompt("Delete Track?").then(function(v) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "recorder",
|
||||
"name": "Recorder (BETA)",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
|
|
|
@ -48,41 +48,50 @@
|
|||
Bangle.removeListener('GPS', onGPS);
|
||||
Bangle.setGPSPower(0,"recorder");
|
||||
},
|
||||
draw : (x,y) => g.setColor(hasFix?"#0ff":"#888").drawImage(atob("DAyBAAACADgDuBOAeA4AzAHADgAAAA=="),x,y)
|
||||
draw : (x,y) => g.setColor(hasFix?"#0f0":"#f88").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),x,y)
|
||||
};
|
||||
},
|
||||
hrm:function() {
|
||||
var bpm = 0, bpmConfidence = 0;
|
||||
var hasBPM = false;
|
||||
function onHRM(h) {
|
||||
if (h.confidence >= bpmConfidence) {
|
||||
bpmConfidence = h.confidence;
|
||||
bpm = h.bpm;
|
||||
if (bpmConfidence) hasBPM = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
name : "HR",
|
||||
fields : ["Heartrate"],
|
||||
fields : ["Heartrate", "Confidence"],
|
||||
getValues : () => {
|
||||
var r = [bpmConfidence?bpm:""];
|
||||
var r = [bpm,bpmConfidence];
|
||||
bpm = 0; bpmConfidence = 0;
|
||||
return r;
|
||||
},
|
||||
start : () => {
|
||||
hasBPM = false;
|
||||
Bangle.on('HRM', onHRM);
|
||||
Bangle.setHRMPower(1,"recorder");
|
||||
},
|
||||
stop : () => {
|
||||
hasBPM = false;
|
||||
Bangle.removeListener('HRM', onHRM);
|
||||
Bangle.setHRMPower(0,"recorder");
|
||||
},
|
||||
draw : (x,y) => g.setColor(hasBPM?"#f00":"#888").drawImage(atob("DAyBAAAAAD/H/n/n/j/D/B+AYAAAAA=="),x,y)
|
||||
draw : (x,y) => g.setColor(Bangle.isHRMOn()?"#f00":"#f88").drawImage(atob("DAwBAAAAMMeef+f+f+P8H4DwBgAA"),x,y)
|
||||
};
|
||||
},
|
||||
bat:function() {
|
||||
return {
|
||||
name : "BAT",
|
||||
fields : ["Battery Percentage", "Battery Voltage", "Charging"],
|
||||
getValues : () => {
|
||||
return [E.getBattery(), NRF.getBattery(), Bangle.isCharging()];
|
||||
},
|
||||
start : () => {
|
||||
},
|
||||
stop : () => {
|
||||
},
|
||||
draw : (x,y) => g.setColor(Bangle.isCharging() ? "#0f0" : "#ff0").drawImage(atob("DAwBAABgH4G4EYG4H4H4H4GIH4AA"),x,y)
|
||||
};
|
||||
},
|
||||
|
||||
temp:function() {
|
||||
var core = 0, skin = 0;
|
||||
var hasCore = false;
|
||||
|
@ -106,7 +115,7 @@
|
|||
hasCore = false;
|
||||
Bangle.removeListener('CoreTemp', onCore);
|
||||
},
|
||||
draw : (x,y) => g.setColor(hasCore?"#0f0":"#888").drawImage(atob("DAyBAAHh0js3EuDMA8A8AWBnDj9A8A=="),x,y)
|
||||
draw : (x,y) => g.setColor(hasCore?"#0f0":"#8f8").drawImage(atob("DAwBAAAOAKPOfgZgZgZgZgfgPAAA"),x,y)
|
||||
};
|
||||
},
|
||||
steps:function() {
|
||||
|
@ -121,7 +130,7 @@
|
|||
},
|
||||
start : () => { lastSteps = Bangle.getStepCount(); },
|
||||
stop : () => {},
|
||||
draw : (x,y) => g.reset().drawImage(atob("DAyBAAADDHnnnnnnnnnnjDmDnDnAAA=="),x,y)
|
||||
draw : (x,y) => g.reset().drawImage(atob("DAwBAAMMeeeeeeeecOMMAAMMMMAA"),x,y)
|
||||
};
|
||||
}
|
||||
// TODO: recAltitude from pressure sensor
|
||||
|
@ -138,7 +147,7 @@
|
|||
}
|
||||
})
|
||||
*/
|
||||
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(fn)(recorders));
|
||||
require("Storage").list(/^.*\.recorder\.js$/).forEach(fn=>eval(require("Storage").read(fn))(recorders));
|
||||
return recorders;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
1.00: Hello Ruuvi Watch!
|
||||
1.01: Clear gfx on startup.
|
|
@ -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
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{ "id": "ruuviwatch",
|
||||
"name": "Ruuvi Watch",
|
||||
"shortName":"Ruuvi Watch",
|
||||
"icon": "ruuviwatch.png",
|
||||
"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",
|
||||
"supports": ["BANGLEJS"],
|
||||
"storage": [
|
||||
{"name":"ruuviwatch.app.js","url":"ruuviwatch.app.js"},
|
||||
{"name":"ruuviwatch.img","url":"ruuviwatch.app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA"))
|
|
@ -0,0 +1,151 @@
|
|||
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.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--;
|
||||
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();
|
Binary file not shown.
After Width: | Height: | Size: 665 B |
|
@ -21,3 +21,4 @@
|
|||
Memory usage enhancements
|
||||
0.20: Fix issue where step count would randomly reset
|
||||
0.21: Memory usage improvements, fix widget initial width (fix #1170)
|
||||
0.22: Fix 'stps' regression for 0.21 (fix #1233)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "widpedom",
|
||||
"name": "Pedometer widget",
|
||||
"version": "0.21",
|
||||
"version": "0.22",
|
||||
"description": "Daily pedometer widget",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
// add your widget
|
||||
WIDGETS["wpedom"]={area:"tl",width:0,
|
||||
getWidth:function() {
|
||||
let stps = stp_today.toString();
|
||||
let newWidth = 24;
|
||||
if (settings.hide)
|
||||
newWidth = 0;
|
||||
|
@ -68,7 +69,6 @@
|
|||
return newWidth;
|
||||
},
|
||||
redraw:function() { // work out the width, and queue a full redraw if needed
|
||||
let stps = stp_today.toString();
|
||||
let newWidth = this.getWidth();
|
||||
if (newWidth!=this.width) {
|
||||
// width has changed, re-layout all widgets
|
||||
|
|
Loading…
Reference in New Issue