Merge remote-tracking branch 'upstream/master' into jekyll-apps.json

master
Adam Schmalhofer 2022-01-07 13:04:35 +01:00
commit 740f1fdb71
26 changed files with 576 additions and 158 deletions

View File

@ -1 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Make overriding the HRM event optional
Emit BTHRM event for external sensor
Add recorder app plugin

View File

@ -2,24 +2,43 @@
var log = function() {};//print var log = function() {};//print
var gatt; var gatt;
var status; var status;
Bangle.isHRMOn = function() { var origIsHRMOn = Bangle.isHRMOn;
Bangle.isBTHRMOn = function(){
return (status=="searching" || status=="connecting") || (gatt!==undefined); 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 // Do app power handling
if (!app) app="?"; if (!app) app="?";
log("setHRMPower ->", isOn, app); log("setBTHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={}; if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.HRM===undefined) Bangle._PWR.HRM=[]; if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM.push(app); if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.HRM.includes(app)) Bangle._PWR.HRM = Bangle._PWR.HRM.filter(a=>a!=app); if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
isOn = Bangle._PWR.HRM.length; isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on // so now we know if we're really on
if (isOn) { if (isOn) {
log("setHRMPower on", app); log("setBTHRMPower on", app);
if (!Bangle.isHRMOn()) { if (!Bangle.isBTHRMOn()) {
log("HRM not already on"); log("BTHRM not already on");
status = "searching"; status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) { NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id); log("Found device "+device.id);
@ -49,7 +68,11 @@
if (flags&16) { if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds var interval = dv.getUint16(idx,1); // in milliseconds
}*/ }*/
Bangle.emit('HRM',{
var eventName = settings.replace ? "HRM" : "BTHRM";
Bangle.emit(eventName, {
bpm:bpm, bpm:bpm,
confidence:100 confidence:100
}); });
@ -65,15 +88,27 @@
}); });
} }
} else { // not on } else { // not on
log("setHRMPower off", app); log("setBTHRMPower off", app);
if (gatt) { if (gatt) {
log("HRM connected - disconnecting"); log("BTHRM connected - disconnecting");
status = undefined; status = undefined;
try {gatt.disconnect();}catch(e) { try {gatt.disconnect();}catch(e) {
log("HRM disconnect error", e); log("BTHRM disconnect error", e);
} }
gatt = undefined; 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);
}
}
})(); })();

61
apps/bthrm/bthrm.js Normal file
View File

@ -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);

View File

@ -2,15 +2,18 @@
"id": "bthrm", "id": "bthrm",
"name": "Bluetooth Heart Rate Monitor", "name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM", "shortName": "BT HRM",
"version": "0.01", "version": "0.02",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png", "icon": "app.png",
"type": "boot", "type": "app",
"tags": "health,bluetooth", "tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.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"}
] ]
} }

27
apps/bthrm/recorder.js Normal file
View File

@ -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)
};
}
})

33
apps/bthrm/settings.js Normal file
View File

@ -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();
}
}
});
})

View File

@ -1,2 +1,3 @@
0.00: Initial check-in. 0.00: Initial check-in.
0.01: Add tap-to-decorate feature. Bugfixes. 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.

View File

@ -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, 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 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 While charging the main display automatically rotates so that noon is up. This can be disabled.
for this integrated into the watchface; you can disable it in the menu, if you prefer.
## Limitations ## Limitations

View File

