forked from FOSS/BangleApps
Add GPS Recorder with companion code in Bangle App Loader that allows the data to be read back as a KML
parent
2bca5d4747
commit
a703e15455
79
README.md
79
README.md
|
@ -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.
|
||||
|
|
|
@ -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:[
|
||||
|
|
|
@ -109,7 +109,3 @@ function viewTrack(n) {
|
|||
}
|
||||
|
||||
showMainMenu();
|
||||
|
||||
|
||||
// f = require("Storage").open(".gpsrc"+n,"r");
|
||||
// f.readLine()...
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:[
|
||||
|
|
42
index.js
42
index.js
|
@ -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}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
Loading…
Reference in New Issue