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

View File

@ -2,3 +2,4 @@
0.02: Fix JSON save format 0.02: Fix JSON save format
0.03: Add "Calculating" placeholder, update 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.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 * GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here
* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/ * 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 ## Supports
* Google Authenticator compatible 2-factor authentication * Google Authenticator compatible 2-factor authentication
@ -14,8 +23,8 @@
* Between 6 and 10 digits * Between 6 and 10 digits
* Phone/PC configuration web page: * Phone/PC configuration web page:
* Add/edit/delete/arrange tokens * Add/edit/delete/arrange tokens
* Scan QR codes * Scan token and migration(import) QR codes
* Produce scannable QR codes * Produce scannable token and migration(export) QR codes
## Usage ## Usage
@ -24,6 +33,8 @@
* Swipe right to exit to the app launcher. * Swipe right to exit to the app launcher.
* Swipe left on selected counter token to advance the counter to the next value. * Swipe left on selected counter token to advance the counter to the next value.
![Screenshot](screenshot.png)
## Creator ## Creator
Andrew Gregory (andrew.gregory at gmail) Andrew Gregory (andrew.gregory at gmail)

View File

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

View File

@ -7,7 +7,10 @@
<style type="text/css"> <style type="text/css">
body{font-family:sans-serif} body{font-family:sans-serif}
body div{display:none} 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 th,#tokens td{padding:5px}
#tokens tr:nth-child(odd){background-color:#ccc} #tokens tr:nth-child(odd){background-color:#ccc}
#tokens tr:nth-child(even){background-color:#eee} #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 */ /* Start of all TOTP URLs */
const otpAuthUrl = 'otpauth://'; 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)']; const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
/* Settings */ /* Settings */
@ -45,6 +54,8 @@ var tokens = settings.tokens;
*/ */
function base32clean(val, nows) { function base32clean(val, nows) {
var ret = val.replaceAll(/\s+/g, ' '); 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, ''); ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
if (nows) { if (nows) {
ret = ret.replaceAll(/\s+/g, ''); ret = ret.replaceAll(/\s+/g, '');
@ -52,6 +63,48 @@ function base32clean(val, nows) {
return ret; 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. /* Save changes to a token to the global tokens[] array.
* id is the index into the global tokens[]. * id is the index into the global tokens[].
* forget is a flag indicating if the token should be forgotten. * 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. /* Generate and display a QR-code representing the current token.
*/ */
function showQrCode() { function showTokenQr() {
var fe = document.forms['edittoken'].elements; var fe = document.forms['edittoken'].elements;
var url = new String(otpAuthUrl); var url = new String(otpAuthUrl);
switch (fe['type'].value) { switch (fe['type'].value) {
@ -122,9 +182,7 @@ function showQrCode() {
if (fe['algorithm'].value != 'SHA1') { if (fe['algorithm'].value != 'SHA1') {
url += '&algorithm=' + fe['algorithm'].value; url += '&algorithm=' + fe['algorithm'].value;
} }
tokenqr.clear(); showQr(url);
tokenqr.makeCode(url);
document.body.className = 'showqr';
} }
function onTypeChanged() { function onTypeChanged() {
@ -138,6 +196,7 @@ function onTypeChanged() {
* id is the index into the global tokens[]. * id is the index into the global tokens[].
*/ */
function editToken(id) { function editToken(id) {
if (document.body.className == 'export') return;
var p; var p;
const selectMarkup = function(name, ary, cur, onchg) { const selectMarkup = function(name, ary, cur, onchg) {
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + 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 += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
markup += '</td></tr>'; markup += '</td></tr>';
markup += '<tr><td>Hash:</td><td>'; 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 += '</td></tr>';
markup += '</tbody><tr><td id="advbtn" colspan="2">'; markup += '</tbody><tr><td id="advbtn" colspan="2">';
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>'; 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="updateTokens()">Cancel Edit</button>';
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>'; markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
if (tokens[id].isnew) { 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 { } 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>'; markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
} }
document.getElementById('edit').innerHTML = markup; document.getElementById('edit').innerHTML = markup;
@ -188,6 +247,46 @@ function addToken() {
editToken(tokens.length - 1); 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[]. /* Move the specified token up or down in the global tokens[].
* id is the index in the global tokens[] of the token to move. * id is the index in the global tokens[] of the token to move.
* dir is the direction to move: -1=up, 1=down. * 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. /* Update the display listing all the tokens.
*/ */
function updateTokens() { function updateTokens() {
const tokenSelect = function(id) {
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
};
const tokenButton = function(fn, id, label, dir) { const tokenButton = function(fn, id, label, dir) {
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>'; 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 */ /* any tokens marked new are cancelled new additions and must be removed */
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
if (tokens[i].isnew) { if (tokens[i].isnew) {
@ -212,6 +316,8 @@ function updateTokens() {
} }
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
markup += '<tr><td>'; markup += '<tr><td>';
markup += tokenSelect(i);
markup += '</td><td>';
markup += tokenButton('editToken', i, tokens[i].label); markup += tokenButton('editToken', i, tokens[i].label);
markup += '</td><td>'; markup += '</td><td>';
if (i < (tokens.length - 1)) { if (i < (tokens.length - 1)) {
@ -224,14 +330,20 @@ function updateTokens() {
markup += '</td></tr>'; markup += '</td></tr>';
} }
markup += '</table>'; markup += '</table>';
markup += '<div class="select">';
markup += '<button type="button" onclick="addToken()">Add Token</button>'; markup += '<button type="button" onclick="addToken()">Add Token</button>';
markup += '<button type="button" onclick="saveTokens()">Save to watch</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.getElementById('tokens').innerHTML = markup;
document.body.className = 'select'; document.body.className = 'select';
} }
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */ function handleTokenQr(res) {
qrcode.callback = res => {
if (res) { if (res) {
if (res.startsWith(otpAuthUrl)) { if (res.startsWith(otpAuthUrl)) {
res = decodeURIComponent(res); res = decodeURIComponent(res);
@ -243,7 +355,8 @@ qrcode.callback = res => {
'counter':'0', 'counter':'0',
'period':'30', 'period':'30',
'secret':'', 'secret':'',
'issuer':'' 'issuer':'',
'label':''
}; };
var otpok = true; var otpok = true;
for (let pi in params) { for (let pi in params) {
@ -261,8 +374,7 @@ qrcode.callback = res => {
if (otpok) { if (otpok) {
scanning = false; scanning = false;
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value)); editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')'; makeLabel(t);
t['label'] = t['label'].substr(0, 10);
var fe = document.forms['edittoken'].elements; var fe = document.forms['edittoken'].elements;
if (res.startsWith(otpAuthUrl + 'hotp/')) { if (res.startsWith(otpAuthUrl + 'hotp/')) {
t['period'] = '30'; 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'; document.body.className = 'scanning';
navigator.mediaDevices navigator.mediaDevices
.getUserMedia({video:{facingMode:'environment'}}) .getUserMedia({video:{facingMode:'environment'}})
@ -339,36 +537,93 @@ function saveTokens() {
Util.hideModal(); 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() { function onInit() {
loadTokens(); loadTokens();
updateTokens(); updateTokens();
} }
function qrBack() {
document.body.className = qrPreviousClass;
}
</script> </script>
</head> </head>
<body class="select"> <body class="select">
<h1>Authentiwatch</h1> <h1>Authentiwatch</h1>
<div id="tokens"> <div id="tokens">
<p>No watch comms.</p> <p>No watch comms.</p>
</div> </div>
<div id="scan"> <div id="scan">
<table> <table>
<tr><td><canvas id="qr-canvas"></canvas></td></tr> <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> </table>
</div> </div>
<div id="edit"> <div id="edit">
</div> </div>
<div id="tokenqr">
<div id="showqr">
<table><tr><td id="qrcode"></td></tr><tr><td> <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> </td></tr></table>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
const video=document.createElement('video'); const video=document.createElement('video');
const canvasElement=document.getElementById('qr-canvas'); const canvasElement=document.getElementById('qr-canvas');
const canvas=canvasElement.getContext('2d'); const canvas=canvasElement.getContext('2d');
let scanning=false; let scanning=false;
const tokenqr=new QRCode(document.getElementById('qrcode'), ''); const tokenqr=new QRCode(document.getElementById('qrcode'), {width:354,height:354});
</script> </script>
<script src="../../core/lib/interface.js"></script> <script src="../../core/lib/interface.js"></script>
</body> </body>

View File

@ -4,7 +4,7 @@
"shortName": "AuthWatch", "shortName": "AuthWatch",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"version": "0.04", "version": "0.05",
"description": "Google Authenticator compatible tool.", "description": "Google Authenticator compatible tool.",
"tags": "tool", "tags": "tool",
"interface": "interface.html", "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 Always emit BTHRM event
Cleanup promises code and allow to configure custom additional waiting times to work around bugs Cleanup promises code and allow to configure custom additional waiting times to work around bugs
Disconnect cleanly on exit 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){ if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request"); 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 { } else {
promise = Promise.resolve(); promise = Promise.resolve();
@ -426,14 +426,14 @@
}); });
promise = promise.then(()=>{ promise = promise.then(()=>{
var getCharacteristicsPromise = Promise.resolve(); var characteristicsPromise = Promise.resolve();
if (characteristics.length == 0){ if (characteristics.length == 0){
getCharacteristicsPromise = getCharacteristicsPromise.then(()=>{ characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services"); log("Getting services");
return gatt.getPrimaryServices(); return gatt.getPrimaryServices();
}); });
getCharacteristicsPromise = getCharacteristicsPromise().then((services)=>{ characteristicsPromise = characteristicsPromise.then((services)=>{
log("Got services:", services); log("Got services:", services);
var result = Promise.resolve(); var result = Promise.resolve();
for (var service of services){ for (var service of services){
@ -453,11 +453,11 @@
} else { } else {
for (var characteristic of characteristics){ for (var characteristic of characteristics){
getCharacteristicsPromise = attachCharacteristicPromise(getCharacteristicsPromise, characteristic, true); characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
} }
} }
return getCharacteristicsPromise; return characteristicsPromise;
}); });
promise = promise.then(()=>{ promise = promise.then(()=>{
@ -489,8 +489,8 @@
if (gatt.connected){ if (gatt.connected){
log("Disconnect with gatt: ", gatt); log("Disconnect with gatt: ", gatt);
gatt.disconnect().then(()=>{ gatt.disconnect().then(()=>{
log("Successful disconnect", e); log("Successful disconnect");
}).catch(()=>{ }).catch((e)=>{
log("Error during disconnect", e); log("Error during disconnect", e);
}); });
} }

View File

@ -2,7 +2,7 @@
"id": "bthrm", "id": "bthrm",
"name": "Bluetooth Heart Rate Monitor", "name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM", "shortName": "BT HRM",
"version": "0.05", "version": "0.06",
"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": "app", "type": "app",

View File

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

View File

@ -5,3 +5,4 @@
0.05: Tweaks for 'HRM-raw' handling 0.05: Tweaks for 'HRM-raw' handling
0.06: Add widgets 0.06: Add widgets
0.07: Update scaling for new firmware 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); if (process.env.HWVERSION == 1) {
Bangle.setLCDTimeout(0); Bangle.setLCDPower(1);
Bangle.setLCDTimeout(0);
}
Bangle.setHRMPower(1); Bangle.setHRMPower(1);
var hrmInfo, hrmOffset = 0; var hrmInfo, hrmOffset = 0;
var hrmInterval; var hrmInterval;

View File

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

View File

@ -8,7 +8,7 @@
require("Storage").write("launch.json",settings); require("Storage").write("launch.json",settings);
} }
const appMenu = { const appMenu = {
/*LANG*/"": {"title": /*LANG*/"Launcher Settings"}, "": {"title": /*LANG*/"Launcher Settings"},
/*LANG*/"< Back": back, /*LANG*/"< Back": back,
/*LANG*/"Font": { /*LANG*/"Font": {
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"), 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.11: Show the gadgetbridge weather temperature (settings).
0.12: Added humidity as an option to display. 0.12: Added humidity as an option to display.
0.13: Improved battery visualization. 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 * Step counter via widget
*/ */
function getSteps() { function getSteps() {
var steps = 0; try{
let health; if (WIDGETS.wpedom !== undefined) {
try { return WIDGETS.wpedom.getSteps();
health = require("health"); } else if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom.getSteps();
}
} catch(ex) { } catch(ex) {
return steps; // In case we failed, we can only show 0 steps.
} }
health.readDay(new Date(), h=>steps+=h.steps); return 0;
return steps;
} }

View File

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

View File

@ -258,7 +258,7 @@ var locales = {
temperature: "°C", temperature: "°C",
ampm: { 0: "", 1: "" }, ampm: { 0: "", 1: "" },
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, 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", 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", month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
abday: "dim,lun,mar,mer,jeu,ven,sam", abday: "dim,lun,mar,mer,jeu,ven,sam",

View File

@ -28,3 +28,5 @@
Spread message action buttons out Spread message action buttons out
Back button now goes back to list of messages 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) 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. When a message is shown, you'll see a screen showing the message title and text.
### Android * The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
* 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 top-left icon shows more options, for instance deleting the message of marking unread * 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 ## Images
_1. Screenshot of a notification_ _1. Screenshot of a notification_

View File

@ -198,9 +198,39 @@ function showMusicMessage(msg) {
layout.render(); 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) { function showMessageSettings(msg) {
E.showMenu({"":{"title":/*LANG*/"Message"}, E.showMenu({"":{"title":/*LANG*/"Message"},
"< Back" : () => showMessage(msg.id), "< Back" : () => showMessage(msg.id),
/*LANG*/"View Message" : () => {
showMessageScroller(msg);
},
/*LANG*/"Delete" : () => { /*LANG*/"Delete" : () => {
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
saveMessages(); saveMessages();
@ -245,12 +275,13 @@ function showMessage(msgid) {
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n"); 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 = [ var buttons = [
{type:"btn", src:getBackImage(), cb:()=>{ {type:"btn", src:getBackImage(), cb:goBack} // back
msg.new = false; saveMessages(); // read mail
cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
}} // back
]; ];
if (msg.positive) { if (msg.positive) {
buttons.push({fillx:1}); buttons.push({fillx:1});
@ -270,9 +301,18 @@ function showMessage(msgid) {
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
}}); }});
} }
var bodyFont = fontMedium; // If body of message is only two lines long w/ large font, use large font.
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10); var body=msg.body, bodyFont = fontLarge, lines;
var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n"); 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: [ layout = new Layout({ type:"v", c: [
{type:"h", fillx:1, bgCol:colBg, c: [ {type:"h", fillx:1, bgCol:colBg, c: [
{ type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{ { 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 }:{}, 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} {type:"h",fillx:1, c: buttons}
]}); ]});
g.clearRect(Bangle.appRect); g.clearRect(Bangle.appRect);
layout.render(); 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", "id": "messages",
"name": "Messages", "name": "Messages",
"version": "0.18", "version": "0.20",
"description": "App to display notifications from iOS and Gadgetbridge", "description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png", "icon": "app.png",
"type": "app", "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.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 0.12: Removed dependancy on widpedom, now uses Bangle.getHealthStatus("day").steps
which requires 2.11.27 firmware to reset at midnight 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", "id": "pastel",
"name": "Pastel Clock", "name": "Pastel Clock",
"shortName": "Pastel", "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", "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", "icon": "pastel.png",
"dependencies": {"mylocation":"app","weather":"app"}, "dependencies": {"mylocation":"app","weather":"app"},

View File

@ -83,7 +83,7 @@ const infoData = {
ID_SS: { calc: () => 'Sunset: ' + sunSet }, ID_SS: { calc: () => 'Sunset: ' + sunSet },
ID_STEP: { calc: () => 'Steps: ' + getSteps() }, ID_STEP: { calc: () => 'Steps: ' + getSteps() },
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' }, 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_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} },
ID_FW: { calc: () => 'Fw: ' + process.env.VERSION } ID_FW: { calc: () => 'Fw: ' + process.env.VERSION }
}; };

View File

@ -13,3 +13,5 @@
Move recording for CoreTemp to its own app Move recording for CoreTemp to its own app
0.08: Memory usage improvements for recorder app itself 0.08: Memory usage improvements for recorder app itself
0.09: Show correct number for log in overwrite prompt 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"); var domTracks = document.getElementById("tracks");
function saveKML(track,title) { 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"?> var kml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"> <kml xmlns="http://www.opengis.net/kml/2.2">
<Document> <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("")} ${track.map(pt=>` <gx:coord>${pt.Longitude} ${pt.Latitude} ${pt.Altitude}</gx:coord>\n`).join("")}
<ExtendedData> <ExtendedData>
<SchemaData schemaUrl="#schema"> <SchemaData schemaUrl="#schema">
${track[0].Heartrate!==undefined ? `<gx:SimpleArrayData name="heartrate"> ${track[0].Heartrate!==undefined ? `<gx:SimpleArrayData name="heartrate">
${track.map(pt=>` <gx:value>${0|pt.Heartrate}</gx:value>\n`).join("")} ${track.map(pt=>` <gx:value>${0|pt.Heartrate}</gx:value>\n`).join("")}
</gx:SimpleArrayData>`:``} </gx:SimpleArrayData>`:``}
@ -80,7 +82,7 @@ function saveGPX(track, title) {
<name>${title}</name> <name>${title}</name>
<trkseg>`; <trkseg>`;
track.forEach(pt=>{ track.forEach(pt=>{
gpx += ` if (pt.Latitude!="" && pt.Longitude!="") gpx += `
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}"> <trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
<ele>${pt.Altitude}</ele> <ele>${pt.Altitude}</ele>
<time>${pt.Time.toISOString()}</time> <time>${pt.Time.toISOString()}</time>
@ -122,6 +124,7 @@ function saveCSV(track, title) {
} }
function trackLineToObject(headers, l) { function trackLineToObject(headers, l) {
if (l===undefined) return {};
var t = l.trim().split(","); var t = l.trim().split(",");
var o = {}; var o = {};
headers.forEach((header,i) => o[header] = t[i]); headers.forEach((header,i) => o[header] = t[i]);
@ -155,7 +158,7 @@ function getTrackList() {
Util.showModal(`Loading Track ${trackNo}...`); Util.showModal(`Loading Track ${trackNo}...`);
Puck.eval(`(function(fn) { Puck.eval(`(function(fn) {
var f = require("Storage").open(fn,"r"); var f = require("Storage").open(fn,"r");
var headers = f.readLine(); var headers = f.readLine().trim();
var data = f.readLine(); var data = f.readLine();
var lIdx = headers.split(",").indexOf("Latitude"); var lIdx = headers.split(",").indexOf("Latitude");
if (lIdx >= 0) { if (lIdx >= 0) {
@ -184,14 +187,14 @@ function getTrackList() {
var html = `<div class="container"> var html = `<div class="container">
<div class="columns">\n`; <div class="columns">\n`;
trackList.forEach(track => { trackList.forEach(track => {
var trackData = trackLineToObject(track.info.headers, track.info.l);
console.log("track", track); console.log("track", track);
var trackData = trackLineToObject(track.info.headers, track.info.l);
console.log("trackData", trackData); console.log("trackData", trackData);
html += ` html += `
<div class="column col-12"> <div class="column col-12">
<div class="card-header"> <div class="card-header">
<div class="card-title h5">Track ${track.number}</div> <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> </div>
${trackData.Latitude ? ` ${trackData.Latitude ? `
<div class="card-image"> <div class="card-image">

View File

@ -2,7 +2,7 @@
"id": "recorder", "id": "recorder",
"name": "Recorder", "name": "Recorder",
"shortName": "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.", "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

@ -1,4 +1,4 @@
(function(back) { (function(back) {
// just go right to our app - we need all the memory // 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. 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.06: Misc memory and screen optimisations.
1.10: Adds Kalman filter. 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 II** has 6 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, VMG to waypoint, Position or Time.
**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.
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. 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** ( 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. **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. **BTN3** : Long press exit and return to watch.
**Touch Screen** If the 'Touch' setting is ON then : **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. 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. 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
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.) 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] Speed and Altitude [speedalt2]
Mike Bennett mike[at]kereru.com Mike Bennett mike[at]kereru.com
1.10 : add inverted colours 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 */ /*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
var KalmanFilter = (function () { 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 , toggle: function() { this.set( ! this.isOn); } // toggle the LED
}, LED1 = LED; // LED1 as 'synonym' for LED }, LED1 = LED; // LED1 as 'synonym' for LED
// Load fonts
//require("Font7x11Numeric7Seg").add(Graphics);
var lf = {fix:0,satellites:0}; var lf = {fix:0,satellites:0};
var showMax = 0; // 1 = display the max values. 0 = display the cur fix 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 pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
var canDraw = 1; 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 tmrLP; // Timer for delay in switching to low power after screen turns off
var maxSpd = 0; 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 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 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. var wp = {}; // Waypoint to use for distance from cur position.
@ -215,16 +217,27 @@ function radians(a) {
return a*Math.PI/180; 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){ function distance(a,b){
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2)); var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
var y = radians(b.lat-a.lat); var y = radians(b.lat-a.lat);
// Distance in selected units // Distance in metres
var d = Math.sqrt(x*x + y*y) * 6371000; 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; return d;
} }
@ -328,8 +341,8 @@ function drawClock() {
function drawWP(wp) { function drawWP(wp) {
buf.setColor(3); buf.setColor(3);
buf.setFontAlign(0,1); //left, bottom buf.setFontAlign(0,1); //left, bottom
buf.setFontVector(48); buf.setFontVector(40);
buf.drawString(wp,120,140); buf.drawString(wp,120,132);
} }
function drawSats(sats) { function drawSats(sats) {
@ -362,14 +375,18 @@ if ( emulator ) {
var m; var m;
var sp = '---'; var sp = '---';
var al = '---'; var al = sp;
var di = '---'; var di = sp;
var age = '---'; var brg = ''; // bearing
var crs = ''; // course
var age = sp;
var lat = '---.--'; var lat = '---.--';
var ns = ''; var ns = '';
var ew = ''; var ew = '';
var lon = '---.--'; var lon = '---.--';
var sats = '---'; var sats = sp;
var vmg = sp;
// Waypoint name // Waypoint name
var wpName = wp.name; var wpName = wp.name;
@ -387,21 +404,33 @@ if ( emulator ) {
lf.smoothed = 1; lf.smoothed = 1;
if ( maxN <= 15 ) maxN++; 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 // Speed
if ( cfg.spd == 0 ) { sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
sp = parseFloat(m[1]); // vmg
cfg.spd_unit = m[2]; if ( a >= 90 ) vmg = sp * Math.cos(radians(180-a)) * -1; // moving away from WP
} else vmg = sp * Math.cos(radians(a)); // towards wp
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
if ( sp < 10 ) sp = sp.toFixed(1); if ( sp < 10 ) sp = sp.toFixed(1);
else sp = Math.round(sp); else sp = Math.round(sp);
if (isNaN(sp)) sp = '---'; if (isNaN(sp)) sp = '---';
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = 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 // Altitude
al = lf.alt; al = lf.alt;
al = Math.round(parseFloat(al)/parseFloat(cfg.alt)); al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
@ -410,8 +439,11 @@ if ( emulator ) {
// Distance to waypoint // Distance to waypoint
di = distance(lf,wp); 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 of last fix (secs)
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000)); 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 ) { if ( cfg.modeA == 0 ) {
// Speed // Speed
if ( showMax ) if ( showMax ) {
drawScrn({ drawScrn({
val:maxSpd, val:maxSpd,
unit:cfg.spd_unit, unit:cfg.spd_unit,
@ -444,7 +497,8 @@ if ( emulator ) {
max:'MAX', max:'MAX',
wp:'' wp:''
}); // Speed maximums }); // Speed maximums
else }
else {
drawScrn({ drawScrn({
val:sp, val:sp,
unit:cfg.spd_unit, unit:cfg.spd_unit,
@ -453,11 +507,12 @@ if ( emulator ) {
max:'SPD', max:'SPD',
wp:'' wp:''
}); });
}
} }
if ( cfg.modeA == 1 ) { if ( cfg.modeA == 1 ) {
// Alt // Alt
if ( showMax ) if ( showMax ) {
drawScrn({ drawScrn({
val:maxAlt, val:maxAlt,
unit:cfg.alt_unit, unit:cfg.alt_unit,
@ -466,7 +521,8 @@ if ( emulator ) {
max:'MAX', max:'MAX',
wp:'' wp:''
}); // Alt maximums }); // Alt maximums
else }
else {
drawScrn({ drawScrn({
val:al, val:al,
unit:cfg.alt_unit, unit:cfg.alt_unit,
@ -475,6 +531,7 @@ if ( emulator ) {
max:'ALT', max:'ALT',
wp:'' wp:''
}); });
}
} }
if ( cfg.modeA == 2 ) { if ( cfg.modeA == 2 ) {
@ -490,6 +547,18 @@ if ( emulator ) {
} }
if ( cfg.modeA == 3 ) { 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 // Position
drawPosn({ drawPosn({
sats:sats, sats:sats,
@ -501,7 +570,7 @@ if ( emulator ) {
}); });
} }
if ( cfg.modeA == 4 ) { if ( cfg.modeA == 5 ) {
// Large clock // Large clock
drawClock(); drawClock();
} }
@ -510,14 +579,14 @@ if ( emulator ) {
function prevScrn() { function prevScrn() {
cfg.modeA = cfg.modeA-1; cfg.modeA = cfg.modeA-1;
if ( cfg.modeA < 0 ) cfg.modeA = 4; if ( cfg.modeA < 0 ) cfg.modeA = 5;
savSettings(); savSettings();
onGPS(lf); onGPS(lf);
} }
function nextScrn() { function nextScrn() {
cfg.modeA = cfg.modeA+1; cfg.modeA = cfg.modeA+1;
if ( cfg.modeA > 4 ) cfg.modeA = 0; if ( cfg.modeA > 5 ) cfg.modeA = 0;
savSettings(); savSettings();
onGPS(lf); onGPS(lf);
} }
@ -529,14 +598,14 @@ function nextFunc(dur) {
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values. 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); onGPS(lf);
} }
function updateClock() { function updateClock() {
if (!canDraw) return; if (!canDraw) return;
if ( cfg.modeA != 4 ) return; if ( cfg.modeA != 5 ) return;
drawClock(); drawClock();
if ( emulator ) {maxSpd++;maxAlt++;} if ( emulator ) {maxSpd++;maxAlt++;}
} }
@ -551,6 +620,7 @@ function startDraw(){
function stopDraw() { function stopDraw() {
canDraw=false; 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. 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}); 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 // == Events
function setButtons(){ function setButtons(){
@ -593,17 +677,6 @@ function setButtons(){
setWatch(function(e){ setWatch(function(e){
nextScrn(); nextScrn();
}, BTN3, {repeat:true,edge:"falling"}); }, 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) { Bangle.on('lcdPower',function(on) {
@ -621,40 +694,22 @@ Bangle.on('swipe',function(dir) {
Bangle.on('touch', function(button){ Bangle.on('touch', function(button){
if ( ! cfg.touch ) return; if ( ! cfg.touch ) return;
nextFunc(0); // Same function as short BTN1 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 // == Main Prog
// Read settings. // Read settings.
let cfg = require('Storage').readJSON('speedalt2.json',1)||{}; 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 = cfg.spd||1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit cfg.spd_unit = cfg.spd_unit||'kph'; // Displayed speed unit
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions. cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions. cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units
cfg.colour = cfg.colour||0; // Colour scheme. cfg.colour = cfg.colour||0; // Colour scheme.
cfg.wp = cfg.wp||0; // Last selected waypoint for dist 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.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt; cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;

View File

@ -2,7 +2,7 @@
"id": "speedalt2", "id": "speedalt2",
"name": "GPS Adventure Sports II", "name": "GPS Adventure Sports II",
"shortName":"GPS Adv Sport 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.", "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", "icon": "app.png",
"type": "app", "type": "app",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342) 0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
0.03: Add Color Changing Settings 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()); g.fillRect(0,0,g.getWidth(),g.getHeight());
// Any button turns off // Any button turns off
setWatch(()=>load(), BTN1); setWatch(()=>load(), BTN1);
setWatch(()=>load(), BTN2); if (global.BTN2) setWatch(()=>load(), BTN2);
setWatch(()=>load(), BTN3); if (global.BTN3) setWatch(()=>load(), BTN3);

View File

@ -2,14 +2,14 @@
"id": "torch", "id": "torch",
"name": "Torch", "name": "Torch",
"shortName": "Torch", "shortName": "Torch",
"version": "0.03", "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. You can also set the color through the apps settings menu.", "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", "icon": "app.png",
"tags": "tool,torch", "tags": "tool,torch",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [
{"name":"torch.app.js","url":"app.js"}, {"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.img","url":"app-icon.js","evaluate":true},
{"name":"torch.settings.js","url":"settings.js"} {"name":"torch.settings.js","url":"settings.js"}
] ]

View File

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

View File

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

View File

@ -5,7 +5,7 @@ Bangle.on('lcdPower', function(on) {
WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() { WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() {
var s = 26; var s = 26;
var t = 13; // thickness var t = 13; // thickness
var x = this.x, y = this.y; var x = this.x, y = this.y + 5;
g.reset(); g.reset();
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.fillRect(x,y+2,x+s-4,y+2+t); // outer 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(""); log("");
//process.exit(1); //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 => { languages.forEach(language => {
if (language.code=="en_GB") { if (language.code == "en_GB") {
console.log("Ignoring "+language.code); console.log(`Ignoring ${language.code}`);
return; return;
} }
console.log("Scanning "+language.code); console.log(`Scanning ${language.code}`);
log(language.code); log(language.code);
log("=========="); log("==========");
var translations = JSON.parse(fs.readFileSync(BASEDIR+"/lang/"+language.url).toString()); let translations = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/${language.url}`).toString());
translatedStrings.forEach(str => { translatedStrings.forEach(translationItem => {
if (!translations.GLOBAL[str.str]) if (!translations.GLOBAL[translationItem.str]) {
console.log(`Missing translation for ${JSON.stringify(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(""); 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": { "GLOBAL": {
"//":"Translations that apply for all apps", "//": "Translations that apply for all apps",
"Alarms" : "Sveglie", "On": "On",
"Hours" : "Ore", "on": "on",
"Minutes" : "Minuti", "Off": "Off",
"Enabled" : "Attiva", "off": "off",
"New Alarm" : "Nuova sveglia", "Ok": "Ok",
"Save" : "Salva", "Yes": "Sì",
"Back" : "Indietro", "No": "No",
"Repeat" : "Ripeti", "Alarm": "Sveglia",
"Delete" : "Cancella", "ALARM": "SVEGLIA",
"ALARM!" : "SVEGLIA!", "Alarms": "Sveglie",
"Sleep" : "Dormi" "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": { "//2": "App-specific overrides",
"//":"App-specific overrides", "launch": {
"rpt" : "ripeti" "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 // Handler for touch events
function touchHandler(l,e) { 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 (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)); if (l.c) l.c.forEach(n => touchHandler(n,e));