forked from FOSS/BangleApps
Merge branch 'espruino:master' into master
commit
a737e7f772
|
@ -7,7 +7,7 @@
|
|||
active = active.sort((a,b)=>(a.hr-b.hr)+(a.last-b.last)*24);
|
||||
var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
|
||||
if (!require('Storage').read("alarm.js")) {
|
||||
console.log(/*LANG*/"No alarm app!");
|
||||
console.log("No alarm app!");
|
||||
require('Storage').write('alarm.json',"[]");
|
||||
} else {
|
||||
var t = 3600000*(active[0].hr-hr);
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Fix JSON save format
|
||||
0.03: Add "Calculating" placeholder, update JSON save format
|
||||
0.04: Fix tapping at very bottom of list, exit on inactivity
|
||||
0.05: Add support for bulk importing and exporting tokens
|
||||
|
|
|
@ -3,6 +3,15 @@
|
|||
* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here
|
||||
* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/
|
||||
|
||||
## Important!
|
||||
|
||||
Tokens are stored *ONLY* on the watch. Make sure you do one or more of the following:
|
||||
|
||||
* Make a backup copy of the "authentiwatch.json" file.
|
||||
* Export all your tokens to another device or print the QR code.
|
||||
|
||||
Keep those copies safe and secure.
|
||||
|
||||
## Supports
|
||||
|
||||
* Google Authenticator compatible 2-factor authentication
|
||||
|
@ -14,8 +23,8 @@
|
|||
* Between 6 and 10 digits
|
||||
* Phone/PC configuration web page:
|
||||
* Add/edit/delete/arrange tokens
|
||||
* Scan QR codes
|
||||
* Produce scannable QR codes
|
||||
* Scan token and migration(import) QR codes
|
||||
* Produce scannable token and migration(export) QR codes
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -24,6 +33,8 @@
|
|||
* Swipe right to exit to the app launcher.
|
||||
* Swipe left on selected counter token to advance the counter to the next value.
|
||||
|
||||

|
||||
|
||||
## Creator
|
||||
|
||||
Andrew Gregory (andrew.gregory at gmail)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const tokenentryheight = 46;
|
||||
const tokenextraheight = 16;
|
||||
var tokendigitsheight = 30;
|
||||
// Hash functions
|
||||
const crypto = require("crypto");
|
||||
const algos = {
|
||||
|
@ -44,9 +45,6 @@ function b32decode(seedstr) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (bitcount > 0) {
|
||||
retstr += String.fromCharCode(buf << (8 - bitcount));
|
||||
}
|
||||
var retbuf = new Uint8Array(retstr.length);
|
||||
for (i in retstr) {
|
||||
retbuf[i] = retstr.charCodeAt(i);
|
||||
|
@ -117,27 +115,31 @@ function drawToken(id, r) {
|
|||
var y1 = r.y;
|
||||
var x2 = r.x + r.w - 1;
|
||||
var y2 = r.y + r.h - 1;
|
||||
var adj, sz;
|
||||
var adj, lbl, sz;
|
||||
g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ),
|
||||
Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2));
|
||||
lbl = tokens[id].label.substr(0, 10);
|
||||
if (id == state.curtoken) {
|
||||
// current token
|
||||
g.setColor(g.theme.fgH);
|
||||
g.setBgColor(g.theme.bgH);
|
||||
g.setFont("Vector", 16);
|
||||
g.setFont("Vector", tokenextraheight);
|
||||
// center just below top line
|
||||
g.setFontAlign(0, -1, 0);
|
||||
adj = y1;
|
||||
} else {
|
||||
g.setColor(g.theme.fg);
|
||||
g.setBgColor(g.theme.bg);
|
||||
g.setFont("Vector", 30);
|
||||
sz = tokendigitsheight;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(lbl) > r.w);
|
||||
// center in box
|
||||
g.setFontAlign(0, 0, 0);
|
||||
adj = (y1 + y2) / 2;
|
||||
}
|
||||
g.clearRect(x1, y1, x2, y2);
|
||||
g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false);
|
||||
g.drawString(lbl, (x1 + x2) / 2, adj, false);
|
||||
if (id == state.curtoken) {
|
||||
if (tokens[id].period > 0) {
|
||||
// timed - draw progress bar
|
||||
|
@ -148,14 +150,14 @@ function drawToken(id, r) {
|
|||
// counter - draw triangle as swipe hint
|
||||
let yc = (y1 + y2) / 2;
|
||||
g.fillPoly([0, yc, 10, yc - 10, 10, yc + 10, 0, yc]);
|
||||
adj = 10;
|
||||
adj = 12;
|
||||
}
|
||||
// digits just below label
|
||||
sz = 30;
|
||||
sz = tokendigitsheight;
|
||||
do {
|
||||
g.setFont("Vector", sz--);
|
||||
} while (g.stringWidth(state.otp) > (r.w - adj));
|
||||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + 16, false);
|
||||
g.drawString(state.otp, (x1 + adj + x2) / 2, y1 + tokenextraheight, false);
|
||||
}
|
||||
// shaded lines top and bottom
|
||||
g.setColor(0.5, 0.5, 0.5);
|
||||
|
@ -196,15 +198,15 @@ function draw() {
|
|||
}
|
||||
if (tokens.length > 0) {
|
||||
var drewcur = false;
|
||||
var id = Math.floor(state.listy / tokenentryheight);
|
||||
var y = id * tokenentryheight + Bangle.appRect.y - state.listy;
|
||||
var id = Math.floor(state.listy / (tokendigitsheight + tokenextraheight));
|
||||
var y = id * (tokendigitsheight + tokenextraheight) + Bangle.appRect.y - state.listy;
|
||||
while (id < tokens.length && y < Bangle.appRect.y2) {
|
||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:tokenentryheight});
|
||||
drawToken(id, {x:Bangle.appRect.x, y:y, w:Bangle.appRect.w, h:(tokendigitsheight + tokenextraheight)});
|
||||
if (id == state.curtoken && (tokens[id].period <= 0 || state.nextTime != 0)) {
|
||||
drewcur = true;
|
||||
}
|
||||
id += 1;
|
||||
y += tokenentryheight;
|
||||
y += (tokendigitsheight + tokenextraheight);
|
||||
}
|
||||
if (drewcur) {
|
||||
// the current token has been drawn - schedule a redraw
|
||||
|
@ -226,7 +228,7 @@ function draw() {
|
|||
state.nexttime = 0;
|
||||
}
|
||||
} else {
|
||||
g.setFont("Vector", 30);
|
||||
g.setFont("Vector", tokendigitsheight);
|
||||
g.setFontAlign(0, 0, 0);
|
||||
g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false);
|
||||
}
|
||||
|
@ -238,18 +240,18 @@ function draw() {
|
|||
|
||||
function onTouch(zone, e) {
|
||||
if (e) {
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / tokenentryheight);
|
||||
var id = Math.floor((state.listy + (e.y - Bangle.appRect.y)) / (tokendigitsheight + tokenextraheight));
|
||||
if (id == state.curtoken || tokens.length == 0 || id >= tokens.length) {
|
||||
id = -1;
|
||||
}
|
||||
if (state.curtoken != id) {
|
||||
if (id != -1) {
|
||||
var y = id * tokenentryheight - state.listy;
|
||||
var y = id * (tokendigitsheight + tokenextraheight) - state.listy;
|
||||
if (y < 0) {
|
||||
state.listy += y;
|
||||
y = 0;
|
||||
}
|
||||
y += tokenentryheight;
|
||||
y += (tokendigitsheight + tokenextraheight);
|
||||
if (y > Bangle.appRect.h) {
|
||||
state.listy += (y - Bangle.appRect.h);
|
||||
}
|
||||
|
@ -266,12 +268,15 @@ function onTouch(zone, e) {
|
|||
function onDrag(e) {
|
||||
if (e.x > g.getWidth() || e.y > g.getHeight()) return;
|
||||
if (e.dx == 0 && e.dy == 0) return;
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * tokenentryheight - Bangle.appRect.h);
|
||||
var newy = Math.min(state.listy - e.dy, tokens.length * (tokendigitsheight + tokenextraheight) - Bangle.appRect.h);
|
||||
state.listy = Math.max(0, newy);
|
||||
draw();
|
||||
}
|
||||
|
||||
function onSwipe(e) {
|
||||
if (e == 1) {
|
||||
exitApp();
|
||||
}
|
||||
if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) {
|
||||
tokens[state.curtoken].period--;
|
||||
let newsettings={tokens:tokens,misc:settings.misc};
|
||||
|
@ -296,7 +301,7 @@ function bangle1Btn(e) {
|
|||
state.curtoken = Math.max(state.curtoken, 0);
|
||||
state.curtoken = Math.min(state.curtoken, tokens.length - 1);
|
||||
var fakee = {};
|
||||
fakee.y = state.curtoken * tokenentryheight - state.listy + Bangle.appRect.y;
|
||||
fakee.y = state.curtoken * (tokendigitsheight + tokenextraheight) - state.listy + Bangle.appRect.y;
|
||||
state.curtoken = -1;
|
||||
state.nextTime = 0;
|
||||
onTouch(0, fakee);
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
<style type="text/css">
|
||||
body{font-family:sans-serif}
|
||||
body div{display:none}
|
||||
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#tokenqr{display:block}
|
||||
body.select tr>:first-child,body.export tr>:nth-child(3),body.export tr>:nth-child(4){display:none}
|
||||
body.select div.select,body.export div.export{display:block}
|
||||
body.select div.export,body.export div.select{display:none}
|
||||
body.select div#tokens,body.editing div#edit,body.scanning div#scan,body.showqr div#showqr,body.export div#tokens{display:block}
|
||||
#tokens th,#tokens td{padding:5px}
|
||||
#tokens tr:nth-child(odd){background-color:#ccc}
|
||||
#tokens tr:nth-child(even){background-color:#eee}
|
||||
|
@ -33,6 +36,12 @@ form.totp tr.hotp,form.hotp tr.totp{display:none}
|
|||
/* Start of all TOTP URLs */
|
||||
const otpAuthUrl = 'otpauth://';
|
||||
|
||||
/* Start of all OTP migration URLs */
|
||||
const otpMigrUrl = 'otpauth-migration://offline?data=';
|
||||
|
||||
/* Hash algorithms */
|
||||
const otpAlgos = ['SHA1','SHA256','SHA512'];
|
||||
|
||||
const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)'];
|
||||
|
||||
/* Settings */
|
||||
|
@ -45,6 +54,8 @@ var tokens = settings.tokens;
|
|||
*/
|
||||
function base32clean(val, nows) {
|
||||
var ret = val.replaceAll(/\s+/g, ' ');
|
||||
ret = ret.replaceAll(/0/g, 'O');
|
||||
ret = ret.replaceAll(/1/g, 'I');
|
||||
ret = ret.replaceAll(/[^A-Za-z2-7 ]/g, '');
|
||||
if (nows) {
|
||||
ret = ret.replaceAll(/\s+/g, '');
|
||||
|
@ -52,6 +63,48 @@ function base32clean(val, nows) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
function b32encode(str) {
|
||||
let buf = 0, bitcount = 0, ret = '';
|
||||
while (str.length > 0) {
|
||||
buf <<= 8;
|
||||
buf |= str.charCodeAt(0);
|
||||
bitcount += 8;
|
||||
str = str.substr(1);
|
||||
while (bitcount >= 5) {
|
||||
ret += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[(buf >> (bitcount - 5)) & 31];
|
||||
bitcount -= 5;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function b32decode(seedstr) {
|
||||
// RFC4648
|
||||
var i, buf = 0, bitcount = 0, ret = '';
|
||||
for (i in seedstr) {
|
||||
var c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(seedstr.charAt(i).toUpperCase(), 0);
|
||||
if (c != -1) {
|
||||
buf <<= 5;
|
||||
buf |= c;
|
||||
bitcount += 5;
|
||||
if (bitcount >= 8) {
|
||||
ret += String.fromCharCode(buf >> (bitcount - 8));
|
||||
buf &= (0xFF >> (16 - bitcount));
|
||||
bitcount -= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function makeLabel(token) {
|
||||
let lbl = token['label'];
|
||||
if (lbl == '') {
|
||||
lbl = (token['issuer'] == '') ? token['account'] : token['issuer'] + ' (' + token['account'] + ')';
|
||||
}
|
||||
token['label'] = lbl.substr(0, 10);
|
||||
}
|
||||
|
||||
/* Save changes to a token to the global tokens[] array.
|
||||
* id is the index into the global tokens[].
|
||||
* forget is a flag indicating if the token should be forgotten.
|
||||
|
@ -84,9 +137,16 @@ function saveEdit(id, forget) {
|
|||
}
|
||||
}
|
||||
|
||||
function showQr(url) {
|
||||
tokenqr.clear();
|
||||
tokenqr.makeCode(url);
|
||||
qrPreviousClass = document.body.className;
|
||||
document.body.className = 'showqr';
|
||||
}
|
||||
|
||||
/* Generate and display a QR-code representing the current token.
|
||||
*/
|
||||
function showQrCode() {
|
||||
function showTokenQr() {
|
||||
var fe = document.forms['edittoken'].elements;
|
||||
var url = new String(otpAuthUrl);
|
||||
switch (fe['type'].value) {
|
||||
|
@ -122,9 +182,7 @@ function showQrCode() {
|
|||
if (fe['algorithm'].value != 'SHA1') {
|
||||
url += '&algorithm=' + fe['algorithm'].value;
|
||||
}
|
||||
tokenqr.clear();
|
||||
tokenqr.makeCode(url);
|
||||
document.body.className = 'showqr';
|
||||
showQr(url);
|
||||
}
|
||||
|
||||
function onTypeChanged() {
|
||||
|
@ -138,6 +196,7 @@ function onTypeChanged() {
|
|||
* id is the index into the global tokens[].
|
||||
*/
|
||||
function editToken(id) {
|
||||
if (document.body.className == 'export') return;
|
||||
var p;
|
||||
const selectMarkup = function(name, ary, cur, onchg) {
|
||||
var ret = '<select name="' + name + '"' + ((typeof onchg == 'string') ? ' onchange="' + onchg + '"' : '') + '>';
|
||||
|
@ -163,7 +222,7 @@ function editToken(id) {
|
|||
markup += selectMarkup('digits', ['6','7','8','9','10'], tokens[id].digits);
|
||||
markup += '</td></tr>';
|
||||
markup += '<tr><td>Hash:</td><td>';
|
||||
markup += selectMarkup('algorithm', ['SHA1','SHA256','SHA512'], tokens[id].algorithm);
|
||||
markup += selectMarkup('algorithm', otpAlgos, tokens[id].algorithm);
|
||||
markup += '</td></tr>';
|
||||
markup += '</tbody><tr><td id="advbtn" colspan="2">';
|
||||
markup += '<button type="button" onclick="document.getElementById(\'edittoken\').classList.toggle(\'showadv\')">Advanced</button>';
|
||||
|
@ -171,9 +230,9 @@ function editToken(id) {
|
|||
markup += '<button type="button" onclick="updateTokens()">Cancel Edit</button>';
|
||||
markup += '<button type="button" onclick="saveEdit(' + id + ', false)">Save Changes</button>';
|
||||
if (tokens[id].isnew) {
|
||||
markup += '<button type="button" onclick="startScan()">Scan QR Code</button>';
|
||||
markup += '<button type="button" onclick="startScan(handleTokenQr,cancelTokenQr)">Scan QR</button>';
|
||||
} else {
|
||||
markup += '<button type="button" onclick="showQrCode()">Show QR Code</button>';
|
||||
markup += '<button type="button" onclick="showTokenQr()">Show QR</button>';
|
||||
markup += '<button type="button" onclick="saveEdit(' + id + ', true)">Forget Token</button>';
|
||||
}
|
||||
document.getElementById('edit').innerHTML = markup;
|
||||
|
@ -188,6 +247,46 @@ function addToken() {
|
|||
editToken(tokens.length - 1);
|
||||
}
|
||||
|
||||
/* Convert a number to a proto3 varint.
|
||||
*/
|
||||
function int2proto3varint(val) {
|
||||
var ret = '';
|
||||
do {
|
||||
let c = val & 0x7F;
|
||||
val >>>= 7;
|
||||
if (val > 0) {
|
||||
c |= 0x80;
|
||||
}
|
||||
ret += String.fromCharCode(c);
|
||||
} while (val > 0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Convert a string to a proto3 field.
|
||||
*/
|
||||
function str2proto3(field_number, str) {
|
||||
return int2proto3varint((field_number << 3) + 2) + int2proto3varint(str.length) + str;
|
||||
}
|
||||
|
||||
/* Convert a number to a proto3 field.
|
||||
*/
|
||||
function int2proto3(field_number, val) {
|
||||
return int2proto3varint(field_number << 3) + int2proto3varint(val);
|
||||
}
|
||||
|
||||
/* Convert the specified token to its proto3 representation.
|
||||
*/
|
||||
function token2proto3(id) {
|
||||
var secret = str2proto3(1, b32decode(tokens[id].secret));
|
||||
var name = str2proto3(2, (tokens[id].account == '') ? tokens[id].label : tokens[id].account);
|
||||
var issuer = (tokens[id].issuer == '') ? '' : str2proto3(3, tokens[id].issuer);
|
||||
var algorithm = int2proto3(4, (tokens[id].algorithm == 'SHA512') ? 3 : ((tokens[id].algorithm == 'SHA256') ? 2 : 1));
|
||||
var digits = int2proto3(5, (tokens[id].digits == 8) ? 2 : 1);
|
||||
var type = int2proto3(6, (tokens[id].period <= 0) ? 1 : 2);
|
||||
var counter = (tokens[id].period <= 0) ? int2proto3(7, -tokens[id].period) : '';
|
||||
return str2proto3(1, secret + name + issuer + algorithm + digits + type + counter);
|
||||
}
|
||||
|
||||
/* Move the specified token up or down in the global tokens[].
|
||||
* id is the index in the global tokens[] of the token to move.
|
||||
* dir is the direction to move: -1=up, 1=down.
|
||||
|
@ -200,10 +299,15 @@ function moveToken(id, dir) {
|
|||
/* Update the display listing all the tokens.
|
||||
*/
|
||||
function updateTokens() {
|
||||
const tokenSelect = function(id) {
|
||||
return '<input name="exp_' + id + '" type="checkbox" onclick="exportTokens(false, \'' + id + '\')">';
|
||||
};
|
||||
const tokenButton = function(fn, id, label, dir) {
|
||||
return '<button type="button" onclick="' + fn + '(' + id + (dir ? ',' + dir : '') + ')">' + label + '</button>';
|
||||
};
|
||||
var markup = '<table><tr><th>Token</th><th colspan="2">Order</th></tr>';
|
||||
var markup = '<table><tr><th>';
|
||||
markup += tokenSelect('all');
|
||||
markup += '</th><th>Token</th><th colspan="2">Order</th></tr>';
|
||||
/* any tokens marked new are cancelled new additions and must be removed */
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (tokens[i].isnew) {
|
||||
|
@ -212,6 +316,8 @@ function updateTokens() {
|
|||
}
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
markup += '<tr><td>';
|
||||
markup += tokenSelect(i);
|
||||
markup += '</td><td>';
|
||||
markup += tokenButton('editToken', i, tokens[i].label);
|
||||
markup += '</td><td>';
|
||||
if (i < (tokens.length - 1)) {
|
||||
|
@ -224,14 +330,20 @@ function updateTokens() {
|
|||
markup += '</td></tr>';
|
||||
}
|
||||
markup += '</table>';
|
||||
markup += '<div class="select">';
|
||||
markup += '<button type="button" onclick="addToken()">Add Token</button>';
|
||||
markup += '<button type="button" onclick="saveTokens()">Save to watch</button>';
|
||||
markup += '<button type="button" onclick="startScan(handleImportQr,cancelImportQr)">Import</button>';
|
||||
markup += '<button type="button" onclick="document.body.className=\'export\'">Export</button>';
|
||||
markup += '</div><div class="export">';
|
||||
markup += '<button type="button" onclick="document.body.className=\'select\'">Cancel</button>';
|
||||
markup += '<button type="button" onclick="exportTokens(true, null)">Show QR</button>';
|
||||
markup += '</div>';
|
||||
document.getElementById('tokens').innerHTML = markup;
|
||||
document.body.className = 'select';
|
||||
}
|
||||
|
||||
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
|
||||
qrcode.callback = res => {
|
||||
function handleTokenQr(res) {
|
||||
if (res) {
|
||||
if (res.startsWith(otpAuthUrl)) {
|
||||
res = decodeURIComponent(res);
|
||||
|
@ -243,7 +355,8 @@ qrcode.callback = res => {
|
|||
'counter':'0',
|
||||
'period':'30',
|
||||
'secret':'',
|
||||
'issuer':''
|
||||
'issuer':'',
|
||||
'label':''
|
||||
};
|
||||
var otpok = true;
|
||||
for (let pi in params) {
|
||||
|
@ -261,8 +374,7 @@ qrcode.callback = res => {
|
|||
if (otpok) {
|
||||
scanning = false;
|
||||
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
||||
t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')';
|
||||
t['label'] = t['label'].substr(0, 10);
|
||||
makeLabel(t);
|
||||
var fe = document.forms['edittoken'].elements;
|
||||
if (res.startsWith(otpAuthUrl + 'hotp/')) {
|
||||
t['period'] = '30';
|
||||
|
@ -283,8 +395,94 @@ qrcode.callback = res => {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function cancelTokenQr() {
|
||||
scanning = false;
|
||||
editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value));
|
||||
}
|
||||
class proto3decoder {
|
||||
constructor(str) {
|
||||
this.buf = [];
|
||||
for (let i in str) {
|
||||
this.buf = this.buf.concat(str.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
getVarint() {
|
||||
let c, ret = 0
|
||||
do {
|
||||
c = this.buf.shift();
|
||||
ret = (ret << 7) | (c & 0x7F);
|
||||
} while ((c & 0x80) != 0);
|
||||
return ret;
|
||||
}
|
||||
getString(length) {
|
||||
let ret = '';
|
||||
for (let i = 0; i < length; ++i) {
|
||||
ret += String.fromCharCode(this.buf.shift());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
parse() {
|
||||
let ret = null;
|
||||
if (this.buf.length > 0) {
|
||||
let field_data = null;
|
||||
let field_type = this.getVarint();
|
||||
let field_number = field_type >>> 3;
|
||||
let wire_type = field_type & 7;
|
||||
switch (wire_type) {
|
||||
case 0: field_data = this.getVarint(); break;
|
||||
case 2: field_data = this.getString(this.getVarint()); break;
|
||||
}
|
||||
ret = {number:field_number,data:field_data};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
function handleImportQr(res) {
|
||||
if (res) {
|
||||
if (res.startsWith(otpMigrUrl)) {
|
||||
scanning = false;
|
||||
let data = new proto3decoder(atob(decodeURIComponent(res.substr(otpMigrUrl.length))));
|
||||
while (data.buf.length > 0) {
|
||||
let field = data.parse();
|
||||
if (field?.number == 1) {
|
||||
let newtoken = {'algorithm':'SHA1','digits':6,'period':30,'issuer':'','account':'','secret':'','label':''};
|
||||
let p3token = new proto3decoder(field.data);
|
||||
while (p3token.buf.length > 0) {
|
||||
let buf = p3token.parse();
|
||||
switch (buf?.number) {
|
||||
case 1: newtoken.secret = b32encode(buf.data); break;
|
||||
case 2: newtoken.account = buf.data; break;
|
||||
case 3: newtoken.issuer = buf.data; break;
|
||||
case 4: newtoken.algorithm = otpAlgos[buf.data - 1]; break;
|
||||
case 5: newtoken.digits = (['6','8'])[buf.data - 1]; break;
|
||||
case 7: newtoken.period = -buf.data; break;
|
||||
}
|
||||
}
|
||||
makeLabel(newtoken);
|
||||
tokens[tokens.length] = newtoken;
|
||||
}
|
||||
}
|
||||
updateTokens();
|
||||
}
|
||||
}
|
||||
}
|
||||
function cancelImportQr() {
|
||||
scanning = false;
|
||||
document.body.className = 'select';
|
||||
}
|
||||
/* Original QR-code reader: https://www.sitepoint.com/create-qr-code-reader-mobile-website/ */
|
||||
qrcode.callback = res => {
|
||||
if (res) {
|
||||
scanCallback(res);
|
||||
if (scanning) {
|
||||
scanBack();
|
||||
}
|
||||
}
|
||||
};
|
||||
function startScan() {
|
||||
function startScan(handler,cancel) {
|
||||
scanCallback = handler;
|
||||
scanBack = cancel;
|
||||
document.body.className = 'scanning';
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({video:{facingMode:'environment'}})
|
||||
|
@ -339,36 +537,93 @@ function saveTokens() {
|
|||
Util.hideModal();
|
||||
});
|
||||
}
|
||||
/* Handle token export.
|
||||
* showqr is true if the QR code should be shown, if false the checkboxes need updating
|
||||
* id is the name of the clicked checkbox, or null if the export button was pressed
|
||||
*/
|
||||
function exportTokens(showqr, id) {
|
||||
let allchecked = true, allclear = true;
|
||||
let cball;
|
||||
let exp = '';
|
||||
for (let cb of document.querySelectorAll('input[type=checkbox]')) {
|
||||
let cbid = cb.name.substring(4);
|
||||
if (cbid == 'all') {
|
||||
cball = cb;
|
||||
} else {
|
||||
if (id == 'all') {
|
||||
cb.checked = cball.checked;
|
||||
} else {
|
||||
if (cb.checked) {
|
||||
if (showqr) {
|
||||
exp += token2proto3(parseInt(cbid));
|
||||
}
|
||||
allclear = false;
|
||||
} else {
|
||||
allchecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id != 'all') {
|
||||
if (allclear) {
|
||||
cball.indeterminate = false;
|
||||
cball.checked = false;
|
||||
} else if (allchecked) {
|
||||
cball.indeterminate = false;
|
||||
cball.checked = true;
|
||||
} else {
|
||||
cball.indeterminate = true;
|
||||
}
|
||||
}
|
||||
if (showqr) {
|
||||
if (exp != '') {
|
||||
/* add version, batch_size, batch_index, but no batch_id */
|
||||
exp += int2proto3(2, 1) + int2proto3(3, 1) + int2proto3(4, 0);
|
||||
let url = otpMigrUrl + encodeURIComponent(btoa(exp));
|
||||
showQr(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
function onInit() {
|
||||
loadTokens();
|
||||
updateTokens();
|
||||
}
|
||||
function qrBack() {
|
||||
document.body.className = qrPreviousClass;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="select">
|
||||
<h1>Authentiwatch</h1>
|
||||
|
||||
<div id="tokens">
|
||||
<p>No watch comms.</p>
|
||||
</div>
|
||||
|
||||
<div id="scan">
|
||||
<table>
|
||||
<tr><td><canvas id="qr-canvas"></canvas></td></tr>
|
||||
<tr><td><button type="button" onclick="editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value))">Cancel</button></td></tr>
|
||||
<tr><td><button type="button" onclick="scanBack()">Cancel</button></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="edit">
|
||||
</div>
|
||||
<div id="tokenqr">
|
||||
|
||||
<div id="showqr">
|
||||
<table><tr><td id="qrcode"></td></tr><tr><td>
|
||||
<button type="button" onclick="document.body.className='editing'">Back</button>
|
||||
<button type="button" onclick="qrBack()">Back</button>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const video=document.createElement('video');
|
||||
const canvasElement=document.getElementById('qr-canvas');
|
||||
const canvas=canvasElement.getContext('2d');
|
||||
let scanning=false;
|
||||
const tokenqr=new QRCode(document.getElementById('qrcode'), '');
|
||||
const tokenqr=new QRCode(document.getElementById('qrcode'), {width:354,height:354});
|
||||
</script>
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
</body>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"shortName": "AuthWatch",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "Google Authenticator compatible tool.",
|
||||
"tags": "tool",
|
||||
"interface": "interface.html",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: Display pressure as number and hand
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4AVmczmALI7oWJgYXBmYLHhvd6AuKGBHdAAYXLDAwXRJIvd73u9oXSLoPuAAJhHOwYYGIYIXDGAwWGMIYvMC5QwBC4ZeMC4x3KL44XEU6KQEC5gAMCqoXZAH4AchAXWxAXWwBGWC62IC6sILywXXxAXUhWqzAXTCwIABOyYXD0AXSCwQABC/4XaO68JC6wYCCygA/AH4AGA"))
|
|
@ -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 |
|
@ -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 |
|
@ -14,3 +14,6 @@
|
|||
Always emit BTHRM event
|
||||
Cleanup promises code and allow to configure custom additional waiting times to work around bugs
|
||||
Disconnect cleanly on exit
|
||||
0.06: Fix bug if no request waiting time is set
|
||||
Fix bug if no connection data was cached
|
||||
Fix error during disconnect
|
||||
|
|
|
@ -365,6 +365,7 @@
|
|||
|
||||
if (settings.gracePeriodRequest){
|
||||
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
|
||||
}
|
||||
|
||||
promise = promise.then((d)=>{
|
||||
log("Got device: ", d);
|
||||
|
@ -376,7 +377,6 @@
|
|||
log("Wait after request");
|
||||
return waitingPromise(settings.gracePeriodRequest);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
|
@ -426,14 +426,14 @@
|
|||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
var getCharacteristicsPromise = Promise.resolve();
|
||||
var characteristicsPromise = Promise.resolve();
|
||||
if (characteristics.length == 0){
|
||||
getCharacteristicsPromise = getCharacteristicsPromise.then(()=>{
|
||||
characteristicsPromise = characteristicsPromise.then(()=>{
|
||||
log("Getting services");
|
||||
return gatt.getPrimaryServices();
|
||||
});
|
||||
|
||||
getCharacteristicsPromise = getCharacteristicsPromise().then((services)=>{
|
||||
characteristicsPromise = characteristicsPromise.then((services)=>{
|
||||
log("Got services:", services);
|
||||
var result = Promise.resolve();
|
||||
for (var service of services){
|
||||
|
@ -453,11 +453,11 @@
|
|||
|
||||
} else {
|
||||
for (var characteristic of characteristics){
|
||||
getCharacteristicsPromise = attachCharacteristicPromise(getCharacteristicsPromise, characteristic, true);
|
||||
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
|
||||
}
|
||||
}
|
||||
|
||||
return getCharacteristicsPromise;
|
||||
return characteristicsPromise;
|
||||
});
|
||||
|
||||
promise = promise.then(()=>{
|
||||
|
@ -489,8 +489,8 @@
|
|||
if (gatt.connected){
|
||||
log("Disconnect with gatt: ", gatt);
|
||||
gatt.disconnect().then(()=>{
|
||||
log("Successful disconnect", e);
|
||||
}).catch(()=>{
|
||||
log("Successful disconnect");
|
||||
}).catch((e)=>{
|
||||
log("Error during disconnect", e);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.05",
|
||||
"version": "0.06",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
0.05: Tweaks for 'HRM-raw' handling
|
||||
0.06: Add widgets
|
||||
0.07: Update scaling for new firmware
|
||||
0.08: Don't force backlight on/watch unlocked on Bangle 2
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
if (process.env.HWVERSION == 1) {
|
||||
Bangle.setLCDPower(1);
|
||||
Bangle.setLCDTimeout(0);
|
||||
}
|
||||
|
||||
Bangle.setHRMPower(1);
|
||||
var hrmInfo, hrmOffset = 0;
|
||||
var hrmInterval;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "hrm",
|
||||
"name": "Heart Rate Monitor",
|
||||
"version": "0.07",
|
||||
"version": "0.08",
|
||||
"description": "Measure your heart rate and see live sensor data",
|
||||
"icon": "heartrate.png",
|
||||
"tags": "health",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
require("Storage").write("launch.json",settings);
|
||||
}
|
||||
const appMenu = {
|
||||
/*LANG*/"": {"title": /*LANG*/"Launcher Settings"},
|
||||
"": {"title": /*LANG*/"Launcher Settings"},
|
||||
/*LANG*/"< Back": back,
|
||||
/*LANG*/"Font": {
|
||||
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"),
|
||||
|
|
|
@ -12,3 +12,4 @@
|
|||
0.12: Added humidity as an option to display.
|
||||
0.13: Improved battery visualization.
|
||||
0.14: Added altitude as an option to display.
|
||||
0.15: Using wpedom to count steps.
|
|
@ -448,16 +448,17 @@ function draw(){
|
|||
* Step counter via widget
|
||||
*/
|
||||
function getSteps() {
|
||||
var steps = 0;
|
||||
let health;
|
||||
try{
|
||||
health = require("health");
|
||||
if (WIDGETS.wpedom !== undefined) {
|
||||
return WIDGETS.wpedom.getSteps();
|
||||
} else if (WIDGETS.activepedom !== undefined) {
|
||||
return WIDGETS.activepedom.getSteps();
|
||||
}
|
||||
} catch(ex) {
|
||||
return steps;
|
||||
// In case we failed, we can only show 0 steps.
|
||||
}
|
||||
|
||||
health.readDay(new Date(), h=>steps+=h.steps);
|
||||
return steps;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "LCARS Clock",
|
||||
"shortName":"LCARS",
|
||||
"icon": "lcars.png",
|
||||
"version":"0.14",
|
||||
"version":"0.15",
|
||||
"readme": "README.md",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"description": "Library Computer Access Retrieval System (LCARS) clock.",
|
||||
|
|
|
@ -258,7 +258,7 @@ var locales = {
|
|||
temperature: "°C",
|
||||
ampm: { 0: "", 1: "" },
|
||||
timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
|
||||
datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%Y" }, // dimanche 1 mars 2020 // 01/03/2020
|
||||
datePattern: { 0: "%d %B %Y", "1": "%d/%m/%Y" }, // 1 mars 2020 // 01/03/2020
|
||||
abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
|
||||
month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
|
||||
abday: "dim,lun,mar,mer,jeu,ven,sam",
|
||||
|
|
|
@ -28,3 +28,5 @@
|
|||
Spread message action buttons out
|
||||
Back button now goes back to list of messages
|
||||
If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267)
|
||||
0.19: Use a larger font for message text if it'll fit
|
||||
0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031)
|
||||
|
|
|
@ -26,19 +26,15 @@ When a new message is received:
|
|||
|
||||
When a message is shown, you'll see a screen showing the message title and text.
|
||||
|
||||
### Android
|
||||
|
||||
* The 'back-arrow' button goes back to Messages, marking the current message as read.
|
||||
* If shown, the 'tick' button opens the notification on the phone
|
||||
* If shown, the 'cross' button dismisses the notification on the phone
|
||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
||||
|
||||
### iOS
|
||||
|
||||
* The 'back-arrow' button goes back to Messages, marking the current message as read.
|
||||
* If shown, the 'tick' button responds positively to the notification (accept call/etc)
|
||||
* If shown, the 'cross' button responds negatively to the notification (dismiss call/etc)
|
||||
* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read.
|
||||
* The top-left icon shows more options, for instance deleting the message of marking unread
|
||||
* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`)
|
||||
* If shown, the 'tick' button:
|
||||
* **Android** opens the notification on the phone
|
||||
* **iOS** responds positively to the notification (accept call/etc)
|
||||
* If shown, the 'cross' button:
|
||||
* **Android** dismisses the notification on the phone
|
||||
* **iOS** responds negatively to the notification (dismiss call/etc)
|
||||
|
||||
## Images
|
||||
_1. Screenshot of a notification_
|
||||
|
|
|
@ -198,9 +198,39 @@ function showMusicMessage(msg) {
|
|||
layout.render();
|
||||
}
|
||||
|
||||
function showMessageScroller(msg) {
|
||||
var bodyFont = fontBig;
|
||||
g.setFont(bodyFont);
|
||||
var lines = [];
|
||||
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10)
|
||||
var titleCnt = lines.length;
|
||||
if (titleCnt) lines.push(""); // add blank line after title
|
||||
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]);
|
||||
E.showScroller({
|
||||
h : g.getFontHeight(), // height of each menu item in pixels
|
||||
c : lines.length, // number of menu items
|
||||
// a function to draw a menu item
|
||||
draw : function(idx, r) {
|
||||
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
|
||||
g.setBgColor(idx<titleCnt ? colBg : g.theme.bg).clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
|
||||
}, select : function(idx) {
|
||||
if (idx>=lines.length-2)
|
||||
showMessage(msg.id);
|
||||
}
|
||||
});
|
||||
// ensure button-press on Bangle.js 2 takes us back
|
||||
if (process.env.HWVERSION>1) Bangle.btnWatches = [
|
||||
setWatch(() => showMessage(msg.id), BTN1, {repeat:1,edge:"falling"})
|
||||
];
|
||||
}
|
||||
|
||||
function showMessageSettings(msg) {
|
||||
E.showMenu({"":{"title":/*LANG*/"Message"},
|
||||
"< Back" : () => showMessage(msg.id),
|
||||
/*LANG*/"View Message" : () => {
|
||||
showMessageScroller(msg);
|
||||
},
|
||||
/*LANG*/"Delete" : () => {
|
||||
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
|
||||
saveMessages();
|
||||
|
@ -245,12 +275,13 @@ function showMessage(msgid) {
|
|||
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n");
|
||||
}
|
||||
}
|
||||
var buttons = [
|
||||
{type:"btn", src:getBackImage(), cb:()=>{
|
||||
function goBack() {
|
||||
msg.new = false; saveMessages(); // read mail
|
||||
cancelReloadTimeout(); // don't auto-reload to clock now
|
||||
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0});
|
||||
}} // back
|
||||
}
|
||||
var buttons = [
|
||||
{type:"btn", src:getBackImage(), cb:goBack} // back
|
||||
];
|
||||
if (msg.positive) {
|
||||
buttons.push({fillx:1});
|
||||
|
@ -270,9 +301,18 @@ function showMessage(msgid) {
|
|||
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1});
|
||||
}});
|
||||
}
|
||||
var bodyFont = fontMedium;
|
||||
// If body of message is only two lines long w/ large font, use large font.
|
||||
var body=msg.body, bodyFont = fontLarge, lines;
|
||||
if (body) {
|
||||
var w = g.getWidth()-48;
|
||||
if (g.setFont(bodyFont).stringWidth(body) > w * 2)
|
||||
bodyFont = fontMedium;
|
||||
if (g.setFont(bodyFont).stringWidth(body) > w) {
|
||||
lines = g.setFont(bodyFont).wrapString(msg.body, g.getWidth()-10);
|
||||
var body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
|
||||
body = (lines.length>4) ? lines.slice(0,4).join("\n")+"..." : lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
layout = new Layout({ type:"v", c: [
|
||||
{type:"h", fillx:1, bgCol:colBg, c: [
|
||||
{ type:"btn", src:getMessageImage(msg), col:getMessageImageCol(msg), pad: 3, cb:()=>{
|
||||
|
@ -284,11 +324,18 @@ function showMessage(msgid) {
|
|||
title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{},
|
||||
]},
|
||||
]},
|
||||
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2 },
|
||||
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{
|
||||
// allow tapping to show a larger version
|
||||
showMessageScroller(msg);
|
||||
} },
|
||||
{type:"h",fillx:1, c: buttons}
|
||||
]});
|
||||
g.clearRect(Bangle.appRect);
|
||||
layout.render();
|
||||
// ensure button-press on Bangle.js 2 takes us back
|
||||
if (process.env.HWVERSION>1) Bangle.btnWatches = [
|
||||
setWatch(goBack, BTN1, {repeat:1,edge:"falling"})
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "messages",
|
||||
"name": "Messages",
|
||||
"version": "0.18",
|
||||
"version": "0.20",
|
||||
"description": "App to display notifications from iOS and Gadgetbridge",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -11,3 +11,5 @@
|
|||
0.11: Changed cycle on minute to prevInfo to avoid the 2nd one being the blank line
|
||||
0.12: Removed dependancy on widpedom, now uses Bangle.getHealthStatus("day").steps
|
||||
which requires 2.11.27 firmware to reset at midnight
|
||||
0.13: call process.memory(false) to avoid triggering a GC of memory
|
||||
supported in pre 2.12.13 firmware
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "pastel",
|
||||
"name": "Pastel Clock",
|
||||
"shortName": "Pastel",
|
||||
"version": "0.12",
|
||||
"version": "0.13",
|
||||
"description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times. Requires firmware 2.11.27",
|
||||
"icon": "pastel.png",
|
||||
"dependencies": {"mylocation":"app","weather":"app"},
|
||||
|
|
|
@ -83,7 +83,7 @@ const infoData = {
|
|||
ID_SS: { calc: () => 'Sunset: ' + sunSet },
|
||||
ID_STEP: { calc: () => 'Steps: ' + getSteps() },
|
||||
ID_BATT: { calc: () => 'Battery: ' + E.getBattery() + '%' },
|
||||
ID_MEM: { calc: () => {var val = process.memory(); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
|
||||
ID_MEM: { calc: () => {var val = process.memory(false); return 'Ram: ' + Math.round(val.usage*100/val.total) + '%';} },
|
||||
ID_ID: { calc: () => {var val = NRF.getAddress().split(':'); return 'Id: ' + val[4] + val[5];} },
|
||||
ID_FW: { calc: () => 'Fw: ' + process.env.VERSION }
|
||||
};
|
||||
|
|
|
@ -13,3 +13,5 @@
|
|||
Move recording for CoreTemp to its own app
|
||||
0.08: Memory usage improvements for recorder app itself
|
||||
0.09: Show correct number for log in overwrite prompt
|
||||
0.10: Fix broken recorder settings (when launched from settings app)
|
||||
0.11: Fix KML and GPX export when there is no GPS data
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
var domTracks = document.getElementById("tracks");
|
||||
|
||||
function saveKML(track,title) {
|
||||
// only include data points with GPS values
|
||||
track=track.filter(pt=>pt.Latitude!="" && pt.Longitude!="");
|
||||
// Now output KML
|
||||
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document>
|
||||
|
@ -37,7 +40,6 @@ ${track.map(pt=>` <when>${pt.Time.toISOString()}</when>\n`).join("")}
|
|||
${track.map(pt=>` <gx:coord>${pt.Longitude} ${pt.Latitude} ${pt.Altitude}</gx:coord>\n`).join("")}
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
|
||||
${track[0].Heartrate!==undefined ? `<gx:SimpleArrayData name="heartrate">
|
||||
${track.map(pt=>` <gx:value>${0|pt.Heartrate}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>`:``}
|
||||
|
@ -80,7 +82,7 @@ function saveGPX(track, title) {
|
|||
<name>${title}</name>
|
||||
<trkseg>`;
|
||||
track.forEach(pt=>{
|
||||
gpx += `
|
||||
if (pt.Latitude!="" && pt.Longitude!="") gpx += `
|
||||
<trkpt lat="${pt.Latitude}" lon="${pt.Longitude}">
|
||||
<ele>${pt.Altitude}</ele>
|
||||
<time>${pt.Time.toISOString()}</time>
|
||||
|
@ -122,6 +124,7 @@ function saveCSV(track, title) {
|
|||
}
|
||||
|
||||
function trackLineToObject(headers, l) {
|
||||
if (l===undefined) return {};
|
||||
var t = l.trim().split(",");
|
||||
var o = {};
|
||||
headers.forEach((header,i) => o[header] = t[i]);
|
||||
|
@ -155,7 +158,7 @@ function getTrackList() {
|
|||
Util.showModal(`Loading Track ${trackNo}...`);
|
||||
Puck.eval(`(function(fn) {
|
||||
var f = require("Storage").open(fn,"r");
|
||||
var headers = f.readLine();
|
||||
var headers = f.readLine().trim();
|
||||
var data = f.readLine();
|
||||
var lIdx = headers.split(",").indexOf("Latitude");
|
||||
if (lIdx >= 0) {
|
||||
|
@ -184,14 +187,14 @@ function getTrackList() {
|
|||
var html = `<div class="container">
|
||||
<div class="columns">\n`;
|
||||
trackList.forEach(track => {
|
||||
var trackData = trackLineToObject(track.info.headers, track.info.l);
|
||||
console.log("track", track);
|
||||
var trackData = trackLineToObject(track.info.headers, track.info.l);
|
||||
console.log("trackData", trackData);
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">Track ${track.number}</div>
|
||||
<div class="card-subtitle text-gray">${trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</div>
|
||||
<div class="card-subtitle text-gray">${trackData.Time?trackData.Time.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }):"No track data"}</div>
|
||||
</div>
|
||||
${trackData.Latitude ? `
|
||||
<div class="card-image">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "recorder",
|
||||
"name": "Recorder",
|
||||
"shortName": "Recorder",
|
||||
"version": "0.09",
|
||||
"version": "0.11",
|
||||
"description": "Record GPS position, heart rate and more in the background, then download to your PC.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
(function(back) {
|
||||
// just go right to our app - we need all the memory
|
||||
load("record.app.js");
|
||||
})();
|
||||
load("recorder.app.js");
|
||||
})
|
||||
|
|
|
@ -10,3 +10,5 @@
|
|||
0.10: Add Kalman filter to smooth the speed and altitude values. Can be disabled in settings.
|
||||
1.06: Misc memory and screen optimisations.
|
||||
1.10: Adds Kalman filter.
|
||||
1.14: Add VMG and coordinates screens
|
||||
1.43: Adds mirroring of the watch face to an Android device. See README.md
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
# GPS Speed, Altimeter and Distance to Waypoint
|
||||
# GPS Speed, Altitude, Distance and VMG
|
||||
|
||||
What is the difference between **GPS Adventure Sports** and **GPS Adventure Sports II** ?
|
||||
|
||||
**GPS Adventure Sports** has 3 screens, each of which display different sets of information.
|
||||
|
||||
**GPS Adventure Sports II** has 5 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, Position or Time.
|
||||
|
||||
In all other respect they perform the same functions.
|
||||
**GPS Adventure Sports II** has 6 screens, each of which displays just one of Speed, Altitude, Distance to waypoint, VMG to waypoint, Position or Time.
|
||||
|
||||
The waypoints list is the same as that used with the [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app so the same set of waypoints can be used across both apps. Refer to that app for waypoint file information.
|
||||
|
||||
|
@ -14,17 +8,17 @@ The waypoints list is the same as that used with the [GPS Navigation](https://ba
|
|||
|
||||
**BTN1** ( Speed and Altitude ) Short press < 2 secs toggles the display between last reading and maximum recorded. Long press > 2 secs resets the recorded maximum values.
|
||||
|
||||
**BTN1** ( Distance ) Select next waypoint. Last fix distance from selected waypoint is displayed.
|
||||
**BTN1** ( Distance and VMG ) Select next waypoint. Last fix distance from selected waypoint or speed towards is displayed.
|
||||
|
||||
**BTN2** : Disables/Restores power saving timeout. Locks the screen on and GPS in SuperE mode to enable reading for longer periods but uses maximum battery drain. Red LED (dot) at top of screen when screen is locked on. Press again to restore power saving timeouts.
|
||||
|
||||
**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, Position and Time
|
||||
**BTN3** : Cycles the screens between Speed, Altitude, Distance to waypoint, VMG to waypoint, Position and Time
|
||||
|
||||
**BTN3** : Long press exit and return to watch.
|
||||
|
||||
**Touch Screen** If the 'Touch' setting is ON then :
|
||||
|
||||
Swipe Left/Right cycles between the five screens.
|
||||
Swipe Left/Right cycles between the six screens.
|
||||
|
||||
Touch functions as BTN1 short press.
|
||||
|
||||
|
@ -51,9 +45,36 @@ When using the GPS Setup App this app switches the GPS to SuperE (default) mode
|
|||
|
||||
The MAX values continue to be collected with the display off so may appear a little odd after the intermittent fixes of the low power mode.
|
||||
|
||||
## Velocity Made Good - VMG
|
||||
|
||||
This implementation of VMG is very simplistic and is simply the component of your current vector ( course and speed ) that is towards your selected waypoint. It is displayed as negative if you are moving away from the waypoint. For it to be displayed you must be moving and the GPS must be able to detemrine a course. If not it will display '---' as the VMG.
|
||||
|
||||
## Mirroring to Android
|
||||
|
||||
This feature is an optional extra to solve and enhance a specific requirement for me. While sailing the Bangle.js watch screen is very difficult to read in bright sunlight while wearing the polaroid prescription lenses that I require on the water. The solution is to mirror the Bangle.js screen to an android device with a daylight readable OLED screen that I keep in a clear waterproof case on the boat. Using this mirroring feature I can see the GPS Adv Sports II app easily at all times, either on my wrist or on the bigger android device while still having full control over the display via the watch buttons.
|
||||
|
||||
There is a caveat. While in use the watch GPS stays in SuperE mode in order to keep the android screen updates going which means a higher battery use on the Bangle.js.
|
||||
|
||||
How is this mirroring done?
|
||||
|
||||
Install Droidscript on your Android device. Must have BLE suport and the PuckJS plugin installed. The Droidscript script can be found in the BangleApps GIT repository : https://github.com/espruino/BangleApps/tree/master/apps/speedalt2
|
||||
|
||||
The Droidscript script file is called : **GPS Adv Sports II.js**
|
||||
|
||||
**Important Gotcha :** For the BLE comms to find and connect to the Bangle.js it must be paired with the Android device but **NOT** connected. The Bangle.js must have been set in the Bluetooth settings as connectable.
|
||||
|
||||
Start/Stop buttons tell the Bangle.js to start or stop sending BLE data packets to the Android device. While stopped the Bangle.js reverts to full power saving mode when the screen is asleep.
|
||||
|
||||
When runnig a blue 'led' will flash each time a data packet is recieved to refresh the android display.
|
||||
|
||||
An orange 'led' will flash for each reconnection attempt if no data is received for 30 seconds. It will keep trying to reconnect so you can restart the Bangle, run another Bangle app or temprarily turn off bluetooth. The android mirror display will automatically reconnect when the GPS Adv Sports II app is running on the Bangle again. ( Designed to leave the Android device running as the display mirror in a sealed case all day while retaining the ability to do other functions on the Bangle.js and returning to the GPS Speed Alt II app. )
|
||||
|
||||
Android Screen Mirroring:<br>
|
||||
<p>
|
||||
|
||||
## Waypoints
|
||||
|
||||
Waypoints are used in [D]istance mode. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode.
|
||||
Waypoints are used in Distance and VMG modes. Create a file waypoints.json and write to storage on the Bangle.js using the IDE. The first 6 characters of the name are displayed in Speed+[D]istance mode.
|
||||
|
||||
The [GPS Navigation](https://banglejs.com/apps/#gps%20navigation) app in the App Loader has a really nice waypoints file editor. (Must be connected to your Bangle.JS and then click on the Download icon.)
|
||||
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
Speed and Altitude [speedalt2]
|
||||
Mike Bennett mike[at]kereru.com
|
||||
1.10 : add inverted colours
|
||||
1.14 : Add VMG screen
|
||||
1.34 : Add bluetooth data stream for Droidscript
|
||||
1.43 : Keep GPS in SuperE mode while using Droiscript screen mirroring
|
||||
*/
|
||||
var v = '1.10';
|
||||
var v = '1.46';
|
||||
var vDroid = '1.46'; // Required DroidScript program version
|
||||
|
||||
/*kalmanjs, Wouter Bulten, MIT, https://github.com/wouterbulten/kalmanjs */
|
||||
var KalmanFilter = (function () {
|
||||
|
@ -180,14 +184,10 @@ let LED = // LED as minimal and only definition (as instance / singleton)
|
|||
, toggle: function() { this.set( ! this.isOn); } // toggle the LED
|
||||
}, LED1 = LED; // LED1 as 'synonym' for LED
|
||||
|
||||
// Load fonts
|
||||
//require("Font7x11Numeric7Seg").add(Graphics);
|
||||
|
||||
var lf = {fix:0,satellites:0};
|
||||
var showMax = 0; // 1 = display the max values. 0 = display the cur fix
|
||||
var pwrSav = 1; // 1 = default power saving with watch screen off and GPS to PMOO mode. 0 = screen kept on.
|
||||
var canDraw = 1;
|
||||
var time = ''; // Last time string displayed. Re displayed in background colour to remove before drawing new time.
|
||||
var tmrLP; // Timer for delay in switching to low power after screen turns off
|
||||
|
||||
var maxSpd = 0;
|
||||
|
@ -195,6 +195,8 @@ var maxAlt = 0;
|
|||
var maxN = 0; // counter. Only start comparing for max after a certain number of fixes to allow kalman filter to have smoohed the data.
|
||||
|
||||
var emulator = (process.env.BOARD=="EMSCRIPTEN")?1:0; // 1 = running in emulator. Supplies test values;
|
||||
var bt = 0; // 0 = bluetooth data feed off. 1 = on
|
||||
var btLast = 0; // time of last bt transmit
|
||||
|
||||
var wp = {}; // Waypoint to use for distance from cur position.
|
||||
|
||||
|
@ -215,16 +217,27 @@ function radians(a) {
|
|||
return a*Math.PI/180;
|
||||
}
|
||||
|
||||
function degrees(a) {
|
||||
var d = a*180/Math.PI;
|
||||
return (d+360)%360;
|
||||
}
|
||||
|
||||
function bearing(a,b){
|
||||
var delta = radians(b.lon-a.lon);
|
||||
var alat = radians(a.lat);
|
||||
var blat = radians(b.lat);
|
||||
var y = Math.sin(delta) * Math.cos(blat);
|
||||
var x = Math.cos(alat)*Math.sin(blat) -
|
||||
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||
return Math.round(degrees(Math.atan2(y, x)));
|
||||
}
|
||||
|
||||
function distance(a,b){
|
||||
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||
var y = radians(b.lat-a.lat);
|
||||
|
||||
// Distance in selected units
|
||||
// Distance in metres
|
||||
var d = Math.sqrt(x*x + y*y) * 6371000;
|
||||
d = (d/parseFloat(cfg.dist)).toFixed(2);
|
||||
if ( d >= 100 ) d = parseFloat(d).toFixed(1);
|
||||
if ( d >= 1000 ) d = parseFloat(d).toFixed(0);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
|
@ -328,8 +341,8 @@ function drawClock() {
|
|||
function drawWP(wp) {
|
||||
buf.setColor(3);
|
||||
buf.setFontAlign(0,1); //left, bottom
|
||||
buf.setFontVector(48);
|
||||
buf.drawString(wp,120,140);
|
||||
buf.setFontVector(40);
|
||||
buf.drawString(wp,120,132);
|
||||
}
|
||||
|
||||
function drawSats(sats) {
|
||||
|
@ -362,14 +375,18 @@ if ( emulator ) {
|
|||
var m;
|
||||
|
||||
var sp = '---';
|
||||
var al = '---';
|
||||
var di = '---';
|
||||
var age = '---';
|
||||
var al = sp;
|
||||
var di = sp;
|
||||
var brg = ''; // bearing
|
||||
var crs = ''; // course
|
||||
var age = sp;
|
||||
var lat = '---.--';
|
||||
var ns = '';
|
||||
var ew = '';
|
||||
var lon = '---.--';
|
||||
var sats = '---';
|
||||
var sats = sp;
|
||||
var vmg = sp;
|
||||
|
||||
|
||||
// Waypoint name
|
||||
var wpName = wp.name;
|
||||
|
@ -388,20 +405,32 @@ if ( emulator ) {
|
|||
if ( maxN <= 15 ) maxN++;
|
||||
}
|
||||
|
||||
// Bearing to waypoint
|
||||
brg = bearing(lf,wp);
|
||||
|
||||
// Current course
|
||||
crs = lf.course;
|
||||
|
||||
// Relative angle to wp
|
||||
var a = Math.max(crs,brg) - Math.min(crs,brg);
|
||||
if ( a >= 180 ) a = 360 -a;
|
||||
|
||||
// Speed
|
||||
if ( cfg.spd == 0 ) {
|
||||
m = require("locale").speed(lf.speed).match(/([0-9,\.]+)(.*)/); // regex splits numbers from units
|
||||
sp = parseFloat(m[1]);
|
||||
cfg.spd_unit = m[2];
|
||||
}
|
||||
else sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
||||
sp = parseFloat(lf.speed)/parseFloat(cfg.spd); // Calculate for selected units
|
||||
|
||||
// vmg
|
||||
if ( a >= 90 ) vmg = sp * Math.cos(radians(180-a)) * -1; // moving away from WP
|
||||
else vmg = sp * Math.cos(radians(a)); // towards wp
|
||||
|
||||
if ( sp < 10 ) sp = sp.toFixed(1);
|
||||
else sp = Math.round(sp);
|
||||
if (isNaN(sp)) sp = '---';
|
||||
|
||||
if (parseFloat(sp) > parseFloat(maxSpd) && maxN > 15 ) maxSpd = sp;
|
||||
|
||||
if ( Math.abs(vmg) >= 0.05 && Math.abs(vmg) < 10 ) vmg = vmg.toFixed(1);
|
||||
else vmg = Math.round(vmg);
|
||||
if (isNaN(vmg)) vmg = '---';
|
||||
|
||||
// Altitude
|
||||
al = lf.alt;
|
||||
al = Math.round(parseFloat(al)/parseFloat(cfg.alt));
|
||||
|
@ -410,7 +439,10 @@ if ( emulator ) {
|
|||
|
||||
// Distance to waypoint
|
||||
di = distance(lf,wp);
|
||||
if (isNaN(di)) di = '--------';
|
||||
di = (di/parseFloat(cfg.dist)).toFixed(2);
|
||||
if ( di >= 100 ) di = parseFloat(di).toFixed(1);
|
||||
if ( di >= 1000 ) di = parseFloat(di).toFixed(0);
|
||||
if (isNaN(di)) di = '------';
|
||||
|
||||
// Age of last fix (secs)
|
||||
age = Math.max(0,Math.round(getTime())-(lf.time.getTime()/1000));
|
||||
|
@ -433,9 +465,30 @@ if ( emulator ) {
|
|||
|
||||
}
|
||||
|
||||
// Bluetooth send data
|
||||
btSend({
|
||||
id:'speedalt2',
|
||||
v:v,
|
||||
vd:vDroid,
|
||||
m:cfg.modeA,
|
||||
spd_unit:cfg.spd_unit,
|
||||
alt_unit:cfg.alt_unit,
|
||||
dist_unit:cfg.dist_unit,
|
||||
wp:wpName,
|
||||
sp:sp,
|
||||
al:al,
|
||||
di:di,
|
||||
sats:sats,
|
||||
vmg:vmg,
|
||||
lat:lat,
|
||||
lon:lon,
|
||||
ns:ns,
|
||||
ew:ew
|
||||
});
|
||||
|
||||
if ( cfg.modeA == 0 ) {
|
||||
// Speed
|
||||
if ( showMax )
|
||||
if ( showMax ) {
|
||||
drawScrn({
|
||||
val:maxSpd,
|
||||
unit:cfg.spd_unit,
|
||||
|
@ -444,7 +497,8 @@ if ( emulator ) {
|
|||
max:'MAX',
|
||||
wp:''
|
||||
}); // Speed maximums
|
||||
else
|
||||
}
|
||||
else {
|
||||
drawScrn({
|
||||
val:sp,
|
||||
unit:cfg.spd_unit,
|
||||
|
@ -454,10 +508,11 @@ if ( emulator ) {
|
|||
wp:''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 1 ) {
|
||||
// Alt
|
||||
if ( showMax )
|
||||
if ( showMax ) {
|
||||
drawScrn({
|
||||
val:maxAlt,
|
||||
unit:cfg.alt_unit,
|
||||
|
@ -466,7 +521,8 @@ if ( emulator ) {
|
|||
max:'MAX',
|
||||
wp:''
|
||||
}); // Alt maximums
|
||||
else
|
||||
}
|
||||
else {
|
||||
drawScrn({
|
||||
val:al,
|
||||
unit:cfg.alt_unit,
|
||||
|
@ -476,6 +532,7 @@ if ( emulator ) {
|
|||
wp:''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 2 ) {
|
||||
// Dist
|
||||
|
@ -490,6 +547,18 @@ if ( emulator ) {
|
|||
}
|
||||
|
||||
if ( cfg.modeA == 3 ) {
|
||||
// VMG
|
||||
drawScrn({
|
||||
val:vmg,
|
||||
unit:cfg.spd_unit,
|
||||
sats:sats,
|
||||
age:age,
|
||||
max:'VMG',
|
||||
wp:wpName
|
||||
});
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 4 ) {
|
||||
// Position
|
||||
drawPosn({
|
||||
sats:sats,
|
||||
|
@ -501,7 +570,7 @@ if ( emulator ) {
|
|||
});
|
||||
}
|
||||
|
||||
if ( cfg.modeA == 4 ) {
|
||||
if ( cfg.modeA == 5 ) {
|
||||
// Large clock
|
||||
drawClock();
|
||||
}
|
||||
|
@ -510,14 +579,14 @@ if ( emulator ) {
|
|||
|
||||
function prevScrn() {
|
||||
cfg.modeA = cfg.modeA-1;
|
||||
if ( cfg.modeA < 0 ) cfg.modeA = 4;
|
||||
if ( cfg.modeA < 0 ) cfg.modeA = 5;
|
||||
savSettings();
|
||||
onGPS(lf);
|
||||
}
|
||||
|
||||
function nextScrn() {
|
||||
cfg.modeA = cfg.modeA+1;
|
||||
if ( cfg.modeA > 4 ) cfg.modeA = 0;
|
||||
if ( cfg.modeA > 5 ) cfg.modeA = 0;
|
||||
savSettings();
|
||||
onGPS(lf);
|
||||
}
|
||||
|
@ -529,14 +598,14 @@ function nextFunc(dur) {
|
|||
if ( dur < 2 ) showMax = !showMax; // Short press toggle fix/max display
|
||||
else { maxSpd = 0; maxAlt = 0; } // Long press resets max values.
|
||||
}
|
||||
else if ( cfg.modeA == 2) nxtWp(); // Dist mode - Select next waypoint
|
||||
else if ( cfg.modeA == 2 || cfg.modeA == 3) nxtWp(); // Dist or VMG mode - Select next waypoint
|
||||
onGPS(lf);
|
||||
}
|
||||
|
||||
|
||||
function updateClock() {
|
||||
if (!canDraw) return;
|
||||
if ( cfg.modeA != 4 ) return;
|
||||
if ( cfg.modeA != 5 ) return;
|
||||
drawClock();
|
||||
if ( emulator ) {maxSpd++;maxAlt++;}
|
||||
}
|
||||
|
@ -551,6 +620,7 @@ function startDraw(){
|
|||
|
||||
function stopDraw() {
|
||||
canDraw=false;
|
||||
if ( bt ) return; // If bt screen mirror to Droidscript in use then keep GPS in SuperE mode to keep screen updates going.
|
||||
if (!tmrLP) tmrLP=setInterval(function () {if (lf.fix) setLpMode('PSMOO');}, 10000); //Drop to low power in 10 secs. Keep lp mode off until we have a first fix.
|
||||
}
|
||||
|
||||
|
@ -564,6 +634,20 @@ function setLpMode(m) {
|
|||
gpssetup.setPowerMode({power_mode:m});
|
||||
}
|
||||
|
||||
// == Droidscript bluetooth data
|
||||
|
||||
function btOn(b) {
|
||||
bt = b; // Turn data transmit on/off
|
||||
}
|
||||
|
||||
function btSend(dat) {
|
||||
if ( ! bt ) return; // bt transmit off
|
||||
var dur = getTime() - btLast;
|
||||
if ( dur < 1.0 ) return; // Don't need to transmit more than every 1.0 secs.
|
||||
btLast = getTime();
|
||||
console.log(JSON.stringify(dat)); // transmit the data
|
||||
}
|
||||
|
||||
// == Events
|
||||
|
||||
function setButtons(){
|
||||
|
@ -593,17 +677,6 @@ function setButtons(){
|
|||
setWatch(function(e){
|
||||
nextScrn();
|
||||
}, BTN3, {repeat:true,edge:"falling"});
|
||||
|
||||
/*
|
||||
// Touch screen same as BTN1 short
|
||||
setWatch(function(e){
|
||||
nextFunc(1); // Same as BTN1 short
|
||||
}, BTN4, {repeat:true,edge:"falling"});
|
||||
setWatch(function(e){
|
||||
nextFunc(1); // Same as BTN1 short
|
||||
}, BTN5, {repeat:true,edge:"falling"});
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
|
@ -621,40 +694,22 @@ Bangle.on('swipe',function(dir) {
|
|||
Bangle.on('touch', function(button){
|
||||
if ( ! cfg.touch ) return;
|
||||
nextFunc(0); // Same function as short BTN1
|
||||
/*
|
||||
switch(button){
|
||||
case 1: // BTN4
|
||||
console.log('BTN4');
|
||||
prevScrn();
|
||||
break;
|
||||
case 2: // BTN5
|
||||
console.log('BTN5');
|
||||
nextScrn();
|
||||
break;
|
||||
case 3:
|
||||
console.log('MDL');
|
||||
nextFunc(0); // Centre - same function as short BTN1
|
||||
break;
|
||||
}
|
||||
*/
|
||||
});
|
||||
|
||||
|
||||
|
||||
// == Main Prog
|
||||
|
||||
// Read settings.
|
||||
let cfg = require('Storage').readJSON('speedalt2.json',1)||{};
|
||||
|
||||
cfg.spd = cfg.spd||0; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd_unit = cfg.spd_unit||''; // Displayed speed unit
|
||||
cfg.spd = cfg.spd||1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||
cfg.spd_unit = cfg.spd_unit||'kph'; // Displayed speed unit
|
||||
cfg.alt = cfg.alt||0.3048;// Multiplier for altitude unit conversions.
|
||||
cfg.alt_unit = cfg.alt_unit||'feet'; // Displayed altitude units
|
||||
cfg.dist = cfg.dist||1000;// Multiplier for distnce unit conversions.
|
||||
cfg.dist_unit = cfg.dist_unit||'km'; // Displayed altitude units
|
||||
cfg.colour = cfg.colour||0; // Colour scheme.
|
||||
cfg.wp = cfg.wp||0; // Last selected waypoint for dist
|
||||
cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3=Position 4=Clock
|
||||
cfg.modeA = cfg.modeA||0; // 0=Speed 1=Alt 2=Dist 3 = vmg 4=Position 5=Clock
|
||||
cfg.primSpd = cfg.primSpd||0; // 1 = Spd in primary, 0 = Spd in secondary
|
||||
|
||||
cfg.spdFilt = cfg.spdFilt==undefined?true:cfg.spdFilt;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "speedalt2",
|
||||
"name": "GPS Adventure Sports II",
|
||||
"shortName":"GPS Adv Sport II",
|
||||
"version":"1.10",
|
||||
"version":"1.46",
|
||||
"description": "GPS speed, altitude and distance to waypoint display. Designed for easy viewing and use during outdoor activities such as para-gliding, hang-gliding, sailing, cycling etc.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
|
@ -48,7 +48,7 @@
|
|||
const unitsMenu = {
|
||||
'': {'title': 'Units'},
|
||||
'< Back': function() { E.showMenu(appMenu); },
|
||||
'default (spd)' : function() { setUnits(0,''); },
|
||||
// 'default (spd)' : function() { setUnits(0,''); },
|
||||
'Kph (spd)' : function() { setUnits(1,'kph'); },
|
||||
'Knots (spd)' : function() { setUnits(1.852,'kts'); },
|
||||
'Mph (spd)' : function() { setUnits(1.60934,'mph'); },
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Change start sequence to BTN1/3/1/3 to avoid accidental turning on (fix #342)
|
||||
0.03: Add Color Changing Settings
|
||||
0.04: Add Support For Bangle.js 2
|
||||
|
|
|
@ -14,5 +14,6 @@ g.setColor(settings.bg);
|
|||
g.fillRect(0,0,g.getWidth(),g.getHeight());
|
||||
// Any button turns off
|
||||
setWatch(()=>load(), BTN1);
|
||||
setWatch(()=>load(), BTN2);
|
||||
setWatch(()=>load(), BTN3);
|
||||
if (global.BTN2) setWatch(()=>load(), BTN2);
|
||||
if (global.BTN3) setWatch(()=>load(), BTN3);
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
"id": "torch",
|
||||
"name": "Torch",
|
||||
"shortName": "Torch",
|
||||
"version": "0.03",
|
||||
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets. You can also set the color through the apps settings menu.",
|
||||
"version": "0.04",
|
||||
"description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets on Bangle.js 1. You can also set the color through the app's setting menu.",
|
||||
"icon": "app.png",
|
||||
"tags": "tool,torch",
|
||||
"supports": ["BANGLEJS"],
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"torch.app.js","url":"app.js"},
|
||||
{"name":"torch.wid.js","url":"widget.js"},
|
||||
{"name":"torch.wid.js","url":"widget.js","supports": ["BANGLEJS"]},
|
||||
{"name":"torch.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"torch.settings.js","url":"settings.js"}
|
||||
]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: Created
|
||||
0.02: Set sort order to -10 so always display in right hand corner
|
||||
0.03: Set sort order from the code
|
||||
0.04: fix vertical align
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"shortName":"Battery Theme",
|
||||
"icon": "widbata.png",
|
||||
"screenshots": [{"url":"screenshot_widbata_1.png"}],
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"type": "widget",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
|
|
|
@ -5,7 +5,7 @@ Bangle.on('lcdPower', function(on) {
|
|||
WIDGETS["bata"]={area:"tr",sortorder:-10,width:27,draw:function() {
|
||||
var s = 26;
|
||||
var t = 13; // thickness
|
||||
var x = this.x, y = this.y;
|
||||
var x = this.x, y = this.y + 5;
|
||||
g.reset();
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillRect(x,y+2,x+s-4,y+2+t); // outer
|
||||
|
|
|
@ -157,19 +157,29 @@ log(untranslatedStrings.filter(e => e.uses>2).filter(e => !translatedStrings.fin
|
|||
log("");
|
||||
//process.exit(1);
|
||||
|
||||
var languages = JSON.parse(fs.readFileSync(BASEDIR+"/lang/index.json").toString());
|
||||
let languages = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/index.json`).toString());
|
||||
languages.forEach(language => {
|
||||
if (language.code == "en_GB") {
|
||||
console.log("Ignoring "+language.code);
|
||||
console.log(`Ignoring ${language.code}`);
|
||||
return;
|
||||
}
|
||||
console.log("Scanning "+language.code);
|
||||
console.log(`Scanning ${language.code}`);
|
||||
log(language.code);
|
||||
log("==========");
|
||||
var translations = JSON.parse(fs.readFileSync(BASEDIR+"/lang/"+language.url).toString());
|
||||
translatedStrings.forEach(str => {
|
||||
if (!translations.GLOBAL[str.str])
|
||||
console.log(`Missing translation for ${JSON.stringify(str)}`);
|
||||
let translations = JSON.parse(fs.readFileSync(`${BASEDIR}/lang/${language.url}`).toString());
|
||||
translatedStrings.forEach(translationItem => {
|
||||
if (!translations.GLOBAL[translationItem.str]) {
|
||||
console.log(`Missing GLOBAL translation for ${JSON.stringify(translationItem)}`);
|
||||
translationItem.files.forEach(file => {
|
||||
let m = file.match(/\/([a-zA-Z0-9_-]*)\//g);
|
||||
if (m && m[0]) {
|
||||
let appName = m[0].replaceAll("/", "");
|
||||
if (translations[appName] && translations[appName][translationItem.str]) {
|
||||
console.log(` but LOCAL translation found in \"${appName}\"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
log("");
|
||||
});
|
||||
|
|
2
core
2
core
|
@ -1 +1 @@
|
|||
Subproject commit c243e6e71f88358de720ad16ba8515b32b8d650f
|
||||
Subproject commit 187af1527e0b830c804049aae834ed658ffeed08
|
139
lang/it_IT.json
139
lang/it_IT.json
|
@ -1,21 +1,148 @@
|
|||
{
|
||||
"//":"Italian language translations",
|
||||
"//1": "Italian language translations",
|
||||
"GLOBAL": {
|
||||
"//": "Translations that apply for all apps",
|
||||
"On": "On",
|
||||
"on": "on",
|
||||
"Off": "Off",
|
||||
"off": "off",
|
||||
"Ok": "Ok",
|
||||
"Yes": "Sì",
|
||||
"No": "No",
|
||||
"Alarm": "Sveglia",
|
||||
"ALARM": "SVEGLIA",
|
||||
"Alarms": "Sveglie",
|
||||
"Date": "Data",
|
||||
"Year": "Anno",
|
||||
"Month": "Mese",
|
||||
"Day": "Giorno",
|
||||
"Hour": "Ora",
|
||||
"Hours": "Ore",
|
||||
"Minute": "Minuto",
|
||||
"Minutes": "Minuti",
|
||||
"Enabled" : "Attiva",
|
||||
"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"
|
||||
"Sleep": "Dormi",
|
||||
"Timer": "Timer",
|
||||
"TIMER": "TIMER",
|
||||
"New Timer": "Nuovo timer",
|
||||
"(repeat)": "(ripeti)",
|
||||
"Auto snooze": "Posticipa automaticamente",
|
||||
"Connected": "Connesso",
|
||||
"Delete all messages": "Cancella tutti i messaggi",
|
||||
"Delete All Messages": "Cancella tutti i messaggi",
|
||||
"Message": "Messaggio",
|
||||
"Messages": "Messaggi",
|
||||
"No Messages": "Nessun messaggio",
|
||||
"Keep Msgs": "Tieni i messaggi",
|
||||
"Mark Unread": "Segna come non letto",
|
||||
"Vibrate": "Vibrazione",
|
||||
"Are you sure": "Sei sicuro/a",
|
||||
"Music": "Musica",
|
||||
"Apps": "App",
|
||||
"App Settings": "Impostazioni app",
|
||||
"Bluetooth": "Bluetooth",
|
||||
"BLE": "BLE",
|
||||
"Make Connectable": "Rendi collegabile",
|
||||
"Programmable": "Programmabile",
|
||||
"Remove": "Rimuovi",
|
||||
"Utils": "Utilità",
|
||||
"LCD": "LCD",
|
||||
"LCD Brightness": "Luminosità LCD",
|
||||
"LCD Timeout": "Timeout LCD",
|
||||
"Wake on BTN1": "Risveglia con BTN1",
|
||||
"Wake on BTN2": "Risveglia con BTN2",
|
||||
"Wake on BTN3": "Risveglia con BTN3",
|
||||
"Wake on FaceUp": "Risveglia a faccia in su",
|
||||
"Wake on Touch": "Risveglia al tocco",
|
||||
"Wake on Twist": "Risveglia con polso",
|
||||
"Twist Timeout": "Timeout torsione",
|
||||
"Twist Max Y": "Torsione Y max",
|
||||
"Twist Threshold": "Soglia torsione",
|
||||
"Customize": "Personalizza",
|
||||
"Add Device": "Aggiungi dispositivo",
|
||||
"Left": "Sinistra",
|
||||
"Right": "Destra",
|
||||
"Widgets": "Widget",
|
||||
"Settings": "Impostazioni",
|
||||
"No app has settings": "Non ci sono app con delle impostazioni",
|
||||
"System": "Sistema",
|
||||
"Alerts": "Avvisi",
|
||||
"Theme": "Tema",
|
||||
"Foreground": "Primo piano",
|
||||
"Background": "Sfondo",
|
||||
"Foreground 2": "Primo piano 2",
|
||||
"Background 2": "Sfondo 2",
|
||||
"Highlight FG": "Selezione PP",
|
||||
"Highlight BG": "Selezione Sf",
|
||||
"Utilities": "Utilità",
|
||||
"Storage": "Memoria",
|
||||
"Compact Storage": "Compatta memoria",
|
||||
"Select Clock": "Seleziona orologio",
|
||||
"No Clocks Found": "Nessun orologio trovato",
|
||||
"Locale": "Localizzazione",
|
||||
"Set Time": "Imposta orario",
|
||||
"Time Zone": "Fuso orario",
|
||||
"Whitelist": "Whitelist",
|
||||
"Quiet Mode": "Modalità silenziosa",
|
||||
"Disable": "Disabilita",
|
||||
"Vibration": "Vibrazione",
|
||||
"Show": "Mostra",
|
||||
"Hide": "Nascondi",
|
||||
"Rewrite Settings": "Riscrivi impostazioni",
|
||||
"Reset Settings": "Reset impostazioni",
|
||||
"Factory Reset": "Ripristino condizioni iniziali",
|
||||
"Flatten Battery": "Scarica la batteria",
|
||||
"Turn Off": "Spegni",
|
||||
"This will remove everything": "Questo rimuoverà TUTTO",
|
||||
"Error in settings": "Errore nelle impostazioni",
|
||||
"Invalid settings": "Impostazioni non valide",
|
||||
"Loading": "Caricamento",
|
||||
"Launcher Settings": "Impostazioni Launcher",
|
||||
"Font": "Font",
|
||||
"Show clocks": "Mostra orologi",
|
||||
"Log": "Log",
|
||||
"Steps": "Passi",
|
||||
"steps": "passi"
|
||||
},
|
||||
"alarm": {
|
||||
"//":"App-specific overrides",
|
||||
"rpt" : "ripeti"
|
||||
"//2": "App-specific overrides",
|
||||
"launch": {
|
||||
"Vector font size": "Dim. font vett.",
|
||||
"App Source\nNot found": "Codice app\nnon trovato"
|
||||
},
|
||||
"messages": {
|
||||
"Unread timer": "Timer msg non letti"
|
||||
},
|
||||
"run": {
|
||||
"Record Run": "Registra corsa"
|
||||
},
|
||||
"setting": {
|
||||
"Clock Style": "Formato ora",
|
||||
"Compacting...\nTakes approx\n1 minute": "Compattamento in corso...\nCi vorrà circa un minuto",
|
||||
"//1": "The new line before 'operazione' improves the layout",
|
||||
"Flattening battery - this can take hours.\nLong-press button to cancel": "Scaricamento batteria in corso - l'\noperazione può richiedere ore. Tieni premuto il pulsante per annullare",
|
||||
"Reset to Defaults": "Ripristinare le impostazioni predefinite",
|
||||
"Connectable": "Collegamento",
|
||||
"Connect device\nto add to\nwhitelist": "Collega un\ndispositivo\nper metterlo\nin whitelist",
|
||||
"Stay Connectable": "Rimanere collegabile",
|
||||
"Light BW": "Chiaro",
|
||||
"Dark BW": "Scuro"
|
||||
},
|
||||
"wid_edit": {
|
||||
"Reset": "Ripristina",
|
||||
"Reset All": "Ripristina tutto",
|
||||
"Reset all widgets": "Ripristina tutti i widget",
|
||||
"Sort Order": "Ordinamento",
|
||||
"Side": "Lato"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@ function Layout(layout, options) {
|
|||
|
||||
// Handler for touch events
|
||||
function touchHandler(l,e) {
|
||||
if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) {
|
||||
if (l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) {
|
||||
if (e.type==2 && l.cbl) l.cbl(e); else if (l.cb) l.cb(e);
|
||||
}
|
||||
if (l.c) l.c.forEach(n => touchHandler(n,e));
|
||||
|
|
Loading…
Reference in New Issue