Merge branch 'espruino:master' into master
commit
04cff3c6a0
|
|
@ -20,3 +20,4 @@
|
||||||
0.17: Load holidays before events so the latter is not overpainted
|
0.17: Load holidays before events so the latter is not overpainted
|
||||||
0.18: Minor code improvements
|
0.18: Minor code improvements
|
||||||
0.19: Read events synchronized from Gadgetbridge
|
0.19: Read events synchronized from Gadgetbridge
|
||||||
|
0.20: Correct start time of all-day events synchronized from Gadgetbridge
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,11 @@ const loadEvents = () => {
|
||||||
}));
|
}));
|
||||||
// all events synchronized from Gadgetbridge
|
// all events synchronized from Gadgetbridge
|
||||||
events = events.concat((require("Storage").readJSON("android.calendar.json",1) || []).map(a => {
|
events = events.concat((require("Storage").readJSON("android.calendar.json",1) || []).map(a => {
|
||||||
// timestamp is in seconds, Date requires milliseconds
|
// All-day events always start at 00:00:00 UTC, so we need to "undo" the
|
||||||
const date = new Date(a.timestamp * 1000);
|
// timezone offsetting to make sure that the day is correct.
|
||||||
return {date: date, msg: a.title, type: "e"};
|
const offset = a.allDay ? new Date().getTimezoneOffset() * 60 : 0
|
||||||
|
const date = new Date((a.timestamp+offset) * 1000);
|
||||||
|
return {date: date, msg: a.title, type: a.allDay ? "o" : "e"};
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "calendar",
|
"id": "calendar",
|
||||||
"name": "Calendar",
|
"name": "Calendar",
|
||||||
"version": "0.19",
|
"version": "0.20",
|
||||||
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
|
"description": "Monthly calendar, displays holidays uploaded from the web interface and scheduled events.",
|
||||||
"icon": "calendar.png",
|
"icon": "calendar.png",
|
||||||
"screenshots": [{"url":"screenshot_calendar.png"}],
|
"screenshots": [{"url":"screenshot_calendar.png"}],
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,4 @@
|
||||||
0.11: Use default Bangle formatter for booleans
|
0.11: Use default Bangle formatter for booleans
|
||||||
0.12: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
0.12: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
||||||
0.13: Upgrade to new translation system
|
0.13: Upgrade to new translation system
|
||||||
|
0.14: Fix auto-start saved state; fix clearing track number; allow widget clicks
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,7 @@ function rScroller(l) {
|
||||||
const w = g.stringWidth(l.label)+40,
|
const w = g.stringWidth(l.label)+40,
|
||||||
y = l.y+l.h/2;
|
y = l.y+l.h/2;
|
||||||
l.offset = l.offset%w;
|
l.offset = l.offset%w;
|
||||||
g.setClipRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
|
g.setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
|
||||||
.setColor(l.col).setBgColor(l.bgCol) // need to set colors: iScroll calls this function outside Layout
|
|
||||||
.setFontAlign(-1, 0) // left center
|
.setFontAlign(-1, 0) // left center
|
||||||
.clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
|
.clearRect(l.x, l.y, l.x+l.w-1, l.y+l.h-1)
|
||||||
.drawString(l.label, l.x-l.offset+40, y)
|
.drawString(l.label, l.x-l.offset+40, y)
|
||||||
|
|
@ -128,57 +127,8 @@ function rInfo(l) {
|
||||||
.setFontAlign(0, -1) // center top
|
.setFontAlign(0, -1) // center top
|
||||||
.drawString(l.label, l.x+l.w/2, l.y);
|
.drawString(l.label, l.x+l.w/2, l.y);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Render icon
|
|
||||||
* @param l
|
|
||||||
*/
|
|
||||||
function rIcon(l) {
|
|
||||||
const x2 = l.x+l.w-1,
|
|
||||||
y2 = l.y+l.h-1;
|
|
||||||
switch(l.icon) {
|
|
||||||
case "pause": {
|
|
||||||
const w13 = l.w/3;
|
|
||||||
g.drawRect(l.x, l.y, l.x+w13, y2);
|
|
||||||
g.drawRect(l.x+l.w-w13, l.y, x2, y2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "play": {
|
|
||||||
g.drawPoly([
|
|
||||||
l.x, l.y,
|
|
||||||
x2, l.y+l.h/2,
|
|
||||||
l.x, y2,
|
|
||||||
], true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "previous": {
|
|
||||||
const w15 = l.w*1/5;
|
|
||||||
g.drawPoly([
|
|
||||||
x2, l.y,
|
|
||||||
l.x+w15, l.y+l.h/2,
|
|
||||||
x2, y2,
|
|
||||||
], true);
|
|
||||||
g.drawRect(l.x, l.y, l.x+w15, y2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "next": {
|
|
||||||
const w45 = l.w*4/5;
|
|
||||||
g.drawPoly([
|
|
||||||
l.x, l.y,
|
|
||||||
l.x+w45, l.y+l.h/2,
|
|
||||||
l.x, y2,
|
|
||||||
], true);
|
|
||||||
g.drawRect(l.x+w45, l.y, x2, y2);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: { // red X
|
|
||||||
console.log(`Unknown icon: ${l.icon}`);
|
|
||||||
g.setColor("#f00")
|
|
||||||
.drawRect(l.x, l.y, x2, y2)
|
|
||||||
.drawLine(l.x, l.y, x2, y2)
|
|
||||||
.drawLine(l.x, y2, x2, l.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let layout;
|
let layout;
|
||||||
function makeUI() {
|
function makeUI() {
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
|
@ -417,7 +367,7 @@ function handleButton2Press() {
|
||||||
let tCommand = {};
|
let tCommand = {};
|
||||||
/**
|
/**
|
||||||
* Send command and highlight corresponding control
|
* Send command and highlight corresponding control
|
||||||
* @param {string} command - "play"/"pause"/"next"/"previous"/"volumeup"/"volumedown"
|
* @param {"play"|"pause"|"playpause"|"next"|"previous"|"volumeup"|"volumedown"} command
|
||||||
*/
|
*/
|
||||||
function sendCommand(command) {
|
function sendCommand(command) {
|
||||||
Bluetooth.println("");
|
Bluetooth.println("");
|
||||||
|
|
@ -433,15 +383,21 @@ function sendCommand(command) {
|
||||||
drawControls();
|
drawControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTouch(btn, pos) {
|
||||||
|
if (pos === undefined || pos.y >= Bangle.appRect.y) {
|
||||||
|
togglePlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function togglePlay() {
|
function togglePlay() {
|
||||||
sendCommand(stat==="play" ? "pause" : "play");
|
sendCommand("playpause");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup touch+swipe for Bangle.js 1
|
* Setup touch+swipe for Bangle.js 1
|
||||||
*/
|
*/
|
||||||
function touch1() {
|
function touch1() {
|
||||||
Bangle.on("touch", togglePlay);
|
Bangle.on("touch", handleTouch);
|
||||||
Bangle.on("swipe", dir => {
|
Bangle.on("swipe", dir => {
|
||||||
sendCommand(dir===1 ? "previous" : "next");
|
sendCommand(dir===1 ? "previous" : "next");
|
||||||
});
|
});
|
||||||
|
|
@ -450,7 +406,7 @@ function touch1() {
|
||||||
* Setup touch+swipe for Bangle.js 2
|
* Setup touch+swipe for Bangle.js 2
|
||||||
*/
|
*/
|
||||||
function touch2() {
|
function touch2() {
|
||||||
Bangle.on("touch", togglePlay);
|
Bangle.on("touch", handleTouch);
|
||||||
// swiping
|
// swiping
|
||||||
let drag;
|
let drag;
|
||||||
Bangle.on("drag", e => {
|
Bangle.on("drag", e => {
|
||||||
|
|
@ -483,10 +439,9 @@ function startLCDWatch() {
|
||||||
Bangle.on("lcdPower", (on) => {
|
Bangle.on("lcdPower", (on) => {
|
||||||
if (on) {
|
if (on) {
|
||||||
// redraw and resume scrolling
|
// redraw and resume scrolling
|
||||||
tick();
|
|
||||||
layout.render();
|
layout.render();
|
||||||
fadeOut();
|
fadeOut();
|
||||||
if (offset.offset!==null) {
|
if (layout.title.offset!==null) { // Making an assumption about what offset.offset was supposed to be
|
||||||
if (!iScroll) {
|
if (!iScroll) {
|
||||||
iScroll = setInterval(scroll, 200);
|
iScroll = setInterval(scroll, 200);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
setTimeout( // make other boot code run first, so we override e.g. android.boot.js GB
|
setTimeout( // make other boot code run first, so we override e.g. android.boot.js GB
|
||||||
() => {
|
() => {
|
||||||
const APP = global.__FILE__==="gbmusic.app.js",
|
const APP = globalThis.__FILE__==="gbmusic.app.js",
|
||||||
a = !!(require("Storage").readJSON("gbmusic.json", 1) || {}).autoStart;
|
a = !!(require("Storage").readJSON("gbmusic.json", 1) || {}).autoStart;
|
||||||
|
|
||||||
let s, i; // state, info
|
let s, i; // state, info
|
||||||
|
|
@ -10,7 +10,7 @@ setTimeout( // make other boot code run first, so we override e.g. android.boot.
|
||||||
* Only runs while other apps are loaded
|
* Only runs while other apps are loaded
|
||||||
*/
|
*/
|
||||||
function check() {
|
function check() {
|
||||||
if (s!=="play" || !i || !a || !Bangle.CLOCK) return; // only launch app if we know which song we are playing, and autoLoad is enabled
|
if ((!s || s.state!=="play") || !i || !a || !Bangle.CLOCK) return; // only launch app if we know which song we are playing, and autoLoad is enabled
|
||||||
delete (i.t);
|
delete (i.t);
|
||||||
// store info and launch music app
|
// store info and launch music app
|
||||||
require("Storage").writeJSON("gbmusic.load.json", {
|
require("Storage").writeJSON("gbmusic.load.json", {
|
||||||
|
|
@ -20,18 +20,19 @@ setTimeout( // make other boot code run first, so we override e.g. android.boot.
|
||||||
load("gbmusic.app.js");
|
load("gbmusic.app.js");
|
||||||
}
|
}
|
||||||
|
|
||||||
global.GB = (_GB => e => {
|
|
||||||
|
globalThis.GB = (_GB => e => {
|
||||||
// we eat music events!
|
// we eat music events!
|
||||||
switch(e.t) {
|
switch(e.t) {
|
||||||
case "musicinfo":
|
case "musicinfo":
|
||||||
i = e;
|
i = e;
|
||||||
return APP ? info(e) : check();
|
return APP ? globalThis.info(e) : check();
|
||||||
case "musicstate":
|
case "musicstate":
|
||||||
s = e.state;
|
s = e;
|
||||||
return APP ? state(e) : check();
|
return APP ? globalThis.state(e) : check();
|
||||||
default:
|
default:
|
||||||
// pass on other events
|
// pass on other events
|
||||||
if (_GB) setTimeout(_GB, 0, e);
|
if (_GB) setTimeout(_GB, 0, e);
|
||||||
}
|
}
|
||||||
})(global.GB);
|
})(globalThis.GB);
|
||||||
}, 1);
|
}, 1);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "gbmusic",
|
"id": "gbmusic",
|
||||||
"name": "Gadgetbridge Music Controls",
|
"name": "Gadgetbridge Music Controls",
|
||||||
"shortName": "Music Controls",
|
"shortName": "Music Controls",
|
||||||
"version": "0.13",
|
"version": "0.14",
|
||||||
"description": "Control the music on your Gadgetbridge-connected phone",
|
"description": "Control the music on your Gadgetbridge-connected phone",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"},
|
"screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
## Pacer
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Run with a virtual partner at your chosen pace, and export the GPX data
|
||||||
|
from the Bangle.js App Store.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Pacer starts up with a menu.
|
||||||
|
|
||||||
|
* **Recording** - whether to record the run
|
||||||
|
* **Units** - imperial or metric
|
||||||
|
* **Lap** - the multiple of a mile or kilometer to use for splits
|
||||||
|
* **Dark mode** - use black or white background
|
||||||
|
* **Eco battery** - display will turn off after 10 seconds
|
||||||
|
* **Eco storage** - only record GPS position every 10 seconds
|
||||||
|
* **Steps** - display step count or cadence
|
||||||
|
* **Pacer** - pace of virtual partner
|
||||||
|
|
||||||
|
On selecting **Start**, GPS position will be detected. A run cannot be
|
||||||
|
started without a GPS fix. The watch touchscreen is disabled while the
|
||||||
|
app is running.
|
||||||
|
|
||||||
|
The app will run on Bangle.js 1 and 2, although use on Bangle.js 2 is not
|
||||||
|
recommended due to poor GPS accuracy.
|
||||||
|
|
||||||
|
On a Bangle.js 1, the top button reverses the screen colours, the middle
|
||||||
|
button starts, pauses or resumes a run, and the bottom button ends the run.
|
||||||
|
|
||||||
|
On a Bangle.js 2, a short press of the button starts, pauses or resumes a
|
||||||
|
run, and a long press (over 0.5 seconds, but under 2!) ends the run. Note
|
||||||
|
that holding the button for 2 seconds will exit back to the default clock
|
||||||
|
app.
|
||||||
|
|
||||||
|
## Downloading
|
||||||
|
|
||||||
|
GPX tracks can be downloaded using the
|
||||||
|
[App Loader](https://banglejs.com/apps/?id=pacer). Connect the
|
||||||
|
Bangle.js and click on the Pacer app's disk icon to see the tracks
|
||||||
|
available for downloading.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
For best results, only start a run when the satellite signal strength bar is
|
||||||
|
green.
|
||||||
|
|
||||||
|
Use the [Assisted GPS Updater](https://banglejs.com/apps/#AGPS) to improve
|
||||||
|
the time taken to get a GPS fix.
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
|
||||||
|
The eco settings are unlikely to be useful.
|
||||||
|
|
||||||
|
GPS track smoothing is accomplished simply by reducing the frequency with
|
||||||
|
which readings are taken, depending on signal strength.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEw4cA///un/+2Eqee1nV+X26NjtfNGLsf+AQOg+kzAROnskyfACJs5CINACJshCKAjMgfJBoPwgpHLiGSpMk3kHyneO5OECIMl23Aj+AO5QRD7ZSJgPpknZkmNCIJFJhFJk1wgFtCIN4CJFCpMwAgIRCt5HIoVN8B/BCIVmEZFyt6RCCIb7Ih5SCmYRCkjoM7YRB78k6ARO++kzwQKhgRC4GEzARKswRD8mT///Lg8DtmbCILvEEw8DkmzCIUcCIWTN40bkmWUoUCCIWbCJGSrIpC5YoB5x7HCINLRZcAg3bsgwBsARNtgRBlgRLmyNBCIMlwAQJgLkBtuyMwIRHvJUBpP2GoMGCIOwCI1SBQOSr3bA4PJSQKtGSoWSpXYBAMZAwKcGoQRCpoLCZAOSoARFh5HCk4IDtmpnikMAAMOpO8CJ0KpKQKAAlyfoQANqVMCByGBt4ROgWSfhgACjmTvAROimTCB0A8mbYgwAIwmYEZ/07wRPj/wCJ4AI"))
|
||||||
|
|
@ -0,0 +1,843 @@
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
g.clear();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
var cfg;
|
||||||
|
try {
|
||||||
|
cfg = require("Storage").readJSON("pacer.json",true)||{};
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
cfg = {};
|
||||||
|
}
|
||||||
|
if (process.env.BOARD == 'BANGLEJS2')
|
||||||
|
var bangle2 = true;
|
||||||
|
else
|
||||||
|
var bangle2 = false;
|
||||||
|
|
||||||
|
var laps = ["Off","0.25","0.5","1","2","5","10"];
|
||||||
|
var fg = 1;
|
||||||
|
var fixed = false;
|
||||||
|
var started = false;
|
||||||
|
var startHidden = false;
|
||||||
|
var recording = false;
|
||||||
|
var invID = 0;
|
||||||
|
var intID = 0;
|
||||||
|
var startID = 0;
|
||||||
|
var cadID = 0;
|
||||||
|
var finID = 0;
|
||||||
|
var lapID = 0;
|
||||||
|
var steps = 0;
|
||||||
|
var sats = 0;
|
||||||
|
var ctr = 0;
|
||||||
|
var elapsed_ms = 0;
|
||||||
|
var finish_ms = 0;
|
||||||
|
var lap_start_ms = 0;
|
||||||
|
var lap_ms;
|
||||||
|
var gps = {fix:0,satellites:0};
|
||||||
|
var fp;
|
||||||
|
var start_time;
|
||||||
|
var current_time;
|
||||||
|
var paused_time = 0;
|
||||||
|
var last_time = 0;
|
||||||
|
var begin_pause;
|
||||||
|
var next_lap = 0.0;
|
||||||
|
var skip_ctr = 0;
|
||||||
|
var skip_max = 0;
|
||||||
|
var force_write = true;
|
||||||
|
var show_lap = false;
|
||||||
|
var lcd_on = true;
|
||||||
|
var drawSats = true;
|
||||||
|
var dist = 0.0;
|
||||||
|
var pdist = 0.0;
|
||||||
|
var oldDist = 0.0;
|
||||||
|
var oldLat = -1;
|
||||||
|
var oldLon = -1;
|
||||||
|
var cadence = 0;
|
||||||
|
var pace = 0;
|
||||||
|
var ppace = 0;
|
||||||
|
var R = 6371;
|
||||||
|
var stepTimes = [];
|
||||||
|
var dists = [];
|
||||||
|
|
||||||
|
function pace_str(pval) {
|
||||||
|
var psecs = 295 + 5 * pval;
|
||||||
|
return ''+Math.floor(psecs/60)+':'+('0'+psecs%60).substr(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaults() {
|
||||||
|
if (typeof(cfg.record) != 'boolean')
|
||||||
|
cfg.record = true;
|
||||||
|
if (typeof(cfg.metric) != 'boolean')
|
||||||
|
cfg.metric = false;
|
||||||
|
if (typeof(cfg.lap_idx) != 'number')
|
||||||
|
cfg.lap_idx = 3;
|
||||||
|
if (typeof(cfg.dark) != 'boolean')
|
||||||
|
cfg.dark = true;
|
||||||
|
if (typeof(cfg.eco) != 'boolean')
|
||||||
|
cfg.eco = false;
|
||||||
|
if (typeof(cfg.storage) != 'boolean')
|
||||||
|
cfg.storage = false;
|
||||||
|
if (typeof(cfg.show_steps) != 'boolean')
|
||||||
|
cfg.show_steps = false;
|
||||||
|
if (typeof(cfg.pacer) != 'number')
|
||||||
|
cfg.pacer = 0;
|
||||||
|
fg = cfg.dark?1:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function genFilename() {
|
||||||
|
var today=new Date();
|
||||||
|
return ('.pacer'+today.getFullYear()+('0'+(today.getMonth()+1)).substr(-2)+('0'+today.getDate()).substr(-2)+('0'+today.getHours()).substr(-2)+('0'+today.getMinutes()).substr(-2)+('0'+today.getSeconds()).substr(-2)+'.csv');
|
||||||
|
}
|
||||||
|
|
||||||
|
function doCadence() {
|
||||||
|
if (steps > 0)
|
||||||
|
clearInterval(cadID);
|
||||||
|
cadID = setTimeout(function() {
|
||||||
|
cadence = 0;
|
||||||
|
}, 2000);
|
||||||
|
if (recording) {
|
||||||
|
steps++;
|
||||||
|
stepTimes.push(Date.now());
|
||||||
|
stepTimes = stepTimes.slice(-20);
|
||||||
|
const elapsed = stepTimes[stepTimes.length - 1] - stepTimes[0];
|
||||||
|
cadence = elapsed ? Math.round(60000 * (stepTimes.length - 1) / elapsed) : 0;
|
||||||
|
} else
|
||||||
|
stepTimes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function doPace(thistime,thisdist) {
|
||||||
|
dists.push([thistime,thisdist]);
|
||||||
|
dists = dists.slice(-30);
|
||||||
|
const thiselapsed = dists[dists.length - 1][0] - dists[0][0];
|
||||||
|
const thisdistance = dists[dists.length - 1][1] - dists[0][1];
|
||||||
|
pace = thisdistance ? ((thiselapsed) / thisdistance) / 1000 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countStep() {
|
||||||
|
if (recording)
|
||||||
|
steps++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcCrow(lat1, lon1, lat2, lon2)
|
||||||
|
{
|
||||||
|
var dLat = toRad(lat2-lat1);
|
||||||
|
var dLon = toRad(lon2-lon1);
|
||||||
|
var lat1r = toRad(lat1);
|
||||||
|
var lat2r = toRad(lat2);
|
||||||
|
|
||||||
|
var a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1r) * Math.cos(lat2r);
|
||||||
|
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||||
|
var d = R * c;
|
||||||
|
if (isNaN(d))
|
||||||
|
return 0;
|
||||||
|
else
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRad(Value)
|
||||||
|
{
|
||||||
|
return Value * Math.PI / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGPS(fix) {
|
||||||
|
var newLat, newLon, newTime, newDist;
|
||||||
|
|
||||||
|
try {
|
||||||
|
newTime = fix.time.getTime();
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
newTime = NaN;
|
||||||
|
}
|
||||||
|
newLat = fix.lat;
|
||||||
|
newLon = fix.lon;
|
||||||
|
gps = fix;
|
||||||
|
if (!cfg.storage) {
|
||||||
|
if (gps.satellites >= 8)
|
||||||
|
skip_max = 0;
|
||||||
|
else if (gps.satellites < 4)
|
||||||
|
skip_max = 5;
|
||||||
|
else
|
||||||
|
skip_max = 8 - gps.satellites;
|
||||||
|
}
|
||||||
|
if (recording && cfg.pacer > 0 && skip_ctr >= (cfg.storage ? 9 : skip_max))
|
||||||
|
pdist = (elapsed_ms / 1000) / ppace;
|
||||||
|
if (isNaN(newLat) || isNaN(newLon) || isNaN(newTime)) {
|
||||||
|
skip_ctr = 0;
|
||||||
|
skip_max = 0;
|
||||||
|
force_write = true;
|
||||||
|
} else {
|
||||||
|
if (oldLat != -1 && recording) {
|
||||||
|
skip_ctr++;
|
||||||
|
if (skip_ctr > (cfg.storage ? 9 : skip_max)) {
|
||||||
|
skip_ctr = 0;
|
||||||
|
oldDist = dist;
|
||||||
|
newDist = calcCrow(oldLat, oldLon, newLat, newLon);
|
||||||
|
dist += newDist;
|
||||||
|
doPace(newTime,dist);
|
||||||
|
oldLat = newLat;
|
||||||
|
oldLon = newLon;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldLat = newLat;
|
||||||
|
oldLon = newLon;
|
||||||
|
}
|
||||||
|
if (recording && cfg.record && (force_write || skip_ctr == 0)) {
|
||||||
|
fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n");
|
||||||
|
last_time = gps.time;
|
||||||
|
if (force_write) {
|
||||||
|
skip_ctr = 0;
|
||||||
|
force_write = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawInvert() {
|
||||||
|
// not applicable to bangle2
|
||||||
|
g.drawImage(atob("DQ0BD4HjHwT4L8D+B/A/gfwL4J8EeMD4AA=="),225,26);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSatIcon() {
|
||||||
|
if (bangle2)
|
||||||
|
g.drawImage(atob("CQkBIDo7i+Pj6O4uAgA="),3,53);
|
||||||
|
else
|
||||||
|
g.drawImage(atob("DAwBEAOAcQ84T8D4HwPyHPCOAcAI"),4,66);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStepsIcon() {
|
||||||
|
if (bangle2)
|
||||||
|
g.drawimage(atob("CQkBBhudzudzgBzsMAA="),3,139);
|
||||||
|
else
|
||||||
|
g.drawImage(atob("DAwBAMAcMeOeeeeeeAecAcOYOAGA"),4,197);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCadenceIcon() {
|
||||||
|
if (bangle2)
|
||||||
|
g.drawImage(atob("CQkBCB4SEBgMBCQ8CAA="),3,139);
|
||||||
|
else
|
||||||
|
g.drawImage(atob("DAwBBAAwP4YxxDwDwDwjjGH8DAAg"),4,197);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStart() {
|
||||||
|
g.clearRect(bangle2?162:226,bangle2?81:113,bangle2?174:238,bangle2?93:125);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStart() {
|
||||||
|
hideStart();
|
||||||
|
g.fillPoly([bangle2?162:226,bangle2?81:113,bangle2?162:226,bangle2?93:125,bangle2?174:238,bangle2?87:119,bangle2?162:226,bangle2?81:113]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPause() {
|
||||||
|
hideStart();
|
||||||
|
g.fillRect(bangle2?165:227,bangle2?82:113,bangle2?167:230,bangle2?92:125);
|
||||||
|
g.fillRect(bangle2?171:234,bangle2?82:113,bangle2?173:237,bangle2?92:125);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStop() {
|
||||||
|
// not applicable to bangle2
|
||||||
|
g.fillRect(226,202,237,213);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawExit() {
|
||||||
|
if (bangle2)
|
||||||
|
g.drawImage(atob("CQkBwfHdx8HB8d3HwQg="),165,82);
|
||||||
|
else
|
||||||
|
g.drawImage(atob("DAwBwD4HcOOcH4DwDwH4OccO4HwD"),226,202);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColours() {
|
||||||
|
g.setBgColor(1-fg,1-fg,1-fg);
|
||||||
|
g.setColor(fg,fg,fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setScreenMode() {
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
g.clearRect(0,24,bangle2?175:239,bangle2?151:215);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doLayout() {
|
||||||
|
setColours();
|
||||||
|
if (!bangle2)
|
||||||
|
drawInvert();
|
||||||
|
drawSatIcon();
|
||||||
|
drawDist();
|
||||||
|
drawTime();
|
||||||
|
if (cfg.pacer == 0)
|
||||||
|
drawPace();
|
||||||
|
else {
|
||||||
|
drawPacer();
|
||||||
|
drawSmallPace();
|
||||||
|
}
|
||||||
|
g.setFont("6x8",bangle2?2:3);
|
||||||
|
if (cfg.show_steps) {
|
||||||
|
drawStepsIcon();
|
||||||
|
g.drawString(steps.toString(),bangle2?15:20,bangle2?134:190,true);
|
||||||
|
} else {
|
||||||
|
drawCadenceIcon();
|
||||||
|
g.drawString(cadence.toString()+" ",bangle2?15:20,bangle2?134:190,true);
|
||||||
|
}
|
||||||
|
drawStart();
|
||||||
|
if (!bangle2)
|
||||||
|
drawStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawDist() {
|
||||||
|
g.setFont("6x8",bangle2?4:5); // 3:5?
|
||||||
|
var dStr = dist.toString();
|
||||||
|
if (dStr.indexOf('.') == -1)
|
||||||
|
dStr += '.0';
|
||||||
|
g.drawString(((' '+(dStr.split('.'))[0])).substr(-2),bangle2?33:53,bangle2?26:35,true);
|
||||||
|
g.fillRect(bangle2?80:112,bangle2?51:66,bangle2?82:115,bangle2?53:69);
|
||||||
|
g.drawString(((dStr.split('.'))[1]+'0').substr(0,2),bangle2?86:120,bangle2?26:35,true);
|
||||||
|
g.setFont("6x8",2);
|
||||||
|
g.drawString(cfg.metric?"K":"M",bangle2?134:180,bangle2?40:56,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPacer() {
|
||||||
|
g.setFont("6x8",bangle2?3:5);
|
||||||
|
var pstr=(pdist>dist?'-':'+')+(Math.floor(Math.abs(dist-pdist)))%100;
|
||||||
|
g.drawString(pstr,bangle2?(49-(pstr.length>2?18:0)):(53-(pstr.length>2?30:0)),bangle2?107:145,true);
|
||||||
|
g.fillRect(bangle2?84:112,bangle2?126:176,bangle2?85:115,bangle2?127:179);
|
||||||
|
g.drawString(('0'+Math.floor(Math.abs(((dist-pdist)*100)%100))).substr(-2),bangle2?89:120,bangle2?107:145,true);
|
||||||
|
g.setFont("6x8",bangle2?1:2);
|
||||||
|
g.drawString(cfg.metric?"K":"M",bangle2?125:180,bangle2?121:166,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPace() {
|
||||||
|
g.setFont("6x8",bangle2?3:5);
|
||||||
|
if (pace > 0 && pace < 6000)
|
||||||
|
g.drawString((' '+Math.floor(pace/60)).substr(-2),bangle2?49:53,bangle2?107:145,true);
|
||||||
|
else
|
||||||
|
g.drawString("--",bangle2?49:53,bangle2?107:145,true);
|
||||||
|
g.fillRect(bangle2?84:112,bangle2?117:160,bangle2?85:115,bangle2?118:163);
|
||||||
|
g.fillRect(bangle2?84:112,bangle2?120:166,bangle2?85:115,bangle2?121:169);
|
||||||
|
if (pace > 0 && pace < 6000)
|
||||||
|
g.drawString(('0'+Math.floor(pace%60)).substr(-2),bangle2?89:120,bangle2?107:145,true);
|
||||||
|
else
|
||||||
|
g.drawString("--",bangle2?89:120,bangle2?107:145,true);
|
||||||
|
g.setFont("6x8",bangle2?1:2);
|
||||||
|
g.drawString(cfg.metric?"/K":"/M",bangle2?124:178,bangle2?121:166,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSmallPace() {
|
||||||
|
g.setFont("6x8",bangle2?2:3);
|
||||||
|
if (pace > 0 && pace < 6000)
|
||||||
|
g.drawString((' '+Math.floor(pace/60)).substr(-2),bangle2?113:136,bangle2?134:190,true);
|
||||||
|
else
|
||||||
|
g.drawString("--",bangle2?113:136,bangle2?134:190,true);
|
||||||
|
if (bangle2) {
|
||||||
|
g.setPixel(136,140);
|
||||||
|
g.setPixel(136,142);
|
||||||
|
} else {
|
||||||
|
g.fillRect(172,199,173,200);
|
||||||
|
g.fillRect(172,203,173,204);
|
||||||
|
}
|
||||||
|
if (pace > 0 && pace < 6000)
|
||||||
|
g.drawString(('0'+Math.floor(pace%60)).substr(-2),bangle2?138:176,bangle2?134:190,true);
|
||||||
|
else
|
||||||
|
g.drawString("--",bangle2?138:176,bangle2?134:190,true);
|
||||||
|
g.setFont("6x8",1);
|
||||||
|
g.drawString(cfg.metric?"/K":"/M",bangle2?164:212,bangle2?141:204,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTime() {
|
||||||
|
var seconds;
|
||||||
|
var minutes;
|
||||||
|
var hours;
|
||||||
|
|
||||||
|
setColours();
|
||||||
|
g.setFont("6x8",bangle2?5:7);
|
||||||
|
seconds = parseInt(elapsed_ms/1000) % 60;
|
||||||
|
minutes = parseInt(elapsed_ms/60000) % 60;
|
||||||
|
hours = parseInt(elapsed_ms/3600000) % 10;
|
||||||
|
g.drawString(hours.toString(),bangle2?6:5,bangle2?63:82,true);
|
||||||
|
g.fillRect(bangle2?34:44,bangle2?79:103,bangle2?36:48,bangle2?81:107);
|
||||||
|
g.fillRect(bangle2?34:44,bangle2?84:112,bangle2?36:48,bangle2?86:116);
|
||||||
|
g.drawString(('0'+minutes).substr(-2),bangle2?40:53,bangle2?63:82,true);
|
||||||
|
g.fillRect(bangle2?98:134,bangle2?79:103,bangle2?100:138,bangle2?81:107);
|
||||||
|
g.fillRect(bangle2?98:134,bangle2?84:112,bangle2?100:138,bangle2?86:116);
|
||||||
|
g.drawString(('0'+seconds).substr(-2),bangle2?104:143,bangle2?63:82,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGPSBox() {
|
||||||
|
g.drawRect(2,26,bangle2?12:17,bangle2?51:63);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGPS() {
|
||||||
|
g.clearRect(3,27,bangle2?11:16,bangle2?50:62);
|
||||||
|
if (gps.satellites > 0) {
|
||||||
|
if (!gps.fix)
|
||||||
|
g.setColor("#FF0000");
|
||||||
|
else if (gps.satellites < 4)
|
||||||
|
g.setColor("#FF5500");
|
||||||
|
else if (gps.satellites < 6)
|
||||||
|
g.setColor("#FF8800");
|
||||||
|
else if (gps.satellites < 8)
|
||||||
|
g.setColor("#FFCC00");
|
||||||
|
else
|
||||||
|
g.setColor("#00FF00");
|
||||||
|
g.fillRect(3,bangle2?50:62,bangle2?11:16,(bangle2?50:62)-(gps.satellites>12?12:gps.satellites)*(bangle2?2:3)+1);
|
||||||
|
g.setColor(fg,fg,fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLap() {
|
||||||
|
show_lap = false;
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
g.clearRect(bangle2?6:5,bangle2?63:82,bangle2?162:224,bangle2?129:182);
|
||||||
|
if (recording) {
|
||||||
|
current_time = Date.now();
|
||||||
|
elapsed_ms = current_time - (start_time + paused_time);
|
||||||
|
} else
|
||||||
|
elapsed_ms = begin_pause - (start_time + paused_time);
|
||||||
|
drawTime();
|
||||||
|
if (cfg.pacer == 0)
|
||||||
|
drawPace();
|
||||||
|
else {
|
||||||
|
drawPacer();
|
||||||
|
drawSmallPace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLap() {
|
||||||
|
g.clearRect(bangle2?6:5,bangle2?63:82,bangle2?162:224,bangle2?129:182);
|
||||||
|
g.drawRect(bangle2?21:28,bangle2?68:90,bangle2?147:201,bangle2?124:174);
|
||||||
|
g.drawRect(bangle2?23:30,bangle2?70:92,bangle2?145:199,bangle2?122:172);
|
||||||
|
g.setFont("6x8",bangle2?1:2);
|
||||||
|
g.drawString("Last lap",bangle2?61:68,bangle2?77:102,true);
|
||||||
|
g.setFont("6x8",bangle2?3:5);
|
||||||
|
if (lap_ms < 600000) {
|
||||||
|
g.drawString((''+Math.floor(lap_ms/60000)),bangle2?57:69,bangle2?89:122,true);
|
||||||
|
g.fillRect(bangle2?74:98,bangle2?99:137,bangle2?75:101,bangle2?100:140);
|
||||||
|
g.fillRect(bangle2?74:98,bangle2?102:143,bangle2?75:101,bangle2?103:146);
|
||||||
|
g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?78:106,bangle2?89:122,true);
|
||||||
|
} else if (lap_ms < 3600000) {
|
||||||
|
g.drawString((''+Math.floor(lap_ms/60000)),bangle2?48:54,bangle2?89:122,true);
|
||||||
|
g.fillRect(bangle2?83:113,bangle2?99:137,bangle2?84:116,bangle2?100:140);
|
||||||
|
g.fillRect(bangle2?83:113,bangle2?102:143,bangle2?84:116,bangle2?103:146);
|
||||||
|
g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?87:121,bangle2?89:122,true);
|
||||||
|
} else {
|
||||||
|
g.drawString((''+Math.floor(lap_ms/3600000)).substr(-1),bangle2?37:35,bangle2?89:122,true);
|
||||||
|
g.fillRect(bangle2?54:64,bangle2?99:137,bangle2?55:67,bangle2?100:140);
|
||||||
|
g.fillRect(bangle2?54:64,bangle2?102:143,bangle2?55:67,bangle2?103:146);
|
||||||
|
g.drawString(('0'+Math.floor((lap_ms%3600000)/60000)).substr(-2),bangle2?58:72,bangle2?89:122,true);
|
||||||
|
g.fillRect(bangle2?93:131,bangle2?99:137,bangle2?94:134,bangle2?100:140);
|
||||||
|
g.fillRect(bangle2?93:131,bangle2?102:143,bangle2?94:134,bangle2?103:146);
|
||||||
|
g.drawString(('0'+Math.floor((lap_ms%60000)/1000)).substr(-2),bangle2?97:139,bangle2?89:122,true);
|
||||||
|
}
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mainLoop() {
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
current_time = Date.now();
|
||||||
|
if (started) {
|
||||||
|
elapsed_ms = current_time - (start_time + paused_time);
|
||||||
|
if (oldDist != dist) {
|
||||||
|
drawDist();
|
||||||
|
if (cfg.lap_idx > 0 && dist >= next_lap ) {
|
||||||
|
show_lap = true;
|
||||||
|
next_lap += parseFloat(laps[cfg.lap_idx]);
|
||||||
|
lap_ms = elapsed_ms - lap_start_ms;
|
||||||
|
lap_start_ms = elapsed_ms;
|
||||||
|
Bangle.buzz();
|
||||||
|
lapID = setTimeout(hideLap,5000);
|
||||||
|
showLap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
elapsed_ms = 0;
|
||||||
|
drawSats = false;
|
||||||
|
if (recording) {
|
||||||
|
if (!show_lap)
|
||||||
|
drawTime();
|
||||||
|
g.setFont("6x8",bangle2?2:3);
|
||||||
|
if (cfg.show_steps)
|
||||||
|
g.drawString(steps.toString(),bangle2?15:20,bangle2?134:190,true);
|
||||||
|
else
|
||||||
|
g.drawString(cadence.toString()+" ",bangle2?15:20,bangle2?134:190,true);
|
||||||
|
} /* else
|
||||||
|
g.setFont("6x8",3); */
|
||||||
|
if (!show_lap)
|
||||||
|
if (cfg.pacer == 0)
|
||||||
|
drawPace();
|
||||||
|
else {
|
||||||
|
drawPacer();
|
||||||
|
drawSmallPace();
|
||||||
|
}
|
||||||
|
if (gps.fix) {
|
||||||
|
if (!started && startHidden) {
|
||||||
|
startHidden = false;
|
||||||
|
if (!bangle2)
|
||||||
|
startID = setWatch(start, BTN2);
|
||||||
|
else
|
||||||
|
startID = setWatch(start, BTN1, {edge: 'falling'});
|
||||||
|
drawStart();
|
||||||
|
}
|
||||||
|
if (!fixed) {
|
||||||
|
fixed = true;
|
||||||
|
drawSats = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!started && !startHidden) {
|
||||||
|
startHidden = true;
|
||||||
|
clearWatch(startID);
|
||||||
|
hideStart();
|
||||||
|
}
|
||||||
|
if (fixed) {
|
||||||
|
fixed = false;
|
||||||
|
drawSats = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gps.satellites != sats) {
|
||||||
|
sats = gps.satellites;
|
||||||
|
drawSats = true;
|
||||||
|
}
|
||||||
|
if (drawSats)
|
||||||
|
drawGPS();
|
||||||
|
if (ctr++%10 == 0) {
|
||||||
|
g.reset();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restart(e) {
|
||||||
|
if (bangle2 && (e.time - e.lastTime > 0.5)) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
paused_time += (Date.now() - begin_pause);
|
||||||
|
pace = 0;
|
||||||
|
drawPause();
|
||||||
|
oldDist = dist;
|
||||||
|
skip_ctr = 0;
|
||||||
|
force_write = true;
|
||||||
|
recording = true;
|
||||||
|
Bangle.buzz();
|
||||||
|
if (!bangle2)
|
||||||
|
setWatch(pause, BTN2);
|
||||||
|
else
|
||||||
|
setWatch(pause, BTN1, { edge: 'falling' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause(e) {
|
||||||
|
if (bangle2 && (e.time - e.lastTime > 0.5)) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
begin_pause = Date.now();
|
||||||
|
elapsed_ms = begin_pause - (start_time + paused_time);
|
||||||
|
finish_ms = elapsed_ms;
|
||||||
|
drawDist();
|
||||||
|
recording = false;
|
||||||
|
if (!show_lap)
|
||||||
|
drawTime();
|
||||||
|
drawStart();
|
||||||
|
if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time))
|
||||||
|
fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n");
|
||||||
|
Bangle.buzz();
|
||||||
|
dists = [];
|
||||||
|
if (!bangle2)
|
||||||
|
setWatch(restart, BTN2);
|
||||||
|
else
|
||||||
|
setWatch(restart, BTN1, { edge: 'falling' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
if (cfg.eco){
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
Bangle.setLCDTimeout(10);
|
||||||
|
}
|
||||||
|
if (cfg.record)
|
||||||
|
fp = require("Storage").open(genFilename(),"w");
|
||||||
|
start_time = Date.now();
|
||||||
|
drawPause();
|
||||||
|
Bangle.buzz();
|
||||||
|
started = true;
|
||||||
|
recording = true;
|
||||||
|
if (!bangle2)
|
||||||
|
setWatch(pause, BTN2);
|
||||||
|
else
|
||||||
|
setWatch(pause, BTN1, { edge: 'falling' });
|
||||||
|
if (cfg.show_steps)
|
||||||
|
Bangle.on('step',countStep);
|
||||||
|
else
|
||||||
|
Bangle.on('step',doCadence);
|
||||||
|
clearInterval(intID);
|
||||||
|
intID = setInterval(mainLoop,200);
|
||||||
|
if (cfg.lap_idx > 0)
|
||||||
|
next_lap = parseFloat(laps[cfg.lap_idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endScreen() {
|
||||||
|
fg = 1-fg;
|
||||||
|
setScreenMode();
|
||||||
|
if (!bangle2)
|
||||||
|
drawInvert();
|
||||||
|
drawExit();
|
||||||
|
g.setFont("6x8",bangle2?1:2);
|
||||||
|
var dStr = dist.toString();
|
||||||
|
if (dStr.indexOf('.') == -1)
|
||||||
|
dStr += '.00';
|
||||||
|
dStr = dStr.slice(0, (dStr.indexOf('.'))+3);
|
||||||
|
if (bangle2)
|
||||||
|
g.drawString('Distance: '+dStr+(cfg.metric?'K':'M'),38,43);
|
||||||
|
else {
|
||||||
|
//g.drawString('Distance: '+dist.toFixed(2),19,53);
|
||||||
|
g.drawString('Distance: '+dStr,19,53);
|
||||||
|
g.setFont("6x8",1);
|
||||||
|
//g.drawString(cfg.metric?'K':'M',139+12*(dist.toFixed(2).length),60);
|
||||||
|
g.drawString(cfg.metric?'K':'M',139+12*(dStr.length),60);
|
||||||
|
g.setFont("6x8",2);
|
||||||
|
}
|
||||||
|
g.drawString('Time: '+parseInt(finish_ms/3600000)%10+':'+('0'+parseInt(finish_ms/60000)%60).substr(-2)+':'+('0'+parseInt(finish_ms/1000)%60).substr(-2),bangle2?62:67,bangle2?63:83);
|
||||||
|
var avgPace = dist?((finish_ms/dist)/1000):0;
|
||||||
|
var paceStr = 'Avg Pace: '+parseInt(avgPace/60)+':'+('0'+parseInt(avgPace%60)).substr(-2);
|
||||||
|
if (bangle2)
|
||||||
|
g.drawString(paceStr+(cfg.metric?'/K':'/M'),38,83);
|
||||||
|
else {
|
||||||
|
g.drawString(paceStr,19,113);
|
||||||
|
g.setFont("6x8",1);
|
||||||
|
g.drawString(cfg.metric?'/K':'/M',19+12*(paceStr.length),120);
|
||||||
|
g.setFont("6x8",2);
|
||||||
|
}
|
||||||
|
g.drawString("Steps: "+steps,bangle2?56:55,bangle2?103:143);
|
||||||
|
var avgCadence = steps?(60*steps/(finish_ms/1000)):0;
|
||||||
|
g.drawString("Cadence: "+parseInt(avgCadence),bangle2?44:31,bangle2?123:173);
|
||||||
|
g.reset();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
if (recording) {
|
||||||
|
finish_ms = elapsed_ms;
|
||||||
|
if (!isNaN(gps.time) && !isNaN(gps.lat) && !isNaN(gps.lon) && !isNaN(gps.alt) && cfg.record && (last_time != gps.time))
|
||||||
|
fp.write([gps.time.getTime(),gps.lat.toFixed(5),gps.lon.toFixed(5),gps.alt].join(",")+"\n");
|
||||||
|
}
|
||||||
|
recording = false;
|
||||||
|
Bangle.setGPSPower(0);
|
||||||
|
Bangle.on('step',function(){});
|
||||||
|
Bangle.on('GPS',function(){});
|
||||||
|
clearInterval(lapID);
|
||||||
|
clearInterval(intID);
|
||||||
|
if (!bangle2) {
|
||||||
|
clearWatch(finID);
|
||||||
|
clearWatch(invID);
|
||||||
|
}
|
||||||
|
if (!bangle2) {
|
||||||
|
setWatch(function() {if (lcd_on) endScreen();}, BTN1, {repeat:true});
|
||||||
|
setWatch(function() {load();},BTN3);
|
||||||
|
} else
|
||||||
|
setWatch(function() {load();},BTN1);
|
||||||
|
fg = 1-fg;
|
||||||
|
endScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startScreen() {
|
||||||
|
clearInterval(intID);
|
||||||
|
if (!bangle2) {
|
||||||
|
clearWatch(invID);
|
||||||
|
}
|
||||||
|
clearWatch(finID);
|
||||||
|
setScreenMode();
|
||||||
|
doLayout();
|
||||||
|
drawGPSBox();
|
||||||
|
Bangle.buzz();
|
||||||
|
if (!bangle2) {
|
||||||
|
invID = setWatch(invertRunning, BTN1, {repeat:true});
|
||||||
|
startID = setWatch(start, BTN2);
|
||||||
|
finID = setWatch(finish, BTN3);
|
||||||
|
} else
|
||||||
|
startID = setWatch(start, BTN1, {edge: 'falling'});
|
||||||
|
fixed = false;
|
||||||
|
intID = setInterval(mainLoop,1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invertRunning() {
|
||||||
|
// not applicable to bangle2
|
||||||
|
if (!lcd_on)
|
||||||
|
return;
|
||||||
|
fg = 1-fg;
|
||||||
|
setScreenMode();
|
||||||
|
if (started)
|
||||||
|
if (recording) {
|
||||||
|
current_time = Date.now();
|
||||||
|
elapsed_ms = current_time - (start_time + paused_time);
|
||||||
|
} else
|
||||||
|
elapsed_ms = begin_pause - (start_time + paused_time);
|
||||||
|
else
|
||||||
|
elapsed_ms = 0;
|
||||||
|
drawInvert();
|
||||||
|
drawSatIcon();
|
||||||
|
drawGPSBox();
|
||||||
|
drawGPS();
|
||||||
|
drawDist();
|
||||||
|
if (show_lap)
|
||||||
|
showLap();
|
||||||
|
else {
|
||||||
|
drawTime();
|
||||||
|
if (cfg.pacer == 0)
|
||||||
|
drawPace();
|
||||||
|
else {
|
||||||
|
drawPacer();
|
||||||
|
drawSmallPace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.setFont("6x8",3);
|
||||||
|
if (cfg.show_steps) {
|
||||||
|
drawStepsIcon();
|
||||||
|
g.drawString(steps.toString(),20,190,true);
|
||||||
|
} else {
|
||||||
|
drawCadenceIcon();
|
||||||
|
g.drawString(cadence.toString()+" ",20,190,true);
|
||||||
|
}
|
||||||
|
if (recording)
|
||||||
|
drawPause();
|
||||||
|
else if (started || gps.fix)
|
||||||
|
drawStart();
|
||||||
|
drawStop();
|
||||||
|
g.reset();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawDots() {
|
||||||
|
if (ctr % 4 == 0)
|
||||||
|
g.drawString(" ",bangle2?116:176,bangle2?90:108,true);
|
||||||
|
else if (ctr % 4 == 1)
|
||||||
|
g.drawString(".",bangle2?116:176,bangle2?90:108);
|
||||||
|
else if (ctr % 4 == 2)
|
||||||
|
g.drawString("..",bangle2?116:176,bangle2?90:108);
|
||||||
|
else
|
||||||
|
g.drawString("...",bangle2?116:176,bangle2?90:108);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invertWaiting() {
|
||||||
|
/* not applicable to bangle2 */
|
||||||
|
fg = 1-fg;
|
||||||
|
setScreenMode();
|
||||||
|
drawInvert();
|
||||||
|
drawExit();
|
||||||
|
g.setFont("6x8",2);
|
||||||
|
g.drawString("Locating",68,88);
|
||||||
|
g.drawString("Satellites",56,108);
|
||||||
|
ctr--;
|
||||||
|
drawDots();
|
||||||
|
g.reset();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
ctr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function awaitGPSLoop() {
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
g.setFont("6x8",bangle2?1:2);
|
||||||
|
drawDots();
|
||||||
|
if (gps.fix)
|
||||||
|
startScreen();
|
||||||
|
if (ctr % 10 == 0) {
|
||||||
|
g.reset();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
||||||
|
ctr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function awaitGPS() {
|
||||||
|
Bangle.setOptions({wakeOnTwist:false});
|
||||||
|
Bangle.setGPSPower(1);
|
||||||
|
Bangle.on('GPS', saveGPS);
|
||||||
|
Bangle.setLCDPower(true);
|
||||||
|
Bangle.setLCDTimeout(0);
|
||||||
|
g.reset();
|
||||||
|
setColours();
|
||||||
|
if (!bangle2) {
|
||||||
|
drawInvert();
|
||||||
|
invID = setWatch(invertWaiting, BTN1, {repeat:true});
|
||||||
|
}
|
||||||
|
drawExit();
|
||||||
|
g.setFont("6x8",bangle2?1:2);
|
||||||
|
// g.drawString("Locating",bangle2?36:68,bangle2?72:88);
|
||||||
|
// g.drawString("Satellites",bangle2?24:56,bangle2?92:108);
|
||||||
|
g.drawString("Locating",bangle2?62:68,bangle2?78:88);
|
||||||
|
g.drawString("Satellites",56,bangle2?90:108);
|
||||||
|
intID = setInterval(awaitGPSLoop,1000);
|
||||||
|
if (bangle2)
|
||||||
|
finID = setWatch(function(){load();},BTN1);
|
||||||
|
else
|
||||||
|
finID = setWatch(function(){load();},BTN3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
require("Storage").write("pacer.json", cfg);
|
||||||
|
E.showMenu();
|
||||||
|
if (cfg.eco)
|
||||||
|
Bangle.on('lcdPower', function(on) {setTimeout(function(){lcd_on = on;}, 500);});
|
||||||
|
fg = cfg.dark?1:0;
|
||||||
|
R = cfg.metric?6371:3959;
|
||||||
|
ppace = 295 + 5 * cfg.pacer;
|
||||||
|
setScreenMode();
|
||||||
|
awaitGPS();
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults();
|
||||||
|
|
||||||
|
var main_menu = {
|
||||||
|
"" : { "title" : "Pacer"},
|
||||||
|
"Start": function() { main(); },
|
||||||
|
"Recording" : {
|
||||||
|
value : cfg.record,
|
||||||
|
format : v => v?"On":"Off",
|
||||||
|
onchange : v => { cfg.record = v; },
|
||||||
|
},
|
||||||
|
"Units" : {
|
||||||
|
value : cfg.metric,
|
||||||
|
format : v => v?"Metric":"Imperial",
|
||||||
|
onchange : v => { cfg.metric = v; },
|
||||||
|
},
|
||||||
|
"Lap" : {
|
||||||
|
value : cfg.lap_idx,
|
||||||
|
format : v => laps[v],
|
||||||
|
min : 0, max : 6,
|
||||||
|
onchange : v => { cfg.lap_idx = v; }
|
||||||
|
},
|
||||||
|
"Dark mode" : {
|
||||||
|
value : cfg.dark,
|
||||||
|
format : v => v?"On":"Off",
|
||||||
|
onchange : v => { cfg.dark = v; },
|
||||||
|
},
|
||||||
|
"Eco battery" : {
|
||||||
|
value : cfg.eco,
|
||||||
|
format : v => v?"On":"Off",
|
||||||
|
onchange : v => { cfg.eco = v; },
|
||||||
|
},
|
||||||
|
"Eco storage" : {
|
||||||
|
value : cfg.storage,
|
||||||
|
format : v => v?"On":"Off",
|
||||||
|
onchange : v => { cfg.storage = v; },
|
||||||
|
},
|
||||||
|
"Steps" : {
|
||||||
|
value : cfg.show_steps,
|
||||||
|
format : v => v?"Count":"Cadence",
|
||||||
|
onchange : v => { cfg.show_steps = v; },
|
||||||
|
},
|
||||||
|
"Pacer" : {
|
||||||
|
value : cfg.pacer,
|
||||||
|
format : v => v==0?"Off":pace_str(v),
|
||||||
|
min : 0, max : 121,
|
||||||
|
onchange : v => { cfg.pacer = v; }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!bangle2) {
|
||||||
|
Bangle.setLCDMode();
|
||||||
|
}
|
||||||
|
Bangle.setLCDBrightness(1);
|
||||||
|
setScreenMode();
|
||||||
|
E.showMenu(main_menu);
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,198 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="tracks"></div>
|
||||||
|
<div class="container" id="toastcontainer" style="position:fixed; bottom:8px; left:0px; right:0px; z-index: 100;"></div>
|
||||||
|
|
||||||
|
<script src="../../core/lib/interface.js"></script>
|
||||||
|
<script src="../../core/js/ui.js"></script>
|
||||||
|
<script src="../../core/js/utils.js"></script>
|
||||||
|
<script>
|
||||||
|
var domTracks = document.getElementById("tracks");
|
||||||
|
var fileCache = new Map();
|
||||||
|
|
||||||
|
function saveGPX(track, title) {
|
||||||
|
// Output GPX
|
||||||
|
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx creator="Bangle.js" version="1.1">
|
||||||
|
<metadata>
|
||||||
|
<time>${isoTime(track[0][0])}</time>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>${title}</name>
|
||||||
|
<trkseg>`;
|
||||||
|
track.forEach(pt=>{
|
||||||
|
gpx += `
|
||||||
|
<trkpt lat="${pt[1]}" lon="${pt[2]}">
|
||||||
|
<ele>${pt[3]}</ele>
|
||||||
|
<time>${isoTime(pt[0])}</time>
|
||||||
|
</trkpt>`;
|
||||||
|
|
||||||
|
});
|
||||||
|
gpx += `
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>`;
|
||||||
|
Util.saveFile(title+".gpx", "application/gpx+xml", gpx);
|
||||||
|
showToast("Download finished.", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackLineToObject(l) {
|
||||||
|
if (l===undefined) return {};
|
||||||
|
return l.trim().split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isoTime(t) {
|
||||||
|
var td = new Date(Number(t));
|
||||||
|
var ti = td.toISOString();
|
||||||
|
return ti;
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTrack(filename, callback) {
|
||||||
|
function onData(data) {
|
||||||
|
var lines = data.trim().split("\n");
|
||||||
|
var headers = lines.shift().split(",");
|
||||||
|
var track = lines.map(l=>trackLineToObject(l));
|
||||||
|
callback(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = fileCache.get(filename);
|
||||||
|
if (data) {
|
||||||
|
onData(data);
|
||||||
|
} else {
|
||||||
|
Util.showModal(`Downloading ${filename}...`);
|
||||||
|
Util.readStorageFile(filename, data => {
|
||||||
|
fileCache.set(filename, data);
|
||||||
|
onData(data);
|
||||||
|
Util.hideModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAll(trackList, cb) {
|
||||||
|
const tracks = trackList.slice();
|
||||||
|
|
||||||
|
const downloadOne = () => {
|
||||||
|
const track = tracks.pop();
|
||||||
|
if(!track) {
|
||||||
|
showToast("Finished downloading all.", "success");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadTrack(
|
||||||
|
track.filename,
|
||||||
|
lines => {
|
||||||
|
cb(lines, `Bangle.js Track ${track.number}`);
|
||||||
|
downloadOne();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
downloadOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackList() {
|
||||||
|
Util.showModal("Loading Track List...");
|
||||||
|
domTracks.innerHTML = "";
|
||||||
|
Puck.eval(`require("Storage").list(/^\\.pacer.*\\.csv$/,{sf:1})`,files=>{
|
||||||
|
var trackList = [];
|
||||||
|
var promise = Promise.resolve();
|
||||||
|
files.forEach(filename => {
|
||||||
|
promise = promise.then(()=>new Promise(resolve => {
|
||||||
|
var trackNo = filename.match(/^\.pacer(.*)\.csv$/)[1];
|
||||||
|
Util.showModal(`Loading Track ${trackNo}...`);
|
||||||
|
Puck.eval(`(${JSON.stringify(filename)})`, trackInfo=>{
|
||||||
|
console.log(filename," => ",trackInfo);
|
||||||
|
trackList.push({
|
||||||
|
filename : filename,
|
||||||
|
number : trackNo,
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
// ================================================
|
||||||
|
// When 'promise' completes we now have all the info in trackList
|
||||||
|
promise.then(() => {
|
||||||
|
var html = `
|
||||||
|
<div class="container">
|
||||||
|
<h2>Tracks</h2>
|
||||||
|
<div class="columns">\n`;
|
||||||
|
trackList.forEach(track => {
|
||||||
|
console.log("track", track);
|
||||||
|
const trackDate = new Date (track.number.slice(0,4) + '-' + track.number.slice(4,6) + '-' + track.number.slice (6,8))
|
||||||
|
const trackFullDate = trackDate.toDateString() + ' ' + track.number.slice(8,10) + ':' + track.number.slice(10,12) + ':' + track.number.slice (12,14)
|
||||||
|
html += `
|
||||||
|
<div class="column col-12">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title h5">
|
||||||
|
${trackFullDate}
|
||||||
|
<button class="btn btn-primary" filename="${track.filename}" trackid="${track.number}" task="downloadgpx">Download GPX</button>
|
||||||
|
<button class="btn btn-default" filename="${track.filename}" trackid="${track.number}" task="delete">Delete</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-subtitle">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
if (trackList.length==0) {
|
||||||
|
html += `
|
||||||
|
<div class="column col-12">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title h5">No tracks</div>
|
||||||
|
<div class="card-subtitle text-gray">No GPS tracks found</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
html += `
|
||||||
|
</div><!-- columns -->
|
||||||
|
<h2>Batch</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary" task="downloadgpx_all">Download all GPX</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
domTracks.innerHTML = html;
|
||||||
|
Util.hideModal();
|
||||||
|
var buttons = domTracks.querySelectorAll("button");
|
||||||
|
for (var i=0;i<buttons.length;i++) {
|
||||||
|
buttons[i].addEventListener("click",event => {
|
||||||
|
var button = event.currentTarget;
|
||||||
|
var filename = button.getAttribute("filename");
|
||||||
|
var trackid = parseInt(button.getAttribute("trackid"));
|
||||||
|
var task = button.getAttribute("task");
|
||||||
|
|
||||||
|
if (!/_all$/.test(task) && (!filename || trackid===undefined)) return;
|
||||||
|
|
||||||
|
switch(task) {
|
||||||
|
case "delete":
|
||||||
|
Util.showModal(`Deleting ${filename}...`);
|
||||||
|
Util.eraseStorageFile(filename,()=>{
|
||||||
|
Util.hideModal();
|
||||||
|
getTrackList();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "downloadgpx":
|
||||||
|
downloadTrack(filename, track => saveGPX(track, `Bangle.js Track ${trackid}`));
|
||||||
|
break;
|
||||||
|
case "downloadgpx_all":
|
||||||
|
downloadAll(trackList, saveGPX);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
getTrackList();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"id": "pacer",
|
||||||
|
"name": "Pacer",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Run with a virtual partner",
|
||||||
|
"icon": "app.png",
|
||||||
|
"tags": "run,running,fitness,outdoors,gps",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"interface": "interface.html",
|
||||||
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
|
"storage": [
|
||||||
|
{"name":"pacer.app.js","url":"app.js"},
|
||||||
|
{"name":"pacer.img","url":"app-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
{"name":"pacer.json"},
|
||||||
|
{"wildcard":".pacer*.csv","storageFile":true}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Loading…
Reference in New Issue