@ -25,8 +25,8 @@
// //
// This only works for Bangle 2. // This only works for Bangle 2.
const isString = x => typeof x === 'string'; const isString = x => typeof x === 'string',
const imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width; imageWidth = i => isString(i) ? i.charCodeAt(0) : i.width;
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
/* System integration */ /* System integration */
@ -115,9 +115,9 @@ class RoundOptions extends Options {
onchange: x => this.calendric = x, onchange: x => this.calendric = x,
format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x], format: x => ['none', 'day', 'date', 'both', 'month', 'full'][x],
}, },
'Auto-Illum.': { 'Autorotate': {
init: _ => this.autolight, init: _ => this.autorotate,
onchange: x => this.autolight = x onchange: x => this.autorotate = x
}, },
Defaults: _ => {this.reset(); this.interact();} Defaults: _ => {this.reset(); this.interact();}
}); });
@ -133,7 +133,7 @@ RoundOptions.defaults = {
calendric: 5, calendric: 5,
dayFg: '#fff', dayFg: '#fff',
nightFg: '#000', nightFg: '#000',
autolight: true, autorotate: true,
}; };
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
@ -144,29 +144,29 @@ const dec = x => E.toString(heatshrink.decompress(atob(x)));
const y10F = [ const y10F = [
dec( dec(
'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' + 'g///EAh////AA4IIBgPwgE+gAOBg/AngXB+EPAYM8gfggEfgF8D4OAj4dB8EDAYI' +
'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUYgECCIZlBAY' + 'fBBAISBAAMOAYUB4AECnEAkAuBgEQBAPgIYX8IYX/wYDCEwIiMMgUcgECCIZlBAY' +
'N4CoRUBIoMP8AZBge8CgMB8+BCAPw+F/gf8jxDB/0D4BGBEQMPAYIeBoAfBnEwge' + 'N4CoRUBIoMP8AZBge8MoMB8+B8B4B+E/gf4jw/B/kD4ADBEQMPSYXgoAfBnEwgeA' +
'Ah0cB4MDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMO4P8LwM/XAJLBT' + 'hw7BvEDx4PBgHn4EB8E7LQM8h/eJ4MDBgIpB+H+g/wnE/WwMMG4ReBn4zBJYKcDH' +
'gY7BAAN/wC9CQwV+jwDB/4pBgP/EQKYBBIIxBPQP+SATfCIYIiCO4I9BBwM//hlB' + '4IABv+AXoSGCv0eAYP/FIMB/4iBTAIJBGIJ6B/yQCb4RDBEQTlBHoIOBn51BwC+B' +
'PQJlCwYGBTAPgIgM4CYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+' + 'MoWDAwKYBRgKYBCYM8hwKBMoODegPA8F+gZlBewP4hz/BE4QrBGgM/LAV//4+BAY' +
'BAYJyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwn4yBnhxBGQJxBGQK5BGQKWDOwUACAM' + 'JyBPwM/KQMeGQMPFwM8H4UHBIPwGQNwZgPwnhxBGQJxBGQK5BGQKWDOwUACALlBI' +
'D/BDCNYPg///8E5HwR2BIwMDSgK0FSocMAYTLBAAYpBQAPnDwJGBEwK+B/hlB+F8' + 'YRrB8H///gnI+COwJGBgaUBWgqVDhgDCZYIADFIKAB84eBIwImBXwP8MoPwviYCI' +
'TARABTAJABTAPBMoR+BMoKXBDoX5DwIuBMoUPS4THCGwJbBhAaBvh5B+EHwPAOwP' + 'AKYBIAKYB4JlCPwJlBS4IdC/IeBFwJlCh6XCY4Q2BLYMIDQN8PIPwg+B4B2B8FwG' +
'guA1BvCcB4E8nxlBn1/VoIyBwDKBO4SGCgA=' 'oN4TgPAnk+MoM+v6tBGQOAZQJ3CQwUAA'
), 48, dec('hgAI'), 34 ), 48, dec('hgAI'), 34
];const y1F = [ ];const y1F = [
dec( dec(
'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnAFEj' + 'g//AAPggE/AoX8gF/AoX+gF8CoU+gHwAoUPgAZBEIQFGCIodFFIo1FIIoADnEAgQ' +
'gFEh0AhA1EiAFCgeAFIf/4A1DFQIED/5MDGB6OEjAECHIIYDhkAuAFCjwFEj6DEn' + 'FCjkAgwFCh0Ahg1EBoIABgeAFIf/4A1DFQIED/5MDGAYADEQYwDRwgMDhAYEH4Nw' +
'+AAod74AFD/PgvAtC+Hwv/wgZSBvEfLwc8RISOBGAJsBVAXgggEBE4PgIgJLC8E8' + 'AoUeAok/QYl/wAFD/fAHgUD+PgvAFBj/g+E/4EBLAN4j5SCgE8h4EB/AwCAoOAVA' +
'I4fgXQS/B8IhBGwOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQJCB/DMDMoMBboYVBKo' + 'PgggeBFoPgQgRLB8E8I4fgXQS/B8KwBMgOA8YFCgfA9+eAoMB4H/j/ACIPA/kPCQ' +
'IDBSYeAAoYlCAATpEg/4Xwc/QIcPFoJcBQIP8GILXCDYLXBbId//BeCL4QwDgIwD' + 'JIB/DMDMoJSBboQVBKoIDBSYZOBAAQlCAATpEg/4Xwc/QIZyBwBcBgf//gxBa4Qb' +
'AAIXBDAQfCEYSPBAoaPCPQKPCAoZgBAoYvBAoIXBBAIFB/ALDEoJHBAoaPDaQSPB' + 'Ba4LZDv/4LwRfCGAcBGAYABC4IYCD4QjCR4IFDR4R6BR4QFDMAIFDF4IFBC4IIBA' +
'AoKcBJgY9DTQX/EoKmCC4SyCYYJJB+CHBj+Aj8ASYJNBBINwIIOAM4ILDAYN/wAB' + 'oLEBBYQlBI4IFDR4ZrBR4QFBTgJMDHoaaCdQSmCC4SyCYYJJB+CHBj+Aj8ASYJNB' +
'BB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBAgaNCYwgFEbAkAwAFEc4S' + 'BINwIIOAM4ILDAYN/wABBB4JBBI45vCRYgADApEHL4pHB8AECFIPhAYLCCAggFBA' +
'PCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh4FDMoQ1DK4ZBBMQIDBJY' + 'gaNCYwgFEbAkAwAFEc4SPCj/+LIKPBv6PEAoRnBFIMDFYLXCKoTLDa4YRDBYIdDh' +
'bWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwRCvEBF4IdB+E/OIp9CJgZ' + '4FDMoQ1DK4ZBBMQIDBJYbWBFIMEIIQpBgxxBgZRBh8AAYN8AoQVBjgbBAoTZBvwR' +
'BCQQUAA=' 'CvEBF4IdB+E/OIp9CJgZBCQQUAA='
), 48, dec('hgAI'), 48 ), 48, dec('hgAI'), 48
];const y10sF = [ ];const y10sF = [
dec( dec(
@ -194,20 +194,20 @@ const y10F = [
];const d1F = [ ];const d1F = [
dec( dec(
'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' + 'AB1/+AECj///4FCAgP/8EAgf/4F//EAg4CBgf8gEPwAUBn0AhwaCAYMeAoUPgEcA' +
'oUHAowRFDoopFGopBFJopZGBgIKCABlAIIcA4AFDgIFEgZBCAoMHAohVBAoY6CHg' + 'oUHAowRFDoopFGopBFJopZGBgIKCAB5BBgA1CAoMBAokDCIgTCAYRTDAoI6CHgU/' +
'U/Aol/AogADGoQFUABEMAQM/AQN8bIRZBRgJ5BLILhBgP3LIcD84rDg/HWYcPw4F' + 'Aol/Aog1GAqgAIhgCBn4CBvjZCLIKMBPIJZBcIMB+4lBMoMD84rDg/HL4cPw4FDj' +
'Dj4PBAoU+Aol8Aon4PocB+CJDgfgAoXgh/ATYX4v+AU4X//w/DbYQFCCwJ3PvDIE' + '5rEnwFEvgFE/AFBaYMB+CJCwED8AFC8EP4CbC/F/wCnC//+H4bbCAoQWBO594EAI' +
'NYQCCdoJ6CgfAiCGCI4NwgEeFwISCLoMeJwJdCnkfHYd4v4FD+f5AoUB9/BAoUD/' + 'TBgBrCAQTtBPQUD4EQQwRHBuEAjwuBCQRdBjxOBLoU8j47DvF/Aofz/IFCgPv4IF' +
'4jCh8HG4IpCh5DBAIMeE4Q/BvjMCfoP8Z4Uf//wCgInB/5lCABs+AoicBAAUDAok' + 'Cgf/EYUPg43BFIUPIYIBBjwnCH4N8ZgT9B/jPCj//+AUBE4P/MoQANnwFETgIACg' +
'P9wFDv+OCAjUCHQP4AoY5BAoUHEIIFCv5JBAoLQBLQYqEApQpDArIAJv5IBnBTCV' + 'YFEh/uAod/xwQEagQ6B/AFDHIIFCg4hBAoV/JIIFBaAJaDFQgFKFIYFZABN/JAM4' +
'4McJAQFBcYLvBB4IkBd4N4cYQBBeoLdBCYIFDngFECoIFDOwIdCc4QpCFwIZCjwu' + 'KYSvBjhICAoLjBd4IPBEgLvBvDjCAIL1BboITBAoc8AogVBAoZ2BDoTnCFIQuBDI' +
'BEoU8FwIxCvAIBEIPB+AUBJIP/8AmBLYWAd4RnBdx4XCcYf/Dgn//AuEP4LjBXoJ' + 'UeFwIlCnguBGIV4BAIhB4PwCgJJB//gEwJbCwDvCM4LuPC4TjD/4cE//4Fwh/BcY' +
'AC//vQYT0BBIKDC+CZBOIM/wAFDVYIFCgIrBAoUDPoIdCO4QnBaQYnBGoQVBIIZI' + 'K9BIAX/96DCegIJBQYXwTIJxBn+AAoarBAoUBFYIFCgZ9BDoR3CE4LSDE4I1CCoJ' +
'CJoTNCLIY4CAYIaDAAKRCAASRDAAIaEYAQtDYAI5DRgZFCAAYuCQoQuBAgIFBvEH' + 'BDJARNCZoRZDHAQDBDQYABSIQACSIYABDQjACFobABHIaMDIoQADFwSFCFwIEBAo' +
'AgIFB+CgBAAMB86lE76EBFwX/GocPNoYmBIwk/HQl8LpIAQRId/SoYDB4ZJCUoPn' + 'N4g4EBAoPwUAIABgPnUonfQgIuC/41Dh5tDEwJGEn46EvhdJACCJDv6VDAYPDJIS' +
'VoUHwP3Y4YYBY4k+Y4h5BdILhBd4YFFCIodFFIo1FIIpNFLIplGAArMFn6oBHYMA' + 'lB86tCg+B+7HDDALHEnzHEPILpBcILvDAooRFDoopFGopBFJopZFMowAFZgs/VAI' +
'DYQFBgP5E4IFBgfgUgIFCwBZBEAL1BPYZbDA4Z7DLYRtCBYYlDBoIxCEYMBHoIvC' + '7BgAbCAoMB/InBAoMD8CkBAoWALIIgBeoJ7DLYYHDPYZbCNoQLDEoYNBGIQjBgI9' +
'HAI7Dh5PBI4X/LIX//7+Dn52Eh4QCA==' 'BF4Q4BHYcPJ4JHC/5ZC///fwc/OwkPCAQA=='
), 48, dec('ikPigAGA'), 48 ), 48, dec('ikPigAGA'), 48
];const dowF = [ ];const dowF = [
dec( dec(
@ -220,10 +220,10 @@ const y10F = [
'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' + 'kDMIgeBFIQEBBYRTBCAZ3FAggAMg4zEj7LEn7LEv++AodzxwFD+ePAofjw4FVDoo' +
'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' + 'pFv+eIImcJomYLImAAoZeEAtTyBAAQFEVYIFDSQIvhAojaCFwgABh4YEngFEuAqJ' +
'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' + 'gPAAocDApYuEgP/fgl/+B9HAAv+Aon8HQMOIAkeAokcAohaDAoM4Aol4AohmDAoJ' +
'BDAoJsDAo7vhABbJDAo9/AojEFMYbKMArCBDFI41FWIYABggFEgbuCDYMPLIQbBj' + 'BDAoJsDAo7vhABZuBQYoFDv4FEYgpjDZRgFYGYYpHGoqxDAAMEAokDdwQbBh//DY' +
'//wBdCn0H4DZCvEBb4YZBdYZBBAofgCIQFDDoIFFDoPggYFBF4IFBGoI7B+AFCE4' + 'cf/+ALoU+g/AbIV4gLfDDILrDIIIFD8ARCAoYdBAoodB8EDAoIvBAoI1BHYPwAoQ' +
'NwCIIlCuAdBIYU4gPwn5VBjC7B/y0Dv/4YwcPCwMAjJlCAAM584FDufDCAUA8eBA' + 'nBuARBEoVwDoJDCnEB+E/KoMYXYP+Wgd//DGDh4WBgEZMoQABnPnAodz4YQCgHjw' +
'p/zC4n5EYj1BAoc//4RDU4IFDA==' 'IFP+YXE/IjEeoIFDn//CIanBAoY='
), 48, dec('kElkMljsljw='), 48 ), 48, dec('kElkMljsljw='), 48
];const mF = [ ];const mF = [
dec( dec(
@ -322,21 +322,20 @@ class Round {
this.r = this.xc - this.minR; 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) { doIcons(which) {
this.state[which] = null; this.state[which] = null;
this.render(new Date()); // Not quite right, I think.
} }
enhanceUntil(t) {this.enhance = t;} enhanceUntil(t) {this.enhance = t;}
pie(f, a0, a1, invert) { pie(f, a0, a1, invert) {
if (!invert) return this.pie(f, a1, a0 + 1, true); 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 i0 = Math.floor(a0 * 4 + 0.5), i1 = Math.floor(a1 * 4 + 0.5);
let x = f.getWidth() / 2, y = f.getHeight() / 2; const x = f.getWidth() / 2, y = f.getHeight() / 2;
let poly = [ const poly = [
x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1), x + (i1 & 2 ? -x : x) * (i1 & 1 ? 1 : t1),
y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1), y + (i1 & 2 ? y : -y) / (i1 & 1 ? t1 : 1),
x, x,
@ -348,16 +347,17 @@ class Round {
for (i0++; i0 <= i1; i0++) poly.push( for (i0++; i0 <= i1; i0++) poly.push(
3 * i0 & 2 ? f.getWidth() : 0, i0 & 2 ? f.getHeight() : 0 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) { hand(t, d, c0, r0, c1, r1) {
const g = this.g;
t *= Math.PI / 30; t *= Math.PI / 30;
const r = this.r; const r = this.r,
const z = 2 * r0 + 1; z = 2 * r0 + 1,
const x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t); x = this.xc + r * Math.sin(t), y = this.yc - r * Math.cos(t),
const x0 = x - r0, y0 = y - r0; x0 = x - r0, y0 = y - r0;
d = d ? d[0] : Graphics.createArrayBuffer(z, z, 16, {msb: true}); 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++) { for (let i = 0; i < z; i++) for (let j = 0; j < z; j++) {
d.setPixel(i, j, g.getPixel(x0 + i, y0 + j)); d.setPixel(i, j, g.getPixel(x0 + i, y0 + j));
} }
@ -366,24 +366,20 @@ class Round {
return [d, x0, y0]; return [d, x0, y0];
} }
render(d) { render(d, rate) {
const g = this.g; const g = this.g, b = this.b, bI = this.bI, c = this.c, cI = this.cI,
const b = this.b, bI = this.bI; e = d < this.enhance,
const c = this.c, cI = this.cI; state = this.state, options = this.options,
const e = d < this.enhance; cal = options.calendric, res = options.resolution,
const state = this.state; dow = (e || cal === 1 || cal > 2) && d.getDay(),
const options = this.options; ts = res < 2 && d.getSeconds(),
const cal = options.calendric; tm = (e || res < 3) && d.getMinutes() + ts / 60,
const res = options.resolution; th = d.getHours() + d.getMinutes() / 60,
const dow = (e || cal == 1 || cal > 2) && d.getDay(); dd = (e || cal > 1) && d.getDate(),
const ts = res < 2 && d.getSeconds(); dm = (e || cal > 3) && d.getMonth(),
const tm = (e || res < 3) && d.getMinutes() + ts / 60; dy = (e || cal > 4) && d.getFullYear();
const th = d.getHours() + d.getMinutes() / 60; const xc = this.xc, yc = this.yc, r = this.r,
const dd = (e || cal > 1) && d.getDate(); dlr = xc * 3/4, dlw = 8, dlhw = 4;
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;
// Restore saveunders for fast-moving, overdrawing indicators. // Restore saveunders for fast-moving, overdrawing indicators.
if (state.sd) g.drawImage.apply(g, state.sd); if (state.sd) g.drawImage.apply(g, state.sd);
@ -397,10 +393,10 @@ class Round {
state.dow = dow; state.dow = dow;
} }
const locked = Bangle.isLocked(); const locked = Bangle.isLocked(),
const charging = Bangle.isCharging(); charging = Bangle.isCharging(),
const battery = E.getBattery(); battery = E.getBattery(),
const HRMOn = Bangle.isHRMOn(); HRMOn = Bangle.isHRMOn();
if (dy !== state.dy || if (dy !== state.dy ||
locked !== state.locked || locked !== state.locked ||
charging !== state.charging || 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) : this.hand(tm, state.md, g.theme.bg, this.minR, g.theme.fg, this.minR - 1) :
null; null;
state.sd = ts === +ts ? 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) : this.hand(ts, state.sd, g.theme.fg2, this.secR) :
null; null;
} }
@ -482,13 +479,23 @@ class Clock {
this.listeners = { this.listeners = {
lcdPower: on => on ? this.active() : this.inactive(), 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();}, lock: () => {face.doIcons('locked'); this.active();},
faceUp: up => { faceUp: up => {
this.conservative = !up; this.conservative = !up;
this.active(); this.active();
}, },
twist: _ => this.options.autolight && Bangle.setLCDPower(true),
drag: e => { drag: e => {
if (this.t0) { if (this.t0) {
if (e.b) { if (e.b) {
@ -498,20 +505,23 @@ class Clock {
if (e.y - this.e0.y < -50) { if (e.y - this.e0.y < -50) {
this.options.resolution > 0 && this.options.resolution--; this.options.resolution > 0 && this.options.resolution--;
this.rates.clock = this.timescales[this.options.resolution]; this.rates.clock = this.timescales[this.options.resolution];
this.ack();
this.active(); this.active();
} else if (e.y - this.e0.y > 50) { } else if (e.y - this.e0.y > 50) {
this.options.resolution < this.timescales.length - 1 && this.options.resolution < this.timescales.length - 1 &&
this.options.resolution++; this.options.resolution++;
this.rates.clock = this.timescales[this.options.resolution]; this.rates.clock = this.timescales[this.options.resolution];
this.ack();
this.active(); this.active();
} else if (this.yX - this.yN < 20) { } else if (this.yX - this.yN < 20) {
const now = new Date(); const now = new Date();
if (now - this.t0 < 250) { if (now - this.t0 < 250) {
this.ack();
face.enhanceUntil(now + 30000); face.enhanceUntil(now + 30000);
face.render(now); this.active();
} else if (now - this.t0 > 500) { } else if (now - this.t0 > 500) {
this.stop(); this.stop();
this.options.interact(); this.ack().then(_ => this.options.interact());
} }
} }
this.t0 = null; 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) { redraw(rate) {
const now = this.updated = new Date(); 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; this.refresh = false;
rate = this.face.render(now, rate); rate = this.face.render(now, rate);
if (rate !== this.rates.face) { if (rate !== this.rates.face) {
@ -541,13 +567,13 @@ class Clock {
this.exception && clearTimeout(this.exception); this.exception && clearTimeout(this.exception);
this.interval && clearInterval(this.interval); this.interval && clearInterval(this.interval);
this.timeout = this.exception = this.interval = this.rate = null; 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; return this;
} }
active() { active() {
const prev = this.rate; const prev = this.rate,
const now = Date.now(); now = Date.now();
let rate = Infinity; let rate = Infinity;
for (const k in this.rates) { for (const k in this.rates) {
let r = this.rates[k]; let r = this.rates[k];

View File

@ -1,7 +1,7 @@
{ "id": "pooqround", { "id": "pooqround",
"name": "pooq Round watch face", "name": "pooq Round watch face",
"shortName":"pooq Round", "shortName":"pooq Round",
"version":"0.01", "version":"0.02",
"description": "A 24 hour analogue watchface with high legibility and a novel style.", "description": "A 24 hour analogue watchface with high legibility and a novel style.",
"icon": "app.png", "icon": "app.png",
"type": "clock", "type": "clock",

View File

@ -1,6 +1,6 @@
// pooqRoman resource maker // 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 // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal // of this software and associated documentation files (the "Software"), to deal
@ -147,18 +147,18 @@ res += prepFont('y10', `
xxx xxx
xxx xxx
-2-------------------------------- -2--------------------------------
x xx x xxx
xx xxx xx xxx
xxxx xxx xxxx xxx
xxxxx xxx xxxxx xxx
xxxxxxx xxx xxxxxxx xxx
xxxx xxx xxx xxxx xxx xxx
xxxx xxxx xxx xxxx xxxx xxxx
xxxx xxxx xxx xxxx xxxxx xxxx
xxxx xxxxxxxx xxxxxxx xxxx xxxxxxx xxxxxx
xxxx xxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxx
xxxx xxxxxxxxxx xxxx xxxxxxxxx
-3-------------------------------- -3--------------------------------
xxx x xxx xxx x xxx
xxx xx xxx xxx xx xxx
@ -270,10 +270,10 @@ res += prepFont('y1', `
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-1---------------------------------------------- -1----------------------------------------------
xxx xxx
xxx xxx x
xxx xxx xx
xxx x xxx xx
xxx x xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
@ -282,18 +282,18 @@ res += prepFont('y1', `
xxx xxx
xxx xxx
-2---------------------------------------------- -2----------------------------------------------
x xx x xxx
xx xxx xx xxx
xxxx xxx xxxx xxx
xxxxx xxx xxxxxx xxx
xxxxxxx xxx xxxxxxxx xxx
xxxx xxxx xxx xxxx xxxxx xxx
xxxx xxxxx xxx xxxx xxxxxx xxxx
xxxx xxxxxxx xxxx xxxx xxxxxxxx xxxx
xxxx xxxxxxxxxxxxx xxxxxxxxxxx xxxx xxxxxxxxxxx xxxxxxxx
xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxx
xxxx xxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxx
-3---------------------------------------------- -3----------------------------------------------
xxx x xxx xxx x xxx
xxx xx xxx xxx xx xxx
@ -645,12 +645,12 @@ xxxx xxxx
-1---------------------------------------------- -1----------------------------------------------
xxx x xxx
xxx xx xxx x
xxx xxx xxx xx
xxx xxx xxx xx
xxx xxxx xxx xxx
xxx xxxx xxx xxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
@ -993,9 +993,9 @@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxx xxx
xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxx
xxxx xxxx
xxxx xxxx
xxx xxx

View File

@ -2,11 +2,19 @@
Directly launch apps from the clock screen with custom patterns. 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. 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. 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 ## Add Pattern Screenshots
@ -28,7 +36,8 @@ Then launch the linked apps directly from the clock screen by simply drawing the
## Detailed Steps ## 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) - Add a new pattern and link it to an app (first entry)
- To create a new pattern first select "Add Pattern" - 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! 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 ## Authors
Initial creation: [crazysaem](https://github.com/crazysaem) 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 detection code readability: [PaddeK](http://forum.espruino.com/profiles/117930/)
Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/) Improve pattern rendering: [HughB](http://forum.espruino.com/profiles/167235/)
Doc additions: [dirkhillbrecht](http://forum.espruino.com/profiles/182498/)

View File

@ -4,3 +4,7 @@
0.03: Fix theme and maps/graphing if no GPS 0.03: Fix theme and maps/graphing if no GPS
0.04: Multiple bugfixes 0.04: Multiple bugfixes
0.05: Add recording for coresensor 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

View File

@ -16,7 +16,8 @@ You can record
* **Time** The current time * **Time** The current time
* **GPS** GPS Latitude, Longitude and Altitude * **GPS** GPS Latitude, Longitude and Altitude
* **Steps** Steps counted by the step counter * **Steps** Steps counted by the step counter
* **HR** Heart rate * **HR** Heart rate and confidence
* **BAT** Battery percentage and voltage
* **Core** CoreTemp body temperature * **Core** CoreTemp body temperature
**Note:** It is possible for other apps to record information using this app **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 ## 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.

View File

@ -199,9 +199,10 @@ function viewTrack(filename, info) {
menu['Plot Alt.'] = function() { menu['Plot Alt.'] = function() {
plotGraph(info, "Altitude"); plotGraph(info, "Altitude");
}; };
menu['Plot Speed'] = function() { if (info.fields.includes("Latitude"))
plotGraph(info, "Speed"); menu['Plot Speed'] = function() {
}; plotGraph(info, "Speed");
};
// TODO: steps, heart rate? // TODO: steps, heart rate?
menu['Erase'] = function() { menu['Erase'] = function() {
E.showPrompt("Delete Track?").then(function(v) { E.showPrompt("Delete Track?").then(function(v) {

View File

@ -2,7 +2,7 @@
"id": "recorder", "id": "recorder",
"name": "Recorder (BETA)", "name": "Recorder (BETA)",
"shortName": "Recorder", "shortName": "Recorder",
"version": "0.05", "version": "0.06",
"description": "Record GPS position, heart rate and more in the background, then download to your PC.", "description": "Record GPS position, heart rate and more in the background, then download to your PC.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,outdoors,gps,widget", "tags": "tool,outdoors,gps,widget",

View File

@ -48,41 +48,50 @@
Bangle.removeListener('GPS', onGPS); Bangle.removeListener('GPS', onGPS);
Bangle.setGPSPower(0,"recorder"); 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() { hrm:function() {
var bpm = 0, bpmConfidence = 0; var bpm = 0, bpmConfidence = 0;
var hasBPM = false;
function onHRM(h) { function onHRM(h) {
if (h.confidence >= bpmConfidence) { if (h.confidence >= bpmConfidence) {
bpmConfidence = h.confidence; bpmConfidence = h.confidence;
bpm = h.bpm; bpm = h.bpm;
if (bpmConfidence) hasBPM = true;
} }
} }
return { return {
name : "HR", name : "HR",
fields : ["Heartrate"], fields : ["Heartrate", "Confidence"],
getValues : () => { getValues : () => {
var r = [bpmConfidence?bpm:""]; var r = [bpm,bpmConfidence];
bpm = 0; bpmConfidence = 0; bpm = 0; bpmConfidence = 0;
return r; return r;
}, },
start : () => { start : () => {
hasBPM = false;
Bangle.on('HRM', onHRM); Bangle.on('HRM', onHRM);
Bangle.setHRMPower(1,"recorder"); Bangle.setHRMPower(1,"recorder");
}, },
stop : () => { stop : () => {
hasBPM = false;
Bangle.removeListener('HRM', onHRM); Bangle.removeListener('HRM', onHRM);
Bangle.setHRMPower(0,"recorder"); 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() { temp:function() {
var core = 0, skin = 0; var core = 0, skin = 0;
var hasCore = false; var hasCore = false;
@ -106,7 +115,7 @@
hasCore = false; hasCore = false;
Bangle.removeListener('CoreTemp', onCore); 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() { steps:function() {
@ -121,7 +130,7 @@
}, },
start : () => { lastSteps = Bangle.getStepCount(); }, start : () => { lastSteps = Bangle.getStepCount(); },
stop : () => {}, 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 // 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; return recorders;
} }

View File

@ -0,0 +1,2 @@
1.00: Hello Ruuvi Watch!
1.01: Clear gfx on startup.

25
apps/ruuviwatch/README.md Normal file
View File

@ -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

View File

@ -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}
]
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4A/ABMP/4ACCyIVDAAXwCyoYPIggAFCx4oEDBw/JJJguCBhAwLBZYjKBQQeGCIYNHB45bIBw4gIRgw+NC4wwJJ5YRLC5DzFCJBGMEYoSEFxoMEBQIXEF4gVFF5QcEC553JC5QRITgy/NVxIXGf5QlFIwy4IGBQuFC5JhGCwpGGERZOEBQ4MEDAwJJGAzdJCxLVJFxoYLCxoYICx6/GCqAA/AH4A/ACA"))

View File

@ -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

View File

@ -21,3 +21,4 @@
Memory usage enhancements Memory usage enhancements
0.20: Fix issue where step count would randomly reset 0.20: Fix issue where step count would randomly reset
0.21: Memory usage improvements, fix widget initial width (fix #1170) 0.21: Memory usage improvements, fix widget initial width (fix #1170)
0.22: Fix 'stps' regression for 0.21 (fix #1233)

View File

@ -1,7 +1,7 @@
{ {
"id": "widpedom", "id": "widpedom",
"name": "Pedometer widget", "name": "Pedometer widget",
"version": "0.21", "version": "0.22",
"description": "Daily pedometer widget", "description": "Daily pedometer widget",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",

View File

@ -55,6 +55,7 @@
// add your widget // add your widget
WIDGETS["wpedom"]={area:"tl",width:0, WIDGETS["wpedom"]={area:"tl",width:0,
getWidth:function() { getWidth:function() {
let stps = stp_today.toString();
let newWidth = 24; let newWidth = 24;
if (settings.hide) if (settings.hide)
newWidth = 0; newWidth = 0;
@ -68,7 +69,6 @@
return newWidth; return newWidth;
}, },
redraw:function() { // work out the width, and queue a full redraw if needed redraw:function() { // work out the width, and queue a full redraw if needed
let stps = stp_today.toString();
let newWidth = this.getWidth(); let newWidth = this.getWidth();
if (newWidth!=this.width) { if (newWidth!=this.width) {
// width has changed, re-layout all widgets // width has changed, re-layout all widgets