Add files via upload
parent
9425a3eef4
commit
d1b344dbf7
|
|
@ -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,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,19 @@
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue