1
0
Fork 0

Merge branch 'espruino:master' into master

master
xxDUxx 2022-02-08 15:54:23 +01:00 committed by GitHub
commit a737e7f772
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1226 additions and 265 deletions

View File

@ -7,7 +7,7 @@
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
if (!require('Storage').read("alarm.js")) {
console.log(/*LANG*/"No alarm app!");
console.log("No alarm app!");
require('Storage').write('alarm.json',"[]");
} else {
var t = 3600000*(active[0].hr-hr);

View File

@ -2,3 +2,4 @@
0.02: Fix JSON save format
0.03: Add "Calculating" placeholder, update JSON save format
0.04: Fix tapping at very bottom of list, exit on inactivity
0.05: Add support for bulk importing and exporting tokens

View File

@ -3,6 +3,15 @@
* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here
* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/
## Important!
Tokens are stored *ONLY* on the watch. Make sure you do one or more of the following:
* Make a backup copy of the "authentiwatch.json" file.
* Export all your tokens to another device or print the QR code.
Keep those copies safe and secure.
## Supports
* Google Authenticator compatible 2-factor authentication
@ -14,8 +23,8 @@
* Between 6 and 10 digits
* Phone/PC configuration web page:
* Add/edit/delete/arrange tokens
* Scan QR codes
* Produce scannable QR codes
* Scan token and migration(import) QR codes
* Produce scannable token and migration(export) QR codes
## Usage
@ -24,6 +33,8 @@
* Swipe right to exit to the app launcher.
* Swipe left on selected counter token to advance the counter to the next value.
![Screenshot](screenshot.png)
## Creator
Andrew Gregory (andrew.gregory at gmail)

View File

@ -1,4 +1,5 @@
const tokenentryheight = 46;
const tokenextraheight = 16;
var tokendigitsheight = 30;
// Hash functions
const crypto = require("crypto");
const algos = {
@ -44,9 +45,6 @@ function b32decode(seedstr) {
}
}
}
if (bitcount > 0) {
retstr += String.fromCharCode(buf << (8 - bitcount));
}
var retbuf = new Uint8Array(retstr.length);
for (i in retstr) {
retbuf[i] = retstr.charCodeAt(i);
@ -117,27 +115,31 @@ function drawToken(id, r) {
var y1 = r.y;
var x2 = r.x + r.w - 1;
var y2 = r.y + r.h - 1;
var adj, sz;
var adj, lbl, sz;
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
lbl = tokens[id].label.substr(0, 10);
if (id == state.curtoken) {
// current token
g.setColor(g.theme.fgH);
g.setBgColor(g.theme.bgH);
g.setFont("Vector", 16);
g.setFont("Vector", tokenextraheight);
// center just below top line
g.setFontAlign(0, -1, 0);
adj = y1;
} else {
g.setColor(g.theme.fg);
g.setBgColor(g.theme.bg);
g.setFont("Vector", 30);
sz = tokendigitsheight;
do {
g.setFont("Vector", sz--);
} while (g.stringWidth(lbl) > r.w);
// center in box
g.setFontAlign(0, 0, 0);
adj = (y1 + y2) / 2;
}
g.clearRect(x1, y1, x2, y2);
g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false);
g.drawString(lbl, (x1 + x2) / 2, adj, false);
if (id == state.curtoken) {
if (tokens[id].period > 0) {
// timed - draw progress bar
@ -148,14 +150,14 @@ function drawToken(id, r) {
// counter - draw triangle as swipe hint
let yc = (y1 + y2) / 2;
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
adj = 10;
adj = 12;
}
// digits just below label
sz = 30;
sz = tokendigitsheight;
do {
g.setFont("Vector", sz--);
} while (g.stringWidth(state.otp) > (r.w - adj));
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false);
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
}
// shaded lines top and bottom
g.setColor(0.5, 0.5, 0.5);
@ -196,15 +198,15 @@ function draw() {
}
if (tokens.length > 0) {
var drewcur = false;
var id = Math.floor(state.listy / tokenentryheight);
var y = id * tokenentryheight + Bangle.appRect.y - state.listy;
var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight));
var y = id * (tokendigitsheight + tokenextraheight) + Bangle.appRect.y - state.listy;
while (id < tokens.length && y < Bangle.appRect.y2) {
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenentryheight});
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)});
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
drewcur = true;
}
id += 1;
y += tokenentryheight;
y += (tokendigitsheight + tokenextraheight);
}
if (drewcur) {
// the current token has been drawn - schedule a redraw
@ -226,7 +228,7 @@ function draw() {
state.nexttime = 0;
}
} else {
g.setFont("Vector", 30);
g.setFont("Vector", tokendigitsheight);
g.setFontAlign(0, 0, 0);
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
}
@ -238,18 +240,18 @@ function draw() {
function onTouch(zone, e) {
if (e) {
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight);
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight));
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
id = -1;
}
if (state.curtoken != id) {
if (id != -1) {
var y = id * tokenentryheight - state.listy;
var y = id * (tokendigitsheight + tokenextraheight) - state.listy;
if (y < 0) {
state.listy += y;
y = 0;
}
y += tokenentryheight;
y += (tokendigitsheight + tokenextraheight);
if (y > Bangle.appRect.h) {
state.listy += (y - Bangle.appRect.h);
}
@ -266,12 +268,15 @@ function onTouch(zone, e) {
function onDrag(e) {
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
if (e.dx == 0 && e.dy == 0) return;
var newy = Math.min(state.listy - e.dy, tokens.length * tokenentryheight - Bangle.appRect.h);
var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h);
state.listy = Math.max(0, newy);
draw();
}
function onSwipe(e) {
if (e == 1) {
exitApp();
}
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
tokens[state.curtoken].period--;
let newsettings={tokens:tokens,misc:settings.misc};
@ -296,7 +301,7 @@ function bangle1Btn(e) {
state.curtoken = Math.max(state.curtoken, 0);
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
var fakee = {};
fakee.y = state.curtoken * tokenentryheight - state.listy + Bangle.appRect.y;
fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y;
state.curtoken = -1;
state.nextTime = 0;
onTouch(0, fakee);

View File

@ -7,7 +7,10 @@
<style type="text/css">
body{font-family:sans-serif}
body div{display:none}
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#tokenqr{display:block}
body.select tr>:first-child,body.export tr>:nth-child(3),body.export tr>:nth-child(4){display:none}
body.select div.select,body.export div.export{display:block}
body.select div.export,body.export div.select{display:none}
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#showqr,body.export div#tokens{display:block}
#tokens th,#tokens td{padding:5px}
#tokens tr:nth-child(odd){background-color:#ccc}
#tokens tr:nth-child(even){background-color:#eee}
@ -33,6 +36,12 @@ form.totp tr.hotp,form.hotp tr.totp{display:none}
/* Start of all TOTP URLs */
const otpAuthUrl = 'otpauth://';
/* Start of all OTP migration URLs */
const otpMigrUrl = 'otpauth-migration://offline?data=';
/* Hash algorithms */
const otpAlgos = ['SHA1','SHA256','SHA512'];
const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
/* Settings */
@ -45,6 +54,8 @@ var tokens = settings.tokens;
*/
function base32clean(val, nows) {
var ret = val.replaceAll(/\s+/g, ' ');
ret = ret.replaceAll(/0/g, 'O');
ret = ret.replaceAll(/1/g, 'I');
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
if (nows) {
ret = ret.replaceAll(/\s+/g, '');
@ -52,6 +63,48 @@ function base32clean(val, nows) {
return ret;
}
function b32encode(str) {
let buf = 0, bitcount = 0, ret = '';
while (str.length > 0) {
buf <<= 8;
buf |= str.charCodeAt(0);
bitcount += 8;
str = str.substr(1);
while (bitcount >= 5) {
ret += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[(buf >> (bitcount - 5)) & 31];
bitcount -= 5;
}
}
return ret;
}
function b32decode(seedstr) {
// RFC4648
var i, buf = 0, bitcount = 0, ret = '';
for (i in seedstr) {
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
if (c != -1) {
buf <<= 5;
buf |= c;
bitcount += 5;
if (bitcount >= 8) {
ret += String.fromCharCode(buf >> (bitcount - 8));
buf &= (0xFF >> (16 - bitcount));
bitcount -= 8;
}
}
}
return ret;
}
function makeLabel(token) {
let lbl = token['label'];
if (lbl == '') {
lbl = (token['issuer'] == '') ? token['account'] : token['issuer'] + ' (' + token['account'] + ')';
}
token['label'] = lbl.substr(0, 10);
}
/* Save changes to a token to the global tokens[] array.
* id is the index into the global tokens[].
* forget is a flag indicating if the token should be forgotten.
@ -84,9 +137,16 @@ function saveEdit(id, forget) {
}
}
function showQr(url) {
tokenqr.clear();
tokenqr.makeCode(url);
qrPreviousClass = document.body.className;
document.body.className = 'showqr';
}
/* Generate and display a QR-code representing the current token.
*/
function showQrCode() {
function showTokenQr() {
var fe = document.forms['edittoken'].elements;
var url = new String(otpAuthUrl);
switch (fe['type'].value) {
@ -122,9 +182,7 @@ function showQrCode() {
if (fe['algorithm'].value != 'SHA1') {
url += '&algorithm=' + fe['algorithm'].value;
}
tokenqr.clear();
tokenqr.makeCode(url);
document.body.className = 'showqr';
showQr(url);
}
function onTypeChanged() {
@ -138,6 +196,7 @@ function onTypeChanged() {
* id is the index into the global tokens[].
*/
function editToken(id) {
if (document.body.className == 'export') return;
var p;
const selectMarkup = function(name, ary, cur, onchg) {
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + onchg + '"' : '') + '>';
@ -163,7 +222,7 @@ function editToken(id) {
markup += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
markup += '</td></tr>';
markup += '<tr><td>Hash:</td><td>';
markup += selectMarkup('algorithm', ['SHA1','SHA256','SHA512'], tokens[id].algorithm);
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
markup += '</td></tr>';
markup += '</tbody><tr><td id="advbtn" colspan="2">';
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
@ -171,9 +230,9 @@ function editToken(id) {
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
if (tokens[id].isnew) {
markup += '<button type="button" onclick="startScan()">Scan QR Code</button>';
markup += '<button type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
} else {
markup += '<button type="button" onclick="showQrCode()">Show QR Code</button>';
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>';
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
}
document.getElementById('edit').innerHTML = markup;
@ -188,6 +247,46 @@ function addToken() {
editToken(tokens.length - 1);
}
/* Convert a number to a proto3 varint.
*/
function int2proto3varint(val) {
var ret = '';
do {
let c = val & 0x7F;
val >>>= 7;
if (val > 0) {
c |= 0x80;
}
ret += String.fromCharCode(c);
} while (val > 0);
return ret;
}
/* Convert a string to a proto3 field.
*/
function str2proto3(field_number, str) {
return int2proto3varint((field_number << 3) + 2) + int2proto3varint(str.length) + str;
}
/* Convert a number to a proto3 field.
*/
function int2proto3(field_number, val) {
return int2proto3varint(field_number << 3) + int2proto3varint(val);
}
/* Convert the specified token to its proto3 representation.
*/
function token2proto3(id) {
var secret = str2proto3(1, b32decode(tokens[id].secret));
var name = str2proto3(2, (tokens[id].account == '') ? tokens[id].label : tokens[id].account);
var issuer = (tokens[id].issuer == '') ? '' : str2proto3(3, tokens[id].issuer);
var algorithm = int2proto3(4, (tokens[id].algorithm == 'SHA512') ? 3 : ((tokens[id].algorithm == 'SHA256') ? 2 : 1));
var digits = int2proto3(5, (tokens[id].digits == 8) ? 2 : 1);
var type = int2proto3(6, (tokens[id].period <= 0) ? 1 : 2);
var counter = (tokens[id].period <= 0) ? int2proto3(7, -tokens[id].period) : '';
return str2proto3(1, secret + name + issuer + algorithm + digits + type + counter);
}
/* Move the specified token up or down in the global tokens[].
* id is the index in the global tokens[] of the token to move.
* dir is the direction to move: -1=up, 1=down.
@ -200,10 +299,15 @@ function moveToken(id, dir) {
/* Update the display listing all the tokens.
*/
function updateTokens() {
const tokenSelect = function(id) {
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
};
const tokenButton = function(fn, id, label, dir) {
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
};
var markup = '<table><tr><th>Token</th><th colspan="2">Order</th></tr>';
var markup = '<table><tr><th>';
markup += tokenSelect('all');
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
/* any tokens marked new are cancelled new additions and must be removed */
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].isnew) {
@ -212,6 +316,8 @@ function updateTokens() {
}
for (let i = 0; i < tokens.length; i++) {
markup += '<tr><td>';
markup += tokenSelect(i);
markup += '</td><td>';
markup += tokenButton('editToken', i, tokens[i].label);
markup += '</td><td>';
if (i < (tokens.length - 1)) {
@ -224,14 +330,20 @@ function updateTokens() {
markup += '</td></tr>';
}
markup += '</table>';
markup += '<div class="select">';
markup += '<button type="button" onclick="addToken()">Add Token</button>';
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
markup += '<button type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
markup += '<button type="button" onclick="document.body.className=\'export\'">Export</button>';
markup += '</div><div class="export">';
markup += '<button type="button" onclick="document.body.className=\'select\'">Cancel</button>';
markup += '<button type="button" onclick="exportTokens(true, null)">Show QR</button>';
markup += '</div>';
document.getElementById('tokens').innerHTML = markup;
document.body.className = 'select';
}
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
qrcode.callback = res => {
function handleTokenQr(res) {
if (res) {
if (res.startsWith(otpAuthUrl)) {
res = decodeURIComponent(res);
@ -243,7 +355,8 @@ qrcode.callback = res => {
'counter':'0',
'period':'30',
'secret':'',
'issuer':''
'issuer':'',
'label':''
};
var otpok = true;
for (let pi in params) {
@ -261,8 +374,7 @@ qrcode.callback = res => {
if (otpok) {
scanning = false;
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')';
t['label'] = t['label'].substr(0, 10);
makeLabel(t);
var fe = document.forms['edittoken'].elements;
if (res.startsWith(otpAuthUrl + 'hotp/')) {
t['period'] = '30';
@ -283,8 +395,94 @@ qrcode.callback = res => {
}
}
}
}
function cancelTokenQr() {
scanning = false;
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
}
class proto3decoder {
constructor(str) {
this.buf = [];
for (let i in str) {
this.buf = this.buf.concat(str.charCodeAt(i));
}
}
getVarint() {
let c, ret = 0
do {
c = this.buf.shift();
ret = (ret << 7) | (c & 0x7F);
} while ((c & 0x80) != 0);
return ret;
}
getString(length) {
let ret = '';
for (let i = 0; i < length; ++i) {
ret += String.fromCharCode(this.buf.shift());
}
return ret;
}
parse() {
let ret = null;
if (this.buf.length > 0) {
let field_data = null;
let field_type = this.getVarint();
let field_number = field_type >>> 3;
let wire_type = field_type & 7;
switch (wire_type) {
case 0: field_data = this.getVarint(); break;
case 2: field_data = this.getString(this.getVarint()); break;
}
ret = {number:field_number,data:field_data};
}
return ret;
}
}
function handleImportQr(res) {
if (res) {
if (res.startsWith(otpMigrUrl)) {
scanning = false;
let data = new proto3decoder(atob(decodeURIComponent(res.substr(otpMigrUrl.length))));
while (data.buf.length > 0) {
let field = data.parse();
if (field?.number == 1) {
let newtoken = {'algorithm':'SHA1','digits':6,'period':30,'issuer':'','account':'','secret':'','label':''};
let p3token = new proto3decoder(field.data);
while (p3token.buf.length > 0) {
let buf = p3token.parse();
switch (buf?.number) {
case 1: newtoken.secret = b32encode(buf.data); break;
case 2: newtoken.account = buf.data; break;
case 3: newtoken.issuer = buf.data; break;
case 4: newtoken.algorithm = otpAlgos[buf.data - 1]; break;
case 5: newtoken.digits = (['6','8'])[buf.data - 1]; break;
case 7: newtoken.period = -buf.data; break;
}
}
makeLabel(newtoken);
tokens[tokens.length] = newtoken;
}
}
updateTokens();
}
}
}
function cancelImportQr() {
scanning = false;
document.body.className = 'select';
}
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
qrcode.callback = res => {
if (res) {
scanCallback(res);
if (scanning) {
scanBack();
}
}
};
function startScan() {
function startScan(handler,cancel) {
scanCallback = handler;
scanBack = cancel;
document.body.className = 'scanning';
navigator.mediaDevices
.getUserMedia({video:{facingMode:'environment'}})
@ -339,36 +537,93 @@ function saveTokens() {
Util.hideModal();
});
}
/* Handle token export.
* showqr is true if the QR code should be shown, if false the checkboxes need updating
* id is the name of the clicked checkbox, or null if the export button was pressed
*/
function exportTokens(showqr, id) {
let allchecked = true, allclear = true;
let cball;
let exp = '';
for (let cb of document.querySelectorAll('input[type=checkbox]')) {
let cbid = cb.name.substring(4);
if (cbid == 'all') {
cball = cb;
} else {
if (id == 'all') {
cb.checked = cball.checked;
} else {
if (cb.checked) {
if (showqr) {
exp += token2proto3(parseInt(cbid));
}
allclear = false;
} else {
allchecked = false;
}
}
}
}
if (id != 'all') {
if (allclear) {
cball.indeterminate = false;
cball.checked = false;
} else if (allchecked) {
cball.indeterminate = false;
cball.checked = true;
} else {
cball.indeterminate = true;
}
}
if (showqr) {
if (exp != '') {
/* add version, batch_size, batch_index, but no batch_id */
exp += int2proto3(2, 1) + int2proto3(3, 1) + int2proto3(4, 0);
let url = otpMigrUrl + encodeURIComponent(btoa(exp));
showQr(url);
}
}
}
function onInit() {
loadTokens();
updateTokens();
}
function qrBack() {
document.body.className = qrPreviousClass;
}
</script>
</head>
<body class="select">
<h1>Authentiwatch</h1>
<div id="tokens">
<p>No watch comms.</p>
</div>
<div id="scan">
<table>
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
<tr><td><button type="button" onclick="editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value))">Cancel</button></td></tr>
<tr><td><button type="button" onclick="scanBack()">Cancel</button></td></tr>
</table>
</div>
<div id="edit">
</div>
<div id="tokenqr">
<div id="showqr">
<table><tr><td id="qrcode"></td></tr><tr><td>
<button type="button" onclick="document.body.className='editing'">Back</button>
<button type="button" onclick="qrBack()">Back</button>
</td></tr></table>
</div>
</div>
<script type="text/javascript">
const video=document.createElement('video');
const canvasElement=document.getElementById('qr-canvas');
const canvas=canvasElement.getContext('2d');
let scanning=false;
const tokenqr=new QRCode(document.getElementById('qrcode'), '');
const tokenqr=new QRCode(document.getElementById('qrcode'), {width:354,height:354});
</script>
<script src="../../core/lib/interface.js"></script>
</body>

View File

@ -4,7 +4,7 @@
"shortName": "AuthWatch",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"version": "0.04",
"version": "0.05",
"description": "Google Authenticator compatible tool.",
"tags": "tool",
"interface": "interface.html",

1
apps/barometer/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Display pressure as number and hand

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4AVmczmALI7oWJgYXBmYLHhvd6AuKGBHdAAYXLDAwXRJIvd73u9oXSLoPuAAJhHOwYYGIYIXDGAwWGMIYvMC5QwBC4ZeMC4x3KL44XEU6KQEC5gAMCqoXZAH4AchAXWxAXWwBGWC62IC6sILywXXxAXUhWqzAXTCwIABOyYXD0AXSCwQABC/4XaO68JC6wYCCygA/AH4AGA"))

120
apps/barometer/app.js Normal file
View File

@ -0,0 +1,120 @@
const center = {
x: g.getWidth()/2,
y: g.getHeight()/2,
};
const MIN = 940;
const MAX = 1090;
const NUMBER_OF_VALUES = MAX - MIN;
const SCALE_TICK_STEP = 5;
const SCALE_VALUES_STEP = 25;
const NUMBER_OF_LABELS = NUMBER_OF_VALUES / SCALE_VALUES_STEP;
const NUMBER_OF_TICKS = NUMBER_OF_VALUES / SCALE_TICK_STEP;
const ZERO_OFFSET = (Math.PI / 4) * 3;
const SCALE_SPAN = (Math.PI / 2) * 3;
const TICK_LENGTH = 10;
const HAND_LENGTH = 45;
const HAND_WIDTH = 5;
function generatePoly(radius, width, angle){
const x = center.x + Math.cos(angle) * radius;
const y = center.y + Math.sin(angle) * radius;
const d = {
x: width/2 * Math.cos(angle + Math.PI/2),
y: width/2 * Math.sin(angle + Math.PI/2),
};
const poly = [center.x - d.x, center.y - d.y, center.x + d.x, center.y + d.y, x + d.x, y + d.y, x - d.x, y - d.y];
return poly;
}
function drawHand(value){
g.setColor(256, 0, 0);
g.setFontAlign(0,0);
g.setFont("Vector",15);
g.drawString(value, center.x, center.y * 2 - 15, true);
const angle = SCALE_SPAN / NUMBER_OF_VALUES * (value - MIN) + ZERO_OFFSET;
g.fillPoly(generatePoly(HAND_LENGTH, HAND_WIDTH, angle), true);
g.fillCircle(center.x ,center.y, 4);
}
function drawTicks(){
g.setColor(1,1,1);
for(let i= 0; i <= NUMBER_OF_TICKS; i++){
const angle = (i * (SCALE_SPAN/NUMBER_OF_TICKS)) + ZERO_OFFSET;
const tickWidth = i%5==0 ? 5 : 2;
g.fillPoly(generatePoly(center.x, tickWidth, angle), true);
}
g.setColor(0,0,0);
g.fillCircle(center.x,center.y,center.x - TICK_LENGTH);
}
function drawScaleLabels(){
g.setColor(1,1,1);
g.setFont("Vector",12);
let label = MIN;
for (let i=0;i <= NUMBER_OF_LABELS; i++){
const angle = (i * (SCALE_SPAN/NUMBER_OF_LABELS)) + ZERO_OFFSET;
const labelDimensions = g.stringMetrics(label);
const LABEL_PADDING = 5;
const radius = center.x - TICK_LENGTH - LABEL_PADDING;
const x = center.x + Math.cos(angle) * radius;
const y = center.y + Math.sin(angle) * radius;
const visualX = x > center.x ? x - labelDimensions.width : x + labelDimensions.width > center.x ? x - (labelDimensions.width / 2) : x;
const visualY = y >= center.y - labelDimensions.height / 2 ? y - labelDimensions.height / 2 : y;
g.drawString(label, visualX, visualY);
label += SCALE_VALUES_STEP;
}
}
function drawIcons() {
const sunIcon = {
width : 24, height : 24, bpp : 4,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAkP+ALeA40PAYf/BYv/CYYLBBwIICCQ4ACHI4ICEIgkEAg48GDApcFAoYPBBY5NDBZIjLHZpTLNZiDKTZSzMZZT7iA="))
};
g.drawImage(sunIcon, center.x + 15, center.y - 12);
const sunRainIcon = {
width : 24, height : 24, bpp : 4,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AB/wBbEPBAoGEDI/wh4jJBQIMJEgUP///IpAJCBgf/+ALCAQRJFAoIHECgI7FIYwSEHAoGBEQwsEDIJdHCYYLLFwwTEQQwGFQQQACYpYpLf0AAEA"))
};
g.drawImage(sunRainIcon, center.x - 12, 30);
const rainIcon = {
width : 24, height : 24, bpp : 4,
transparent : 0,
buffer : require("heatshrink").decompress(atob("ADnwBRP/AIQAGh4ZKA4YLLh//EwoTFh4GCCIIfGDAQ5DIQ5bIBbQvII4gAGWLwzBOoarLCw4RKLBAAgA"))
};
g.drawImage(rainIcon, center.x - 44, center.y - 12);
}
g.setBgColor(0,0,0);
g.clear();
drawTicks();
drawScaleLabels();
drawIcons();
try {
Bangle.getPressure().then(data => {
drawHand(Math.round(data.pressure));
});
} catch(e) {
print(e.message);
print("barometer not supporter, show a demo value");
drawHand(MIN);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

View File

@ -0,0 +1,15 @@
{ "id": "barometer",
"name": "Barometer",
"shortName":"Barometer",
"version":"0.01",
"description": "A simple barometer that displays the current air pressure",
"icon": "barometer.png",
"tags": "tool,outdoors",
"allow_emulator":true,
"screenshots" : [ { "url": "screenshot.png" } ],
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"barometer.app.js","url":"app.js"},
{"name":"barometer.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -14,3 +14,6 @@
Always emit BTHRM event
Cleanup promises code and allow to configure custom additional waiting times to work around bugs
Disconnect cleanly on exit
0.06: Fix bug if no request waiting time is set
Fix bug if no connection data was cached
Fix error during disconnect

View File

@ -365,18 +365,18 @@
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
promise = promise.then((d)=>{
log("Got device: ", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
}
promise = promise.then((d)=>{
log("Got device: ", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
@ -426,14 +426,14 @@
});
promise = promise.then(()=>{
var getCharacteristicsPromise = Promise.resolve();
var characteristicsPromise = Promise.resolve();
if (characteristics.length == 0){
getCharacteristicsPromise = getCharacteristicsPromise.then(()=>{
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
});
getCharacteristicsPromise = getCharacteristicsPromise().then((services)=>{
characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services:", services);
var result = Promise.resolve();
for (var service of services){
@ -453,11 +453,11 @@
} else {
for (var characteristic of characteristics){
getCharacteristicsPromise = attachCharacteristicPromise(getCharacteristicsPromise, characteristic, true);
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
return getCharacteristicsPromise;
return characteristicsPromise;
});
promise = promise.then(()=>{
@ -489,8 +489,8 @@
if (gatt.connected){
log("Disconnect with gatt: ", gatt);
gatt.disconnect().then(()=>{
log("Successful disconnect", e);
}).catch(()=>{
log("Successful disconnect");
}).catch((e)=>{
log("Error during disconnect", e);
});
}

View File

@ -2,7 +2,7 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.05",
"version": "0.06",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",

View File

@ -1,43 +1,43 @@
(function(back) {
Bangle.removeAllListeners('drag');
Bangle.setUI("");
var settings = require('Storage').readJSON('contourclock.json', true) || {};
if (settings.fontIndex==undefined) {
settings.fontIndex=0;
require('Storage').writeJSON("myapp.json", settings);
}
savedIndex=settings.fontIndex;
saveListener = setWatch(function() { //save changes and return to settings menu
require('Storage').writeJSON('contourclock.json', settings);
Bangle.removeAllListeners('swipe');
Bangle.removeAllListeners('lock');
clearWatch(saveListener);
g.clear();
back();
}, BTN, { repeat:false, edge:'falling' });
lockListener = Bangle.on('lock', function () { //discard changes and return to clock
settings.fontIndex=savedIndex;
require('Storage').writeJSON('contourclock.json', settings);
Bangle.removeAllListeners('swipe');
Bangle.removeAllListeners('lock');
clearWatch(saveListener);
g.clear();
load();
});
swipeListener = Bangle.on('swipe', function (direction) {
var fontName = require('contourclock').drawClock(settings.fontIndex+direction);
if (fontName) {
settings.fontIndex+=direction;
g.clearRect(0,0,g.getWidth()-1,16);
g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0);
} else {
require('contourclock').drawClock(settings.fontIndex);
}
});
g.reset();
g.clear();
g.setFont('6x8:2x2').setFontAlign(0,-1);
g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0);
g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36);
g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18);
Bangle.removeAllListeners('drag');
Bangle.setUI("");
var settings = require('Storage').readJSON('contourclock.json', true) || {};
if (settings.fontIndex==undefined) {
settings.fontIndex=0;
require('Storage').writeJSON("myapp.json", settings);
}
savedIndex=settings.fontIndex;
saveListener = setWatch(function() { //save changes and return to settings menu
require('Storage').writeJSON('contourclock.json', settings);
Bangle.removeAllListeners('swipe');
Bangle.removeAllListeners('lock');
clearWatch(saveListener);
g.clear();
back();
}, BTN, { repeat:false, edge:'falling' });
lockListener = Bangle.on('lock', function () { //discard changes and return to clock
settings.fontIndex=savedIndex;
require('Storage').writeJSON('contourclock.json', settings);
Bangle.removeAllListeners('swipe');
Bangle.removeAllListeners('lock');
clearWatch(saveListener);
g.clear();
load();
});
swipeListener = Bangle.on('swipe', function (direction) {
var fontName = require('contourclock').drawClock(settings.fontIndex+direction);
if (fontName) {
settings.fontIndex+=direction;
g.clearRect(0,0,g.getWidth()-1,16);
g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0);
} else {
require('contourclock').drawClock(settings.fontIndex);
}
});
g.reset();
g.clear();
g.setFont('6x8:2x2').setFontAlign(0,-1);
g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0);
g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36);
g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18);
})

View File

@ -5,3 +5,4 @@
0.05: Tweaks for 'HRM-raw' handling
0.06: Add widgets
0.07: Update scaling for new firmware
0.08: Don't force backlight on/watch unlocked on Bangle 2

View File

@ -1,5 +1,8 @@
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
if (process.env.HWVERSION == 1) {
Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
}
Bangle.setHRMPower(1);
var hrmInfo, hrmOffset = 0;
var hrmInterval;

View File

@ -1,7 +1,7 @@
{
"id": "hrm",
"name": "Heart Rate Monitor",
"version": "0.07",
"version": "0.08",
"description": "Measure your heart rate and see live sensor data",
"icon": "heartrate.png",
"tags": "health",

View File

@ -8,7 +8,7 @@
require("Storage").write("launch.json",settings);
}
const appMenu = {
/*LANG*/"": {"title": /*LANG*/"Launcher Settings"},
"": {"title": /*LANG*/"Launcher Settings"},
/*LANG*/"< Back": back,
/*LANG*/"Font": {
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"),

View File

@ -11,4 +11,5 @@
0.11: Show the gadgetbridge weather temperature (settings).
0.12: Added humidity as an option to display.
0.13: Improved battery visualization.
0.14: Added altitude as an option to display.
0.14: Added altitude as an option to display.
0.15: Using wpedom to count steps.

View File

@ -448,16 +448,17 @@ function draw(){
* Step counter via widget
*/
function getSteps() {
var steps = 0;
let health;
try {
health = require("health");
try{
if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom.getSteps();
}
} catch(ex) {
return steps;
// In case we failed, we can only show 0 steps.
}
health.readDay(new Date(), h=>steps+=h.steps);
return steps;
return 0;
}

View File

@ -3,7 +3,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.14",
"version":"0.15",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",

View File

@ -258,7 +258,7 @@ var locales = {
temperature: "°C",
ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%Y" }, // dimanche 1 mars 2020 // 01/03/2020
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%Y" }, // 1 mars 2020 // 01/03/2020
abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
abday: "dim,lun,mar,mer,jeu,ven,sam",

View File

@ -28,3 +28,5 @@
Spread message action buttons out
Back button now goes back to list of messages
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
0.19: Use a larger font for message text if it'll fit
0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)

View File

@ -26,19 +26,15 @@ When a new message is received:
When a message is shown, you'll see a screen showing the message title and text.
### Android
* The 'back-arrow' button goes back to Messages, marking the current message as read.
* If shown, the 'tick' button opens the notification on the phone
* If shown, the 'cross' button dismisses the notification on the phone
* The top-left icon shows more options, for instance deleting the message of marking unread
### iOS
* The 'back-arrow' button goes back to Messages, marking the current message as read.
* If shown, the 'tick' button responds positively to the notification (accept call/etc)
* If shown, the 'cross' button responds negatively to the notification (dismiss call/etc)
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
* The top-left icon shows more options, for instance deleting the message of marking unread
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
* If shown, the 'tick' button:
* **Android** opens the notification on the phone
* **iOS** responds positively to the notification (accept call/etc)
* If shown, the 'cross' button:
* **Android** dismisses the notification on the phone
* **iOS** responds negatively to the notification (dismiss call/etc)
## Images
_1. Screenshot of a notification_

View File

@ -198,9 +198,39 @@ function showMusicMessage(msg) {
layout.render();
}
function showMessageScroller(msg) {
var bodyFont = fontBig;
g.setFont(bodyFont);
var lines = [];
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10)
var titleCnt = lines.length;
if (titleCnt) lines.push(""); // add blank line after title
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
E.showScroller({
h : g.getFontHeight(), // height of each menu item in pixels
c : lines.length, // number of menu items
// a function to draw a menu item
draw : function(idx, r) {
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
g.setBgColor(idx<titleCnt ? colBg : g.theme.bg).clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
}, select : function(idx) {
if (idx>=lines.length-2)
showMessage(msg.id);
}
});
// ensure button-press on Bangle.js 2 takes us back
if (process.env.HWVERSION>1) Bangle.btnWatches = [
setWatch(() => showMessage(msg.id), BTN1, {repeat:1,edge:"falling"})
];
}
function showMessageSettings(msg) {
E.showMenu({"":{"title":/*LANG*/"Message"},
"< Back" : () => showMessage(msg.id),
/*LANG*/"View Message" : () => {
showMessageScroller(msg);
},
/*LANG*/"Delete" : () => {
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
saveMessages();
@ -245,12 +275,13 @@ function showMessage(msgid) {
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
}
}
function goBack() {
msg.new = false; saveMessages(); // read mail
cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
}
var buttons = [
{type:"btn", src:getBackImage(), cb:()=>{
msg.new = false; saveMessages(); // read mail
cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
}} // back
{type:"btn", src:getBackImage(), cb:goBack} // back
];
if (msg.positive) {
buttons.push({fillx:1});
@ -270,9 +301,18 @@ function showMessage(msgid) {
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
}});
}
var bodyFont = fontMedium;
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10);
var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
// If body of message is only two lines long w/ large font, use large font.
var body=msg.body, bodyFont = fontLarge, lines;
if (body) {
var w = g.getWidth()-48;
if (g.setFont(bodyFont).stringWidth(body) > w * 2)
bodyFont = fontMedium;
if (g.setFont(bodyFont).stringWidth(body) > w) {
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10);
body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
}
}
layout = new Layout({ type:"v", c: [
{type:"h", fillx:1, bgCol:colBg, c: [
{ type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{
@ -284,11 +324,18 @@ function showMessage(msgid) {
title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{},
]},
]},
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2 },
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{
// allow tapping to show a larger version
showMessageScroller(msg);
} },
{type:"h",fillx:1, c: buttons}
]});
g.clearRect(Bangle.appRect);
layout.render();
// ensure button-press on Bangle.js 2 takes us back
if (process.env.HWVERSION>1) Bangle.btnWatches = [
setWatch(goBack, BTN1, {repeat:1,edge:"falling"})
];
}

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.18",
"version": "0.20",
"description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png",
"type": "app",

View File

@ -11,3 +11,5 @@
0.11: Changed cycle on minute to prevInfo to avoid the 2nd one being the blank line
0.12: Removed dependancy on widpedom, now uses Bangle.getHealthStatus("day").steps
which requires 2.11.27 firmware to reset at midnight
0.13: call process.memory(false) to avoid triggering a GC of memory
supported in pre 2.12.13 firmware

View File

@ -2,7 +2,7 @@
"id": "pastel",
"name": "Pastel Clock",
"shortName": "Pastel",
"version": "0.12",
"version": "0.13",
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times. Requires firmware 2.11.27",
"icon": "pastel.png",
"dependencies": {"mylocation":"app","weather":"app"},

View File

@ -83,7 +83,7 @@ const infoData = {
ID_SS: { calc: () => 'Sunset: ' + sunSet },
ID_STEP: { calc: () => 'Steps: ' + getSteps() },
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' },
ID_MEM: { calc: () => {var val = process.memory(); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
ID_MEM: { calc: () => {var val = process.memory(false); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
ID_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} },
ID_FW: { calc: () => 'Fw: ' + process.env.VERSION }
};

View File

@ -13,3 +13,5 @@
Move recording for CoreTemp to its own app
0.08: Memory usage improvements for recorder app itself
0.09: Show correct number for log in overwrite prompt
0.10: Fix broken recorder settings (when launched from settings app)
0.11: Fix KML and GPX export when there is no GPS data

View File

@ -10,6 +10,9 @@
var domTracks = document.getElementById("tracks");
function saveKML(track,title) {
// only include data points with GPS values
track=track.filter(pt=>pt.Latitude!="" && pt.Longitude!="");
// Now output KML
var kml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
@ -37,7 +40,6 @@ ${track.map(pt=>` <when>${pt.Time.toISOString()}</when>\n`).join("")}
${track.map(pt=>` <gx:coord>${pt.Longitude} ${pt.Latitude} ${pt.Altitude}</gx:coord>\n`).join("")}
<ExtendedData>
<SchemaData schemaUrl="#schema">
${track[0].Heartrate!==undefined ? `<gx:SimpleArrayData name="heartrate">
${track.map(pt=>` <gx:value>${0|pt.Heartrate}</gx:value>\n`).join("")}
</gx:SimpleArrayData>`:``}
@ -80,7 +82,7 @@ function saveGPX(track, title) {
<name>${title}</name>
<trkseg>`;
track.forEach(pt=>{
gpx += `
if (pt.Latitude!="" && pt.Longitude!="") gpx += `
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
<ele>${pt.Altitude}</ele>
<time>${pt.Time.toISOString()}</time>
@ -122,6 +124,7 @@ function saveCSV(track, title) {
}
function trackLineToObject(headers, l) {
if (l===undefined) return {};
var t = l.trim().split(",");
var o = {};
headers.forEach((header,i) => o[header] = t[i]);
@ -155,7 +158,7 @@ function getTrackList() {
Util.showModal(`Loading Track ${trackNo}...`);
Puck.eval(`(function(fn) {
var f = require("Storage").open(fn,"r");
var headers = f.readLine();
var headers = f.readLine().trim();
var data = f.readLine();
var lIdx = headers.split(",").indexOf("Latitude");
if (lIdx >= 0) {
@ -184,14 +187,14 @@ function getTrackList() {
var html = `<div class="container">
<div class="columns">\n`;
trackList.forEach(track => {
var trackData = trackLineToObject(track.info.headers, track.info.l);
console.log("track", track);
var trackData = trackLineToObject(track.info.headers, track.info.l);
console.log("trackData", trackData);
html += `
<div class="column col-12">
<div class="card-header">
<div class="card-title h5">Track ${track.number}</div>
<div class="card-subtitle text-gray">${trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</div>
<div class="card-subtitle text-gray">${trackData.Time?trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }):"No track data"}</div>
</div>
${trackData.Latitude ? `
<div class="card-image">

View File

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

View File

@ -1,4 +1,4 @@
(function(back) {
// just go right to our app - we need all the memory
load("record.app.js");
})();
load("recorder.app.js");
})

View File

@ -10,3 +10,5 @@
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
1.06: Misc memory and screen optimisations.
1.10: Adds Kalman filter.
1.14: Add VMG and coordinates screens
1.43: Adds mirroring of the watch face to an Android device. See README.md

View File

@ -0,0 +1,273 @@
// Add this script to DroidScript on an android device.
// Uses the PuckJS plugin to provide mirroring of the GPS Adv Sports II Bangle app face onto an android device.
app.LoadPlugin("PuckJS");
//Called when application is started.
function OnStart() {
v = '1.49' // Version of this script
requiredBangleVer = '1.46'; // Minimum speedalt2 version required on Bangle
curBangleVer = '-.--'
isStopped = true; // Data receive turned off
lastData = new Date().getTime() / 1000; // Time of last data received
addr = ''; // Address of last connection
// Mode = 0 // 0=SPD, 1=ALT, 2=DST, 3=VMG, 4=POSN, 5=TIME
btnOff = '#175A63'
btnOn = '#4285F4'
col = new Array(['black'],['#64FF00'],['#FCFA00'],['#00E4FF']) // bg, main, units, wp - 0xFFFF,0x007F,0x0054,0x0054
// Connect to Bangle
puck = app.CreatePuckJS();
puck.SetOnConnect(onConnect); // Callback.
puck.SetOnReceive(readResponse); // Callback to capture console output from app.
puck.Scan("Bangle");
setInterval(checkConnection,5000) // Periodic check for data timeout and attempt a reconnect
// Controls
app.SetScreenMode("Full")
//Create a layout with objects vertically centered.
layVert = app.CreateLayout("Linear", "VCenter,FillXY")
layVert.SetPadding(0.02, 0.02, 0.02, 0.02);
layVert.SetBackColor(col[0])
//Create a text label and add it to layout.
val = app.CreateText('', -1, -1, "Html,Multiline") // main value
val.SetTextSize(120)
val.SetTextColor(col[1]) // green
layVert.AddChild(val)
val2 = app.CreateText('') // minor value or waypoint name
val2.SetTextSize(50)
val2.SetTextColor(col[3]) // cyan
layVert.AddChild(val2)
// Units and status text
layHor = app.CreateLayout("Linear", "Horizontal")
layHor.SetMargins(-1, -1, -1, 10, 'px')
unit = app.CreateText('')
unit.SetSize(200, -1, "px")
unit.SetTextSize(32)
unit.SetTextColor('#FCFA00') // yellow
layHor.AddChild(unit)
// mode = app.CreateText( '' ,.3,-1,"Center" )
mode = app.CreateText('', -1, -1)
mode.SetSize(200, -1, "px")
mode.SetTextSize(32)
mode.SetTextColor('#FCFA00') // yellow
layHor.AddChild(mode)
// sats = app.CreateText( '' ,.3,-1,"Right")
sats = app.CreateText('', -1, -1, "FillXY,Bottom")
sats.SetSize(200, -1, "px")
sats.SetTextSize(20)
sats.SetTextColor(col[3]) // cyan
layHor.AddChild(sats)
layVert.AddChild(layHor)
// Buttons
layBtn = app.CreateLayout("Linear", "Horizontal")
btnAbout = app.CreateButton("About");
btnAbout.SetOnTouch(btn_OnAbout);
btnAbout.SetBackColor(btnOff)
layBtn.AddChild(btnAbout);
btnStart = app.CreateButton("Start");
btnStart.SetOnTouch(btn_OnStart);
btnStart.SetBackColor(btnOff)
layBtn.AddChild(btnStart);
btnStop = app.CreateButton("Stop");
btnStop.SetOnTouch(btn_OnStop);
btnStop.SetBackColor(btnOff)
layBtn.AddChild(btnStop);
btnScan = app.CreateButton("Scan");
btnScan.SetOnTouch(btn_OnScan);
btnScan.SetBackColor(btnOff)
layBtn.AddChild(btnScan);
// Status 'LED'
led = app.AddCanvas(layBtn, 0.1, 0.1, "FillXY,Bottom")
layVert.AddChild(layBtn)
//Add layout to app.
app.AddLayout(layVert)
}
function readResponse(data) {
if (data.substring(0, 1) != '{') return; // ignore non JSON
btnStart.SetBackColor(btnOn)
lastData = new Date().getTime() / 1000; // Time of last data received
d = JSON.parse(data);
if ( ( d.id != 'speedalt2' ) || (parseFloat(d.v) < parseFloat(requiredBangleVer)) || (typeof(d.v) == 'undefined')) {
btn_OnStop()
app.Alert('The GPS Adv Sports II app on your Bangle must be at least version ' + requiredBangleVer, 'Bangle App Upgrade Required')
return
}
curBangleVer = d.v
if (parseFloat(v) < parseFloat(d.vd)) {
btn_OnStop()
app.Alert('This GPS Adv Sports II script must be at least version ' + d.vd, 'Droidscript script Upgrade Required')
return
}
isStopped = false; // Flag that we are running and receiving data
// Flash blue 'led' indicator when data packet received.
setLED(led,true,"#0051FF")
setTimeout(function() {setLED(led,false,-1)}, 500)
if (d.m == 0) { // Speed ( dont need pos or time here )
val.SetTextSize(120)
val2.SetTextSize(50)
val.SetText(d.sp)
val2.SetText('')
unit.SetText(d.spd_unit)
mode.SetText('SPD')
sats.SetText(d.sats)
}
if (d.m == 1) { // Alt
val.SetTextSize(120)
val2.SetTextSize(50)
val.SetText(d.al)
val2.SetText('')
unit.SetText(d.alt_unit)
mode.SetText('ALT')
sats.SetText(d.sats)
}
if (d.m == 2) { // Dist
val.SetTextSize(120)
val2.SetTextSize(50)
val.SetText(d.di)
val2.SetText(d.wp)
unit.SetText(d.dist_unit)
mode.SetText('DST')
sats.SetText(d.sats)
}
if (d.m == 3) { // VMG
val.SetTextSize(120)
val2.SetTextSize(50)
val.SetText(d.vmg)
val2.SetText(d.wp)
unit.SetText(d.spd_unit)
mode.SetText('VMG')
sats.SetText(d.sats)
}
if (d.m == 4) { // POS
val.SetTextSize(80)
val2.SetTextSize(10)
txt = d.lat +
' <font color='+col[2]+'><small><small>' +
d.ns +
"</small></small></font><br>" +
d.lon +
' <font color='+col[2]+'><small><small>' +
d.ew +
"</small></small></font>"
val.SetHtml(txt)
val2.SetText('')
unit.SetText('')
mode.SetText('')
sats.SetText(d.sats)
}
if (d.m == 5) { // Time
val.SetTextSize(90)
val2.SetTextSize(10)
dt = new Date();
val.SetText(String(dt.getHours()).padStart(2, '0') + "\n" + String(dt.getMinutes()).padStart(2, '0'))
val2.SetText('')
unit.SetText('')
mode.SetText('')
sats.SetText(d.sats)
}
}
function setLED(canvas,on,colour) {
if ( on ) {
canvas.SetPaintColor(colour)
canvas.DrawCircle(0.5, 0.5, 0.1)
}
else {
canvas.Clear()
}
canvas.Update()
}
function onConnect(name, address, bonded, rssi) {
addr = address
console.log( "Connected to " + address );
btn_OnStart() // Once connect tell app to start sending updates
}
// Periodic check for data timeout and attempt a reconnect
function checkConnection() {
if (isStopped) return
if ( parseFloat(new Date().getTime() / 1000) - lastData > 30 ) {
console.log( "Reconnecting to : "+addr);
// Flash orange 'led' indicator for connection attempts.
setLED(led,true,"#FC8A00")
setTimeout(function() {setLED(led,false,-1)}, 500)
puck.Connect(addr)
}
}
function btn_OnAbout() {
app.ShowPopup("GPS Adv Sports II\nAndroid Mirror : "+v+"\nBangle.js : "+curBangleVer,"Long")
}
function btn_OnStart() {
btnStart.SetBackColor(btnOn)
btnStop.SetBackColor(btnOff)
puck.SendCode('btOn(1)\n') // Enable the data send
isStopped = false
}
function btn_OnStop() {
btnStart.SetBackColor(btnOff)
btnStop.SetBackColor(btnOn)
puck.SendCode('btOn(0)\n') // Disable the data send
isStopped = true
val.SetText('')
val2.SetText('')
unit.SetText('')
mode.SetText('')
sats.SetText('')
}
function btn_OnScan() {
btnStart.SetBackColor(btnOff)
btnStop.SetBackColor(btnOff)
puck.Scan("Bangle");
}

View File

@ -1,12 +1,6 @@
# GPS Speed, Altimeter and Distance to Waypoint
# GPS Speed, Altitude, Distance and VMG
What is the difference between **GPS Adventure Sports** and **GPS Adventure Sports II** ?
**GPS Adventure Sports** has 3 screens, each of which display different sets of information.
**GPS Adventure Sports II** has 5 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, Position or Time.
In all other respect they perform the same functions.
**GPS Adventure Sports II** has 6 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, VMG to waypoint, Position or Time.
The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information.
@ -14,17 +8,17 @@ The waypoints list is the same as that used with the [GPS Navigation](https://ba
**BTN1** ( Speed and Altitude ) Short press < 2 secs toggles the display between last reading and maximum recorded. Long press > 2 secs resets the recorded maximum values.
**BTN1** ( Distance ) Select next waypoint. Last fix distance from selected waypoint is displayed.
**BTN1** ( Distance and VMG ) Select next waypoint. Last fix distance from selected waypoint or speed towards is displayed.
**BTN2** : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts.
**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, Position and Time
**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, VMG to waypoint, Position and Time
**BTN3** : Long press exit and return to watch.
**Touch Screen** If the 'Touch' setting is ON then :
Swipe Left/Right cycles between the five screens.
Swipe Left/Right cycles between the six screens.
Touch functions as BTN1 short press.
@ -51,9 +45,36 @@ When using the GPS Setup App this app switches the GPS to SuperE (default) mode
The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode.
## Velocity Made Good - VMG
This implementation of VMG is very simplistic and is simply the component of your current vector ( course and speed ) that is towards your selected waypoint. It is displayed as negative if you are moving away from the waypoint. For it to be displayed you must be moving and the GPS must be able to detemrine a course. If not it will display '---' as the VMG.
## Mirroring to Android
This feature is an optional extra to solve and enhance a specific requirement for me. While sailing the Bangle.js watch screen is very difficult to read in bright sunlight while wearing the polaroid prescription lenses that I require on the water. The solution is to mirror the Bangle.js screen to an android device with a daylight readable OLED screen that I keep in a clear waterproof case on the boat. Using this mirroring feature I can see the GPS Adv Sports II app easily at all times, either on my wrist or on the bigger android device while still having full control over the display via the watch buttons.
There is a caveat. While in use the watch GPS stays in SuperE mode in order to keep the android screen updates going which means a higher battery use on the Bangle.js.
How is this mirroring done?
Install Droidscript on your Android device. Must have BLE suport and the PuckJS plugin installed. The Droidscript script can be found in the BangleApps GIT repository : https://github.com/espruino/BangleApps/tree/master/apps/speedalt2
The Droidscript script file is called : **GPS Adv Sports II.js**
**Important Gotcha :** For the BLE comms to find and connect to the Bangle.js it must be paired with the Android device but **NOT** connected. The Bangle.js must have been set in the Bluetooth settings as connectable.
Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep.
When runnig a blue 'led' will flash each time a data packet is recieved to refresh the android display.
An orange 'led' will flash for each reconnection attempt if no data is received for 30 seconds. It will keep trying to reconnect so you can restart the Bangle, run another Bangle app or temprarily turn off bluetooth. The android mirror display will automatically reconnect when the GPS Adv Sports II app is running on the Bangle again. ( Designed to leave the Android device running as the display mirror in a sealed case all day while retaining the ability to do other functions on the Bangle.js and returning to the GPS Speed Alt II app. )
Android Screen Mirroring:<br>
![](screenmirror.jpg)<p>
## Waypoints
Waypoints are used in [D]istance mode. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode.
Waypoints are used in Distance and VMG modes. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode.
The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.)

View File

@ -2,8 +2,12 @@
Speed and Altitude [speedalt2]
Mike Bennett mike[at]kereru.com
1.10 : add inverted colours
1.14 : Add VMG screen
1.34 : Add bluetooth data stream for Droidscript
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
*/
var v = '1.10';
var v = '1.46';
var vDroid = '1.46'; // Required DroidScript program version
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
var KalmanFilter = (function () {
@ -180,14 +184,10 @@ let LED = // LED as minimal and only definition (as instance / singleton)
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
}, LED1 = LED; // LED1 as 'synonym' for LED
// Load fonts
//require("Font7x11Numeric7Seg").add(Graphics);
var lf = {fix:0,satellites:0};
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
var canDraw = 1;
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
var tmrLP; // Timer for delay in switching to low power after screen turns off
var maxSpd = 0;
@ -195,6 +195,8 @@ var maxAlt = 0;
var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
var bt = 0; // 0 = bluetooth data feed off. 1 = on
var btLast = 0; // time of last bt transmit
var wp = {}; // Waypoint to use for distance from cur position.
@ -215,16 +217,27 @@ function radians(a) {
return a*Math.PI/180;
}
function degrees(a) {
var d = a*180/Math.PI;
return (d+360)%360;
}
function bearing(a,b){
var delta = radians(b.lon-a.lon);
var alat = radians(a.lat);
var blat = radians(b.lat);
var y = Math.sin(delta) * Math.cos(blat);
var x = Math.cos(alat)*Math.sin(blat) -
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
return Math.round(degrees(Math.atan2(y, x)));
}
function distance(a,b){
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat);
// Distance in selected units
// Distance in metres
var d = Math.sqrt(x*x + y*y) * 6371000;
d = (d/parseFloat(cfg.dist)).toFixed(2);
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
return d;
}
@ -328,8 +341,8 @@ function drawClock() {
function drawWP(wp) {
buf.setColor(3);
buf.setFontAlign(0,1); //left, bottom
buf.setFontVector(48);
buf.drawString(wp,120,140);
buf.setFontVector(40);
buf.drawString(wp,120,132);
}
function drawSats(sats) {
@ -362,14 +375,18 @@ if ( emulator ) {
var m;
var sp = '---';
var al = '---';
var di = '---';
var age = '---';
var al = sp;
var di = sp;
var brg = ''; // bearing
var crs = ''; // course
var age = sp;
var lat = '---.--';
var ns = '';
var ew = '';
var lon = '---.--';
var sats = '---';
var sats = sp;
var vmg = sp;
// Waypoint name
var wpName = wp.name;
@ -387,21 +404,33 @@ if ( emulator ) {
lf.smoothed = 1;
if ( maxN <= 15 ) maxN++;
}
// Bearing to waypoint
brg = bearing(lf,wp);
// Current course
crs = lf.course;
// Relative angle to wp
var a = Math.max(crs,brg) - Math.min(crs,brg);
if ( a >= 180 ) a = 360 -a;
// Speed
if ( cfg.spd == 0 ) {
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
sp = parseFloat(m[1]);
cfg.spd_unit = m[2];
}
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
// vmg
if ( a >= 90 ) vmg = sp * Math.cos(radians(180-a)) * -1; // moving away from WP
else vmg = sp * Math.cos(radians(a)); // towards wp
if ( sp < 10 ) sp = sp.toFixed(1);
else sp = Math.round(sp);
if (isNaN(sp)) sp = '---';
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;
if ( Math.abs(vmg) >= 0.05 && Math.abs(vmg) < 10 ) vmg = vmg.toFixed(1);
else vmg = Math.round(vmg);
if (isNaN(vmg)) vmg = '---';
// Altitude
al = lf.alt;
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
@ -410,8 +439,11 @@ if ( emulator ) {
// Distance to waypoint
di = distance(lf,wp);
if (isNaN(di)) di = '--------';
di = (di/parseFloat(cfg.dist)).toFixed(2);
if ( di >= 100 ) di = parseFloat(di).toFixed(1);
if ( di >= 1000 ) di = parseFloat(di).toFixed(0);
if (isNaN(di)) di = '------';
// Age of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
@ -433,9 +465,30 @@ if ( emulator ) {
}
// Bluetooth send data
btSend({
id:'speedalt2',
v:v,
vd:vDroid,
m:cfg.modeA,
spd_unit:cfg.spd_unit,
alt_unit:cfg.alt_unit,
dist_unit:cfg.dist_unit,
wp:wpName,
sp:sp,
al:al,
di:di,
sats:sats,
vmg:vmg,
lat:lat,
lon:lon,
ns:ns,
ew:ew
});
if ( cfg.modeA == 0 ) {
// Speed
if ( showMax )
if ( showMax ) {
drawScrn({
val:maxSpd,
unit:cfg.spd_unit,
@ -444,7 +497,8 @@ if ( emulator ) {
max:'MAX',
wp:''
}); // Speed maximums
else
}
else {
drawScrn({
val:sp,
unit:cfg.spd_unit,
@ -453,11 +507,12 @@ if ( emulator ) {
max:'SPD',
wp:''
});
}
}
if ( cfg.modeA == 1 ) {
// Alt
if ( showMax )
if ( showMax ) {
drawScrn({
val:maxAlt,
unit:cfg.alt_unit,
@ -466,7 +521,8 @@ if ( emulator ) {
max:'MAX',
wp:''
}); // Alt maximums
else
}
else {
drawScrn({
val:al,
unit:cfg.alt_unit,
@ -475,6 +531,7 @@ if ( emulator ) {
max:'ALT',
wp:''
});
}
}
if ( cfg.modeA == 2 ) {
@ -490,6 +547,18 @@ if ( emulator ) {
}
if ( cfg.modeA == 3 ) {
// VMG
drawScrn({
val:vmg,
unit:cfg.spd_unit,
sats:sats,
age:age,
max:'VMG',
wp:wpName
});
}
if ( cfg.modeA == 4 ) {
// Position
drawPosn({
sats:sats,
@ -501,7 +570,7 @@ if ( emulator ) {
});
}
if ( cfg.modeA == 4 ) {
if ( cfg.modeA == 5 ) {
// Large clock
drawClock();
}
@ -510,14 +579,14 @@ if ( emulator ) {
function prevScrn() {
cfg.modeA = cfg.modeA-1;
if ( cfg.modeA < 0 ) cfg.modeA = 4;
if ( cfg.modeA < 0 ) cfg.modeA = 5;
savSettings();
onGPS(lf);
}
function nextScrn() {
cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 4 ) cfg.modeA = 0;
if ( cfg.modeA > 5 ) cfg.modeA = 0;
savSettings();
onGPS(lf);
}
@ -529,14 +598,14 @@ function nextFunc(dur) {
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values.
}
else if ( cfg.modeA == 2) nxtWp(); // Dist mode - Select next waypoint
else if ( cfg.modeA == 2 || cfg.modeA == 3) nxtWp(); // Dist or VMG mode - Select next waypoint
onGPS(lf);
}
function updateClock() {
if (!canDraw) return;
if ( cfg.modeA != 4 ) return;
if ( cfg.modeA != 5 ) return;
drawClock();
if ( emulator ) {maxSpd++;maxAlt++;}
}
@ -551,6 +620,7 @@ function startDraw(){
function stopDraw() {
canDraw=false;
if ( bt ) return; // If bt screen mirror to Droidscript in use then keep GPS in SuperE mode to keep screen updates going.
if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix.
}
@ -564,6 +634,20 @@ function setLpMode(m) {
gpssetup.setPowerMode({power_mode:m});
}
// == Droidscript bluetooth data
function btOn(b) {
bt = b; // Turn data transmit on/off
}
function btSend(dat) {
if ( ! bt ) return; // bt transmit off
var dur = getTime() - btLast;
if ( dur < 1.0 ) return; // Don't need to transmit more than every 1.0 secs.
btLast = getTime();
console.log(JSON.stringify(dat)); // transmit the data
}
// == Events
function setButtons(){
@ -593,17 +677,6 @@ function setButtons(){
setWatch(function(e){
nextScrn();
}, BTN3, {repeat:true,edge:"falling"});
/*
// Touch screen same as BTN1 short
setWatch(function(e){
nextFunc(1); // Same as BTN1 short
}, BTN4, {repeat:true,edge:"falling"});
setWatch(function(e){
nextFunc(1); // Same as BTN1 short
}, BTN5, {repeat:true,edge:"falling"});
*/
}
Bangle.on('lcdPower',function(on) {
@ -621,40 +694,22 @@ Bangle.on('swipe',function(dir) {
Bangle.on('touch', function(button){
if ( ! cfg.touch ) return;
nextFunc(0); // Same function as short BTN1
/*
switch(button){
case 1: // BTN4
console.log('BTN4');
prevScrn();
break;
case 2: // BTN5
console.log('BTN5');
nextScrn();
break;
case 3:
console.log('MDL');
nextFunc(0); // Centre - same function as short BTN1
break;
}
*/
});
});
// == Main Prog
// Read settings.
let cfg = require('Storage').readJSON('speedalt2.json',1)||{};
cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit
cfg.spd = cfg.spd||1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = cfg.spd_unit||'kph'; // Displayed speed unit
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units
cfg.colour = cfg.colour||0; // Colour scheme.
cfg.wp = cfg.wp||0; // Last selected waypoint for dist
cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3=Position 4=Clock
cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock
cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;

View File

@ -2,7 +2,7 @@
"id": "speedalt2",
"name": "GPS Adventure Sports II",
"shortName":"GPS Adv Sport II",
"version":"1.10",
"version":"1.46",
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
"icon": "app.png",
"type": "app",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -48,7 +48,7 @@
const unitsMenu = {
'': {'title': 'Units'},
'< Back': function() { E.showMenu(appMenu); },
'default (spd)' : function() { setUnits(0,''); },
// 'default (spd)' : function() { setUnits(0,''); },
'Kph (spd)' : function() { setUnits(1,'kph'); },
'Knots (spd)' : function() { setUnits(1.852,'kts'); },
'Mph (spd)' : function() { setUnits(1.60934,'mph'); },

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
0.03: Add Color Changing Settings
0.04: Add Support For Bangle.js 2

View File

@ -14,5 +14,6 @@ g.setColor(settings.bg);
g.fillRect(0,0,g.getWidth(),g.getHeight());
// Any button turns off
setWatch(()=>load(), BTN1);
setWatch(()=>load(), BTN2);
setWatch(()=>load(), BTN3);
if (global.BTN2) setWatch(()=>load(), BTN2);
if (global.BTN3) setWatch(()=>load(), BTN3);

View File

@ -2,14 +2,14 @@
"id": "torch",
"name": "Torch",
"shortName": "Torch",
"version": "0.03",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets. You can also set the color through the apps settings menu.",
"version": "0.04",
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets on Bangle.js 1. You can also set the color through the app's setting menu.",
"icon": "app.png",
"tags": "tool,torch",
"supports": ["BANGLEJS"],
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"torch.app.js","url":"app.js"},
{"name":"torch.wid.js","url":"widget.js"},
{"name":"torch.wid.js","url":"widget.js","supports": ["BANGLEJS"]},
{"name":"torch.img","url":"app-icon.js","evaluate":true},
{"name":"torch.settings.js","url":"settings.js"}
]

View File

@ -1,3 +1,4 @@
0.01: Created
0.02: Set sort order to -10 so always display in right hand corner
0.03: Set sort order from the code
0.04: fix vertical align

View File

@ -4,7 +4,7 @@
"shortName":"Battery Theme",
"icon": "widbata.png",
"screenshots": [{"url":"screenshot_widbata_1.png"}],
"version":"0.03",
"version":"0.04",
"type": "widget",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",

View File

@ -5,7 +5,7 @@ Bangle.on('lcdPower', function(on) {
WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() {
var s = 26;
var t = 13; // thickness
var x = this.x, y = this.y;
var x = this.x, y = this.y + 5;
g.reset();
g.setColor(g.theme.fg);
g.fillRect(x,y+2,x+s-4,y+2+t); // outer

View File

@ -157,19 +157,29 @@ log(untranslatedStrings.filter(e => e.uses>2).filter(e => !translatedStrings.fin
log("");
//process.exit(1);
var languages = JSON.parse(fs.readFileSync(BASEDIR+"/lang/index.json").toString());
let languages = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/index.json`).toString());
languages.forEach(language => {
if (language.code=="en_GB") {
console.log("Ignoring "+language.code);
if (language.code == "en_GB") {
console.log(`Ignoring ${language.code}`);
return;
}
console.log("Scanning "+language.code);
console.log(`Scanning ${language.code}`);
log(language.code);
log("==========");
var translations = JSON.parse(fs.readFileSync(BASEDIR+"/lang/"+language.url).toString());
translatedStrings.forEach(str => {
if (!translations.GLOBAL[str.str])
console.log(`Missing translation for ${JSON.stringify(str)}`);
let translations = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/${language.url}`).toString());
translatedStrings.forEach(translationItem => {
if (!translations.GLOBAL[translationItem.str]) {
console.log(`Missing GLOBAL translation for ${JSON.stringify(translationItem)}`);
translationItem.files.forEach(file => {
let m = file.match(/\/([a-zA-Z0-9_-]*)\//g);
if (m && m[0]) {
let appName = m[0].replaceAll("/", "");
if (translations[appName] && translations[appName][translationItem.str]) {
console.log(` but LOCAL translation found in \"${appName}\"`);
}
}
});
}
});
log("");
});

2
core

@ -1 +1 @@
Subproject commit c243e6e71f88358de720ad16ba8515b32b8d650f
Subproject commit 187af1527e0b830c804049aae834ed658ffeed08

View File

@ -1,21 +1,148 @@
{
"//":"Italian language translations",
"//1": "Italian language translations",
"GLOBAL": {
"//":"Translations that apply for all apps",
"Alarms" : "Sveglie",
"Hours" : "Ore",
"Minutes" : "Minuti",
"Enabled" : "Attiva",
"New Alarm" : "Nuova sveglia",
"Save" : "Salva",
"Back" : "Indietro",
"Repeat" : "Ripeti",
"Delete" : "Cancella",
"ALARM!" : "SVEGLIA!",
"Sleep" : "Dormi"
"//": "Translations that apply for all apps",
"On": "On",
"on": "on",
"Off": "Off",
"off": "off",
"Ok": "Ok",
"Yes": "Sì",
"No": "No",
"Alarm": "Sveglia",
"ALARM": "SVEGLIA",
"Alarms": "Sveglie",
"Date": "Data",
"Year": "Anno",
"Month": "Mese",
"Day": "Giorno",
"Hour": "Ora",
"Hours": "Ore",
"Minute": "Minuto",
"Minutes": "Minuti",
"Second": "Secondo",
"Seconds": "Secondi",
"week": "settimana",
"Week": "Settimana",
"Enabled": "Attivo/a",
"New Alarm": "Nuova sveglia",
"Save": "Salva",
"Cancel": "Annulla",
"Back": "Indietro",
"Repeat": "Ripeti",
"Delete": "Cancella",
"ALARM!": "SVEGLIA!",
"Sleep": "Dormi",
"Timer": "Timer",
"TIMER": "TIMER",
"New Timer": "Nuovo timer",
"(repeat)": "(ripeti)",
"Auto snooze": "Posticipa automaticamente",
"Connected": "Connesso",
"Delete all messages": "Cancella tutti i messaggi",
"Delete All Messages": "Cancella tutti i messaggi",
"Message": "Messaggio",
"Messages": "Messaggi",
"No Messages": "Nessun messaggio",
"Keep Msgs": "Tieni i messaggi",
"Mark Unread": "Segna come non letto",
"Vibrate": "Vibrazione",
"Are you sure": "Sei sicuro/a",
"Music": "Musica",
"Apps": "App",
"App Settings": "Impostazioni app",
"Bluetooth": "Bluetooth",
"BLE": "BLE",
"Make Connectable": "Rendi collegabile",
"Programmable": "Programmabile",
"Remove": "Rimuovi",
"Utils": "Utilità",
"LCD": "LCD",
"LCD Brightness": "Luminosità LCD",
"LCD Timeout": "Timeout LCD",
"Wake on BTN1": "Risveglia con BTN1",
"Wake on BTN2": "Risveglia con BTN2",
"Wake on BTN3": "Risveglia con BTN3",
"Wake on FaceUp": "Risveglia a faccia in su",
"Wake on Touch": "Risveglia al tocco",
"Wake on Twist": "Risveglia con polso",
"Twist Timeout": "Timeout torsione",
"Twist Max Y": "Torsione Y max",
"Twist Threshold": "Soglia torsione",
"Customize": "Personalizza",
"Add Device": "Aggiungi dispositivo",
"Left": "Sinistra",
"Right": "Destra",
"Widgets": "Widget",
"Settings": "Impostazioni",
"No app has settings": "Non ci sono app con delle impostazioni",
"System": "Sistema",
"Alerts": "Avvisi",
"Theme": "Tema",
"Foreground": "Primo piano",
"Background": "Sfondo",
"Foreground 2": "Primo piano 2",
"Background 2": "Sfondo 2",
"Highlight FG": "Selezione PP",
"Highlight BG": "Selezione Sf",
"Utilities": "Utilità",
"Storage": "Memoria",
"Compact Storage": "Compatta memoria",
"Select Clock": "Seleziona orologio",
"No Clocks Found": "Nessun orologio trovato",
"Locale": "Localizzazione",
"Set Time": "Imposta orario",
"Time Zone": "Fuso orario",
"Whitelist": "Whitelist",
"Quiet Mode": "Modalità silenziosa",
"Disable": "Disabilita",
"Vibration": "Vibrazione",
"Show": "Mostra",
"Hide": "Nascondi",
"Rewrite Settings": "Riscrivi impostazioni",
"Reset Settings": "Reset impostazioni",
"Factory Reset": "Ripristino condizioni iniziali",
"Flatten Battery": "Scarica la batteria",
"Turn Off": "Spegni",
"This will remove everything": "Questo rimuoverà TUTTO",
"Error in settings": "Errore nelle impostazioni",
"Invalid settings": "Impostazioni non valide",
"Loading": "Caricamento",
"Launcher Settings": "Impostazioni Launcher",
"Font": "Font",
"Show clocks": "Mostra orologi",
"Log": "Log",
"Steps": "Passi",
"steps": "passi"
},
"alarm": {
"//":"App-specific overrides",
"rpt" : "ripeti"
"//2": "App-specific overrides",
"launch": {
"Vector font size": "Dim. font vett.",
"App Source\nNot found": "Codice app\nnon trovato"
},
"messages": {
"Unread timer": "Timer msg non letti"
},
"run": {
"Record Run": "Registra corsa"
},
"setting": {
"Clock Style": "Formato ora",
"Compacting...\nTakes approx\n1 minute": "Compattamento in corso...\nCi vorrà circa un minuto",
"//1": "The new line before 'operazione' improves the layout",
"Flattening battery - this can take hours.\nLong-press button to cancel": "Scaricamento batteria in corso - l'\noperazione può richiedere ore. Tieni premuto il pulsante per annullare",
"Reset to Defaults": "Ripristinare le impostazioni predefinite",
"Connectable": "Collegamento",
"Connect device\nto add to\nwhitelist": "Collega un\ndispositivo\nper metterlo\nin whitelist",
"Stay Connectable": "Rimanere collegabile",
"Light BW": "Chiaro",
"Dark BW": "Scuro"
},
"wid_edit": {
"Reset": "Ripristina",
"Reset All": "Ripristina tutto",
"Reset all widgets": "Ripristina tutti i widget",
"Sort Order": "Ordinamento",
"Side": "Lato"
}
}

View File

@ -164,7 +164,7 @@ function Layout(layout, options) {
// Handler for touch events
function touchHandler(l,e) {
if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) {
if (l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) {
if (e.type==2 && l.cbl) l.cbl(e); else if (l.cb) l.cb(e);
}
if (l.c) l.c.forEach(n => touchHandler(n,e));