1
0
Fork 0

Add GPS Recorder with companion code in Bangle App Loader that allows the data to be read back as a KML

master
Gordon Williams 2020-02-10 13:49:36 +00:00
parent 2bca5d4747
commit a703e15455
9 changed files with 358 additions and 54 deletions

View File

@ -192,10 +192,12 @@ about the app.
"custom": "custom.html", // if supplied, apps/custom.html is loaded in an
// iframe, and it must post back an 'app' structure
// like this one with 'storage','name' and 'id' set up
// see below for more info
"interface": "interface.html", // if supplied, apps/interface.html is loaded in an
// iframe, and it may interact with the connected Bangle
// to retrieve information from it
// see below for more info
"allow_emulator":true, // if 'app.js' will run in the emulator, set to true to
// add an icon to allow your app to be tested
@ -218,6 +220,83 @@ about the app.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
* storage is used to identify the app files and how to handle them
### `apps.json`: `custom` element
Apps that can be customised need to define a `custom` element in `apps.json`,
which names an HTML file in that app's folder.
When `custom` is defined, the 'upload' button is replaced by a customize
button, and when clicked it opens the HTML page specified in an iframe.
In that HTML file you're then responsible for handling a button
press and calling `sendCustomizedApp` with your own customised
version of what's in `apps.json`:
```
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p><button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../lib/customize.js"></script>
<script>
document.getElementById("upload").addEventListener("click", function() {
sendCustomizedApp({
id : "7chname",
storage:[
{name:"-7chname", content:app_source_code},
{name:"+7chname", content:JSON.stringify({
name:"My app's name",
icon:"*7chname",
src:"-7chname"
})},
{name:"*7chname", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true},
]
});
});
</script>
</body>
</html>
```
This'll then be loaded in to the watch. See [apps/qrcode/grcode.html](the QR Code app)
for a clean example.
### `apps.json`: `interface` element
Apps that create data that can be read back can define a `interface` element in `apps.json`,
which names an HTML file in that app's folder.
When `interface` is defined, a `Download from App` button is added to
the app's description, and when clicked it opens the HTML page specified
in an iframe.
```
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<script src="../../lib/interface.js"></script>
<div id="t">Loading...</div>
<script>
function onInit() {
Puck.eval("E.getTemperature()", temp=> {
document.getElementById("t").innerHTML = temp;
});
}
</script>
</body>
</html>
```
When the page is ready a function called `onInit` is called,
and in that you can call `Puck.write` and `Puck.eval` to get
the data you require from Bangle.js.
See [apps/gpsrec/interface.html](the GPS Recorder) for a full example.
## Coding hints
- Need to save state? Use the `E.on('kill',...)` event to save JSON to a file called `@7chname`, then load it at startup.

View File

@ -33,6 +33,7 @@
<p>If ok, Click <button id="upload" class="btn btn-primary">Upload</button></p>
</div>
<script src="../../lib/customize.js"></script>
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://unpkg.com/osmtogeojson@2.2.12/osmtogeojson.js"></script>
@ -202,8 +203,7 @@ g.clear();`;
});
var icon = `require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))`;
window.postMessage({
sendCustomizedApp({
id : "beer",
storage:[

View File

@ -109,7 +109,3 @@ function viewTrack(n) {
}
showMainMenu();
// f = require("Storage").open(".gpsrc"+n,"r");
// f.readLine()...

View File

@ -3,34 +3,202 @@
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="tracks"></div>
<p>Hello!</p>
<div class="modal active" id="status-modal">
<div class="modal-overlay"></div>
<div class="modal-container">
<div class="modal-header">
<div class="modal-title h5">Please wait</div>
</div>
<div class="modal-body">
<div class="content">
Loading...
</div>
</div>
</div>
</div>
<script src="../../lib/interface.js"></script>
<script>
var __id = 0, __idlookup = [];
var Puck = {
eval : function(data,callback) {
__id++;
__idlookup[__id] = callback;
window.postMessage({type:"eval",data:data,id:__id});
},write : function(data,callback) {
__id++;
__idlookup[__id] = callback;
window.postMessage({type:"write",data:data,id:__id});
}
};
window.addEventListener("message", function(event) {
var msg = event.data;
if (msg.type=="evalrsp" || msg.type=="writersp") {
var cb = __idlookup[msg.id];
delete __idlookup[msg.id];
cb(msg.data);
}
}, false);
var domTracks = document.getElementById("tracks");
var domModal = document.getElementById("status-modal");
Puck.eval("E.getTemperature()",function(d) {
console.log("GOT: "+d);
});
function showModal(title) {
domModal.querySelector(".content").innerHTML = title;
domModal.classList.add("active");
}
function hideModal(title) {
domModal.classList.remove("active");
}
function saveKML(track,title) {
var kml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Placemark>
<name>${title}</name>
<LineString>
<coordinates>
${track.map(pt=>[pt.lon, pt.lat, pt.alt].join(",")).join(" \n")}
</coordinates>
</LineString>
</Placemark>
</Document>
</kml>`;
var a = document.createElement("a"),
file = new Blob([kml], {type: "application/vnd.google-earth.kml+xml"});
var url = URL.createObjectURL(file);
a.href = url;
a.download = title+".kml";
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
function saveGPX(track, title) {
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="Bangle.js" version="1.1">
<metadata>
<time>${track[0].date.toISOString()}</time>
</metadata>
<trk>
<name>${title}</name>
<trkseg>`;
track.forEach(pt=>{
gpx += `
<trkpt lat="${pt.lat}" lon="${pt.lon}">
<ele>${pt.alt}</ele>
<time>${pt.date.toISOString()}</time>
</trkpt>`;
});
gpx += `
</trkseg>
</trk>
</gpx>`;
var a = document.createElement("a"),
file = new Blob([gpx], {type: "application/gpx+xml"});
var url = URL.createObjectURL(file);
a.href = url;
a.download = title+".gpx";
document.body.appendChild(a);
a.click();
setTimeout(function() {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 0);
}
function trackLineToObject(l, hasTrackNumber) {
var t = l.trim().split(",");
var n = hasTrackNumber ? 1 : 0;
var o = {
date : new Date(parseInt(t[n+0])),
lat : parseFloat(t[n+1]),
lon : parseFloat(t[n+2]),
alt : parseFloat(t[n+3])
};
if (hasTrackNumber)
o.number = t[0];
return o;
}
function downloadTrack(trackid, callback) {
showModal("Downloading Track...");
Puck.write(`\x10(function() {
var f = require("Storage").open(".gpsrc${trackid.toString(36)}","r");
var l = f.readLine();
while (l!==undefined) { Bluetooth.print(l); l = f.readLine(); }
})()\n`,tracklist=>{
hideModal();
var track = tracklist.trim().split("\n").map(l=>trackLineToObject(l,false));
callback(track);
});
}
function getTrackList() {
showModal("Loading Tracks...");
domTracks.innerHTML = "";
Puck.write(`\x10(function() {
for (var n=0;n<36;n++) {
var f = require("Storage").open(".gpsrc"+n.toString(36),"r");
var l = f.readLine();
if (l!==undefined)
Bluetooth.println(n+","+l.trim());
}
})()\n`,tracklist=>{
var trackLines = tracklist.trim().split("\n");
var html = `<div class="container">
<div class="columns">\n`;
trackLines.forEach(l => {
var track = trackLineToObject(l, true /*has track number*/);
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">${track.date.toString().substr(0,24)}</div>
</div>
<div class="card-image">
<iframe
width="100%"
height="250"
frameborder="0" style="border:0"
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4&q=${track.lat},${track.lon}&zoom=10" allowfullscreen>
</iframe>
</div>
<div class="card-body"></div>
<div class="card-footer">
<button class="btn btn-primary" trackid="${track.number}" task="downloadkml">Download KML</button>
<button class="btn btn-primary" trackid="${track.number}" task="downloadgpx">Download GPX</button>
<button class="btn btn-default" trackid="${track.number}" task="delete">Delete</button>
</div>
</div>
`;
});
if (trackLines.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>
</div>`;
domTracks.innerHTML = html;
hideModal();
var buttons = domTracks.querySelectorAll("button");
for (var i=0;i<buttons.length;i++) {
buttons[i].addEventListener("click",event => {
var button = event.currentTarget;
var trackid = button.getAttribute("trackid");
var task = button.getAttribute("task");
if (task=="delete") {
showModal("Deleting Track...");
Puck.write(`\x10require("Storage").open(".gpsrc${trackid.toString(36)}","r").erase()\n`,()=>{
hideModal();
getTrackList();
});
}
if (task=="downloadkml") {
downloadTrack(trackid, track => saveKML(track, `Bangle.js Track ${trackid}`));
}
if (task=="downloadgpx") {
downloadTrack(trackid, track => saveGPX(track, `Bangle.js Track ${trackid}`));
}
});
}
})
}
function onInit() {
getTrackList();
}
</script>
</body>

View File

@ -8,7 +8,7 @@
<p>Try your QR Code: <div id="qrcode"></div></p>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../lib/customize.js"></script>
<script src="../../lib/qrcode.min.js"></script><!-- https://davidshimjs.github.io/qrcodejs/ -->
<script src="https://espruino.github.io/EspruinoWebTools/heatshrink.js"></script>
<script src="https://espruino.github.io/EspruinoWebTools/imageconverter.js"></script>
@ -41,15 +41,13 @@ g.setColor(0,0,0);
g.drawString(url,120,230);
g.setColor(1,1,1);
`;
console.log(app);
var json = JSON.stringify({
name:"QR Code",
icon:"*qrcode",
src:"-qrcode"
});
var icon = `require("heatshrink").decompress(atob("mEwgP/AEX8gE8nkAn4FSngCWF6xfYDgIABHAQFPDQXD4YgDApxNDMooFOAQIdDAqIvWfcYA="))`;
window.postMessage({
sendCustomizedApp({
id : "qrcode",
storage:[
@ -58,6 +56,7 @@ g.setColor(1,1,1);
{name:"*qrcode", content:icon, evaluate:true},
]
});
});
</script>

View File

@ -25,6 +25,7 @@
<input class="form-input" type="file" id="fileLoader"/></p>
<p><button id="upload" style="display:none" class="btn btn-primary">Upload</button></p>
<pre id="log"></pre>
<script src="../../lib/customize.js"></script>
<script>
var xmlText = "";
var xmlDoc;
@ -246,7 +247,7 @@ src:"-route"
});
var icon = `require("heatshrink").decompress(atob("mEwgIkhvgFE/wEDgOHAocDgYFEgOAAp4XEEYsB4w1E5hBKnByFKw8/AQNAAQP/4EAAIMB4HggBABHoNwCwUGE4kOgEYBAMAhk+hgIBAoM/hkEAoMIv8MC4QFChARCAoIMCDoQXChkcjA1EAoJBBg5dCJoJHDKYWAsCGD4AJBAAXBDYIlCsYFBGwUzPok+AokcsOOmIUCAogAWA=="))`;
window.postMessage({
sendCustomizedApp({
id : "route",
storage:[

View File

@ -149,27 +149,31 @@ function handleAppInterface(app) {
});
});
var iframe = modal.getElementsByTagName("iframe")[0];
var iwin = iframe.contentWindow;
iwin.addEventListener("message", function(event) {
var msg = event.data;
if (msg.type=="eval") {
Puck.eval(msg.data, function(result) {
iwin.postMessage({
type : "evalrsp",
data : result,
id : msg.id
iframe.onload = function() {
var iwin = iframe.contentWindow;
iwin.addEventListener("message", function(event) {
var msg = event.data;
if (msg.type=="eval") {
Puck.eval(msg.data, function(result) {
iwin.postMessage({
type : "evalrsp",
data : result,
id : msg.id
});
});
});
} else if (msg.type=="write") {
Puck.write(msg.data, function() {
iwin.postMessage({
type : "writersp",
id : msg.id
} else if (msg.type=="write") {
Puck.write(msg.data, function(result) {
iwin.postMessage({
type : "writersp",
data : result,
id : msg.id
});
});
});
}
}, false);
iframe.src = `apps/${app.id}/${app.interface}`
}
}, false);
iwin.postMessage({type:"init"});
};
iframe.src = `apps/${app.id}/${app.interface}`;
});
}

25
lib/customize.js Normal file
View File

@ -0,0 +1,25 @@
/* Library for 'custom' HTML files that are to
be used from within BangleApps
See: README.md / `apps.json`: `custom` element
*/
/* Call with a JS object:
sendCustomizedApp({
id : "7chname",
storage:[
{name:"-7chname", content:app_source_code},
{name:"+7chname", content:JSON.stringify({
name:"My app's name",
icon:"*7chname",
src:"-7chname"
})},
{name:"*7chname", content:'require("heatshrink").decompress(atob("mEwg...4"))', evaluate:true},
]
});
*/
function sendCustomizedApp(app) {
window.postMessage(app);
}

32
lib/interface.js Normal file
View File

@ -0,0 +1,32 @@
/* Library for 'interface' HTML files that are to
be used from within BangleApps
See: README.md / `apps.json`: `interface` element
This exposes a 'Puck' object like the puck.js library,
and calls `onInit` when it's ready. `Puck` can be used
for sending/receiving data to the correctly connected
device with Puck.eval/write.
*/
var __id = 0, __idlookup = [];
var Puck = {
eval : function(data,callback) {
__id++;
__idlookup[__id] = callback;
window.postMessage({type:"eval",data:data,id:__id});
},write : function(data,callback) {
__id++;
__idlookup[__id] = callback;
window.postMessage({type:"write",data:data,id:__id});
}
};
window.addEventListener("message", function(event) {
var msg = event.data;
if (msg.type=="init") {
onInit();
} else if (msg.type=="evalrsp" || msg.type=="writersp") {
var cb = __idlookup[msg.id];
delete __idlookup[msg.id];
cb(msg.data);
}
}, false);