63
apps.json
|
@ -80,7 +80,7 @@
|
||||||
"name": "Notifications (default)",
|
"name": "Notifications (default)",
|
||||||
"shortName":"Notifications",
|
"shortName":"Notifications",
|
||||||
"icon": "notify.png",
|
"icon": "notify.png",
|
||||||
"version":"0.06",
|
"version":"0.07",
|
||||||
"description": "A handler for displaying notifications that displays them in a bar at the top of the screen",
|
"description": "A handler for displaying notifications that displays them in a bar at the top of the screen",
|
||||||
"tags": "widget",
|
"tags": "widget",
|
||||||
"type": "notify",
|
"type": "notify",
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
{ "id": "gbridge",
|
{ "id": "gbridge",
|
||||||
"name": "Gadgetbridge",
|
"name": "Gadgetbridge",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"version":"0.20",
|
"version":"0.21",
|
||||||
"description": "The default notification handler for Gadgetbridge notifications from Android",
|
"description": "The default notification handler for Gadgetbridge notifications from Android",
|
||||||
"tags": "tool,system,android,widget",
|
"tags": "tool,system,android,widget",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
|
@ -584,7 +584,7 @@
|
||||||
{ "id": "hrm",
|
{ "id": "hrm",
|
||||||
"name": "Heart Rate Monitor",
|
"name": "Heart Rate Monitor",
|
||||||
"icon": "heartrate.png",
|
"icon": "heartrate.png",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "Measure your heart rate and see live sensor data",
|
"description": "Measure your heart rate and see live sensor data",
|
||||||
"tags": "health",
|
"tags": "health",
|
||||||
"storage": [
|
"storage": [
|
||||||
|
@ -1590,7 +1590,7 @@
|
||||||
"name": "BangleRun",
|
"name": "BangleRun",
|
||||||
"shortName": "BangleRun",
|
"shortName": "BangleRun",
|
||||||
"icon": "banglerun.png",
|
"icon": "banglerun.png",
|
||||||
"version": "0.09",
|
"version": "0.10",
|
||||||
"interface": "interface.html",
|
"interface": "interface.html",
|
||||||
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
||||||
"tags": "run,running,fitness,outdoors",
|
"tags": "run,running,fitness,outdoors",
|
||||||
|
@ -2822,8 +2822,8 @@
|
||||||
"name": "Walkers Clock",
|
"name": "Walkers Clock",
|
||||||
"shortName":"Walkers Clock",
|
"shortName":"Walkers Clock",
|
||||||
"icon": "walkersclock48.png",
|
"icon": "walkersclock48.png",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "A larg font watch, displays steps, can switch GPS on/off, displays grid reference",
|
"description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference",
|
||||||
"type":"clock",
|
"type":"clock",
|
||||||
"tags": "clock, gps, tools, outdoors",
|
"tags": "clock, gps, tools, outdoors",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
|
@ -2856,6 +2856,18 @@
|
||||||
{"name":"widhrt.wid.js","url":"widget.js"}
|
{"name":"widhrt.wid.js","url":"widget.js"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{ "id": "countdowntimer",
|
||||||
|
"name" : "Countdown Timer",
|
||||||
|
"icon": "countdowntimer.png",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "A simple countdown timer with a focus on usability",
|
||||||
|
"tags": "timer, tool",
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name": "countdowntimer.app.js", "url": "countdowntimer.js"},
|
||||||
|
{"name": "countdowntimer.img", "url": "countdowntimer-icon.js", "evaluate": true}
|
||||||
|
]
|
||||||
|
},
|
||||||
{ "id": "helloworld",
|
{ "id": "helloworld",
|
||||||
"name": "hello,world!",
|
"name": "hello,world!",
|
||||||
"shortName":"helloworld",
|
"shortName":"helloworld",
|
||||||
|
@ -2868,5 +2880,44 @@
|
||||||
{"name":"helloworld.app.js","url":"app.js"},
|
{"name":"helloworld.app.js","url":"app.js"},
|
||||||
{"name":"helloworld.img","url":"app-icon.js","evaluate":true}
|
{"name":"helloworld.img","url":"app-icon.js","evaluate":true}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{ "id": "widcom",
|
||||||
|
"name": "Compass Widget",
|
||||||
|
"icon": "widget.png",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Tiny widget to show the power on/off status of the Compass. Requires firmware v2.08.167 or later",
|
||||||
|
"tags": "widget, compass",
|
||||||
|
"type":"widget",
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"widcom.wid.js","url":"widget.js"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "arrow",
|
||||||
|
"name": "Arrow Compass",
|
||||||
|
"icon": "arrow.png",
|
||||||
|
"type":"app",
|
||||||
|
"version":"0.02",
|
||||||
|
"description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass",
|
||||||
|
"tags": "tool,outdoors",
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"arrow.app.js","url":"app.js"},
|
||||||
|
{"name":"arrow.img","url":"icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "id": "waypointer",
|
||||||
|
"name": "Way Pointer",
|
||||||
|
"icon": "waypointer.png",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Navigate to a waypoint using the GPS for bearing and compass to point way, uses the same waypoint interface as GPS Navigation",
|
||||||
|
"tags": "tool,outdoors,gps",
|
||||||
|
"readme": "README.md",
|
||||||
|
"interface":"waypoints.html",
|
||||||
|
"storage": [
|
||||||
|
{"name":"waypointer.app.js","url":"app.js"},
|
||||||
|
{"name":"waypoints.json","url":"waypoints.json","evaluate":false},
|
||||||
|
{"name":"waypointer.img","url":"icon.js","evaluate":true}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Arrow Compass
|
||||||
|
|
||||||
|
A variation of jeffmer's Navigation Compass. The compass points
|
||||||
|
North and shows the current heading.
|
||||||
|
|
||||||
|
This is a tilt and roll compensated compass with a linear
|
||||||
|
display. The compass will display the same direction that it shows
|
||||||
|
when flat as when it is tilted (rotation around the W-S axis) or
|
||||||
|
rolled (rotation around the N-S) axis. *Even with compensation, it
|
||||||
|
would be beyond foolish to rely solely on this app for any serious
|
||||||
|
navigational purpose.*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Calibration
|
||||||
|
|
||||||
|
Correct operation of this app depends critically on calibration. When
|
||||||
|
first run on a Bangle, the app will request calibration. This lasts
|
||||||
|
for 30 seconds during which you should move the watch slowly through
|
||||||
|
figures of 8. It is important that during calibration the watch is
|
||||||
|
fully rotated around each of it axes. If the app does give the
|
||||||
|
correct direction heading or is not stable with respect to tilt and
|
||||||
|
roll - redo the calibration by pressing *BTN3*. Calibration data is
|
||||||
|
recorded in a storage file named `magnav.json`.
|
||||||
|
|
||||||
|
It is also worth noting that the presence of the magnetic charging
|
||||||
|
clamps will require the compass to be recalibrated after every
|
||||||
|
charge.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
*BTN1* - switches to your selected clock app.
|
||||||
|
|
||||||
|
*BTN2* - switches to the app launcher.
|
||||||
|
|
||||||
|
*BTN3* - invokes calibration ( can be cancelled if pressed accidentally)
|
||||||
|
|
||||||
|
|
||||||
|
## Acknowledgement
|
||||||
|
|
||||||
|
This app is based in the work done by [jeffmer](https://github.com/jeffmer/JeffsBangleAppsDev)
|
|
@ -0,0 +1,179 @@
|
||||||
|
var pal1color = new Uint16Array([0x0000,0xFFC0],0,1);
|
||||||
|
var pal2color = new Uint16Array([0x0000,0xffff],0,1);
|
||||||
|
var buf1 = Graphics.createArrayBuffer(160,160,1,{msb:true});
|
||||||
|
var buf2 = Graphics.createArrayBuffer(80,40,1,{msb:true});
|
||||||
|
var img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo="));
|
||||||
|
|
||||||
|
var bearing=0; // always point north
|
||||||
|
var heading = 0;
|
||||||
|
var candraw = false;
|
||||||
|
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
|
||||||
|
|
||||||
|
Bangle.setLCDTimeout(30);
|
||||||
|
|
||||||
|
function flip1(x,y) {
|
||||||
|
g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal1color},x,y);
|
||||||
|
buf1.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip2(x,y) {
|
||||||
|
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal2color},x,y);
|
||||||
|
buf2.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function radians(d) {
|
||||||
|
return (d*Math.PI) / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCompass(course) {
|
||||||
|
if(!candraw) return;
|
||||||
|
|
||||||
|
buf1.setColor(1);
|
||||||
|
buf1.fillCircle(80,80,79,79);
|
||||||
|
buf1.setColor(0);
|
||||||
|
buf1.fillCircle(80,80,69,69);
|
||||||
|
buf1.setColor(1);
|
||||||
|
buf1.drawImage(img, 80, 80, {scale:3, rotate:radians(course)} );
|
||||||
|
flip1(40, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
function newHeading(m,h){
|
||||||
|
var s = Math.abs(m - h);
|
||||||
|
var delta = (m>h)?1:-1;
|
||||||
|
if (s>=180){s=360-s; delta = -delta;}
|
||||||
|
if (s<2) return h;
|
||||||
|
var hd = h + delta*(1 + Math.round(s/5));
|
||||||
|
if (hd<0) hd+=360;
|
||||||
|
if (hd>360)hd-= 360;
|
||||||
|
return hd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tiltfixread(O,S){
|
||||||
|
var start = Date.now();
|
||||||
|
var m = Bangle.getCompass();
|
||||||
|
var g = Bangle.getAccel();
|
||||||
|
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
|
||||||
|
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
|
||||||
|
if (d<0) d+=360;
|
||||||
|
var phi = Math.atan(-g.x/-g.z);
|
||||||
|
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
|
||||||
|
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
|
||||||
|
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
|
||||||
|
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
|
||||||
|
var yh = m.dz*sinphi - m.dx*cosphi;
|
||||||
|
var psi = Math.atan2(yh,xh)*180/Math.PI;
|
||||||
|
if (psi<0) psi+=360;
|
||||||
|
return psi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note actual mag is 360-m, error in firmware
|
||||||
|
function reading() {
|
||||||
|
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
||||||
|
heading = newHeading(d,heading);
|
||||||
|
var dir = bearing - heading;
|
||||||
|
if (dir < 0) dir += 360;
|
||||||
|
if (dir > 360) dir -= 360;
|
||||||
|
drawCompass(dir); // we want compass to show us where to go
|
||||||
|
buf2.setColor(1);
|
||||||
|
buf2.setFontAlign(-1,-1);
|
||||||
|
buf2.setFont("Vector",38);
|
||||||
|
var course = Math.round(heading);
|
||||||
|
var cs = course.toString();
|
||||||
|
cs = course<10?"00"+cs : course<100 ?"0"+cs : cs;
|
||||||
|
buf2.drawString(cs,0,0);
|
||||||
|
flip2(90, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calibrate(){
|
||||||
|
var max={x:-32000, y:-32000, z:-32000},
|
||||||
|
min={x:32000, y:32000, z:32000};
|
||||||
|
var ref = setInterval(()=>{
|
||||||
|
var m = Bangle.getCompass();
|
||||||
|
max.x = m.x>max.x?m.x:max.x;
|
||||||
|
max.y = m.y>max.y?m.y:max.y;
|
||||||
|
max.z = m.z>max.z?m.z:max.z;
|
||||||
|
min.x = m.x<min.x?m.x:min.x;
|
||||||
|
min.y = m.y<min.y?m.y:min.y;
|
||||||
|
min.z = m.z<min.z?m.z:min.z;
|
||||||
|
}, 100);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(()=>{
|
||||||
|
if(ref) clearInterval(ref);
|
||||||
|
var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2};
|
||||||
|
var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2};
|
||||||
|
var avg = (delta.x+delta.y+delta.z)/3;
|
||||||
|
var scale = {x:avg/delta.x, y:avg/delta.y, z:avg/delta.z};
|
||||||
|
resolve({offset:offset,scale:scale});
|
||||||
|
},30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function docalibrate(e,first){
|
||||||
|
const title = "Calibrate";
|
||||||
|
const msg = "takes 30 seconds";
|
||||||
|
function action(b){
|
||||||
|
if (b) {
|
||||||
|
buf1.setColor(1);
|
||||||
|
buf1.setFont("Vector", 30);
|
||||||
|
buf1.setFontAlign(0,-1);
|
||||||
|
buf1.drawString("Figure 8s",80, 40);
|
||||||
|
buf1.drawString("to",80, 80);
|
||||||
|
buf1.drawString("Calibrate",80, 120);
|
||||||
|
flip1(40,40);
|
||||||
|
|
||||||
|
calibrate().then((r)=>{
|
||||||
|
require("Storage").write("magnav.json",r);
|
||||||
|
Bangle.buzz();
|
||||||
|
CALIBDATA = r;
|
||||||
|
startdraw();
|
||||||
|
setButtons();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
startdraw();
|
||||||
|
setTimeout(setButtons,1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first===undefined) first=false;
|
||||||
|
stopdraw();
|
||||||
|
clearWatch();
|
||||||
|
if (first)
|
||||||
|
E.showAlert(msg,title).then(action.bind(null,true));
|
||||||
|
else
|
||||||
|
E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
var intervalRef;
|
||||||
|
|
||||||
|
function startdraw(){
|
||||||
|
g.clear();
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
candraw = true;
|
||||||
|
intervalRef = setInterval(reading,200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopdraw() {
|
||||||
|
candraw=false;
|
||||||
|
if(intervalRef) {clearInterval(intervalRef);}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setButtons(){
|
||||||
|
setWatch(()=>{load();}, BTN1, {repeat:false,edge:"falling"});
|
||||||
|
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
||||||
|
setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"});
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on('lcdPower',function(on) {
|
||||||
|
if (on) {
|
||||||
|
startdraw();
|
||||||
|
} else {
|
||||||
|
stopdraw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Bangle.on('kill',()=>{Bangle.setCompassPower(0);});
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.setCompassPower(1);
|
||||||
|
startdraw();
|
||||||
|
setButtons();
|
After Width: | Height: | Size: 934 B |
After Width: | Height: | Size: 53 KiB |
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA="))
|
|
@ -7,3 +7,7 @@
|
||||||
0.07: Fixed GPS update, added guards against NaN values
|
0.07: Fixed GPS update, added guards against NaN values
|
||||||
0.08: Fix issue with GPS coordinates being wrong after the first one
|
0.08: Fix issue with GPS coordinates being wrong after the first one
|
||||||
0.09: Another GPS fix (log raw coordinates - not filtered ones)
|
0.09: Another GPS fix (log raw coordinates - not filtered ones)
|
||||||
|
0.10: Removed kalman filtering to allow distance log to work
|
||||||
|
Only log data every 5 seconds (not 1 sec)
|
||||||
|
Don't create a file until the first log entry is ready
|
||||||
|
Add labels for buttons
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
!function(){"use strict";const t={STOP:63488,PAUSE:65504,RUN:2016};function n(t,n,r){g.setColor(0),g.fillRect(n-60,r,n+60,r+30),g.setColor(65535),g.drawString(t,n,r)}function r(r){var e;g.setFontVector(30),g.setFontAlign(0,-1,0),n((r.distance/1e3).toFixed(2),60,55),n(function(t){const n=Math.round(t),r=Math.floor(n/3600),e=Math.floor(n/60)%60,o=n%60;return(r?r+":":"")+("0"+e).substr(-2)+":"+("0"+o).substr(-2)}(r.duration),180,55),n(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),r=Math.floor(n/60),e=n%60;return("0"+r).substr(-2)+"'"+("0"+e).substr(-2)+'"'}(r.speed),60,115),n(r.hr.toFixed(0),180,115),n(r.steps.toFixed(0),60,175),n(r.cadence.toFixed(0),180,175),g.setFont("6x8",2),g.setColor(r.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(e=new Date).getHours()).substr(-2)+":"+("0"+e.getMinutes()).substr(-2),120,220),g.setColor(t[r.status]),g.fillRect(160,216,240,240),g.setColor(0),g.drawString(r.status,200,220)}function e(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),r(t),Bangle.drawWidgets()}var o;function a(t){t.status===o.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),r=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(r,"w"),t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n")}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Running,r(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(o||(o={}));const s={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,v:NaN,t:NaN,dt:NaN,pError:NaN,vError:NaN,hr:60,hrError:100,file:null,drawing:!1,status:o.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var i;i=s,Bangle.on("GPS",t=>function(t,n){t.lat=n.lat,t.lon=n.lon,t.alt=n.alt,t.vel=n.speed/3.6,t.fix=n.fix,t.dop=n.hdop,t.gpsValid=t.fix>0&&t.dop<=5,function(t){const n=Date.now();let r=(n-t.t)/1e3;if(isFinite(r)||(r=0),t.t=n,t.dt+=r,t.status===o.Running&&(t.duration+=r),!t.gpsValid)return;const e=6371008.8+t.alt,a=t.lat*Math.PI/180,s=t.lon*Math.PI/180,i=e*Math.cos(a)*Math.cos(s),d=e*Math.cos(a)*Math.sin(s),u=e*Math.sin(a),g=t.vel;if(!t.x)return t.x=i,t.y=d,t.z=u,t.v=g,t.pError=2.5*t.dop,void(t.vError=.05*t.dop);const l=i-t.x,c=d-t.y,p=u-t.z,f=g-t.v,N=Math.sqrt(l*l+c*c+p*p),h=Math.abs(f);t.pError+=t.v*t.dt,t.dt=0;const S=N+2.5*t.dop,E=h+.05*t.dop,w=t.pError/(t.pError+S)||0,x=t.vError/(t.vError+E)||0;t.x+=l*w,t.y+=c*w,t.z+=p*w,t.v+=f*x,t.pError+=(S-t.pError)*w,t.vError+=(E-t.vError)*x,t.status===o.Running&&(t.distance+=N*w,t.speed=t.distance/t.duration||0,t.cadence=60*t.steps/t.duration||0)}(t),r(t),t.gpsValid&&t.status===o.Running&&function(t){t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(t)}(i,t)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const r=n.bpm-t.hr,e=Math.abs(r)+101-n.confidence,o=t.hrError/(t.hrError+e)||0;t.hr+=r*o,t.hrError+=(e-t.hrError)*o}(t,n)),Bangle.setHRMPower(1)}(s),function(t){Bangle.on("step",()=>function(t){t.status===o.Running&&(t.steps+=1)}(t))}(s),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&e(t)}),e(t)}(s),setWatch(()=>a(s),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===o.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===o.Running?t.status=o.Paused:t.status=o.Stopped,r(t)}(s),BTN3,{repeat:!0,edge:"falling"})}();
|
!function(){"use strict";var t;!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(t||(t={}));const n={STOP:63488,PAUSE:65504,RUN:2016};function e(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function i(i){var s;g.setFontVector(30),g.setFontAlign(0,-1,0),e((i.distance/1e3).toFixed(2),60,55),e(function(t){const n=Math.round(t),e=Math.floor(n/3600),i=Math.floor(n/60)%60,s=n%60;return(e?e+":":"")+("0"+i).substr(-2)+":"+("0"+s).substr(-2)}(i.duration),172,55),e(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),i=n%60;return("0"+e).substr(-2)+"'"+("0"+i).substr(-2)+'"'}(i.speed),60,115),e(i.hr.toFixed(0),172,115),e(i.steps.toFixed(0),60,175),e(i.cadence.toFixed(0),172,175),g.setFont("6x8",2),g.setColor(i.gpsValid?2016:63488),g.fillRect(0,216,80,240),g.setColor(0),g.drawString("GPS",40,220),g.setColor(65535),g.fillRect(80,216,160,240),g.setColor(0),g.drawString(("0"+(s=new Date).getHours()).substr(-2)+":"+("0"+s.getMinutes()).substr(-2),120,220),g.setColor(n[i.status]),g.fillRect(160,216,230,240),g.setColor(0),g.drawString(i.status,200,220),g.setFont("6x8").setFontAlign(0,0,1).setColor(-1),i.status===t.Paused?g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1):i.status===t.Running?g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1):g.drawString("START",236,60,1).drawString(" ",236,180,1)}function s(t){g.clear(),g.setColor(50712),g.setFont("6x8",2),g.setFontAlign(0,-1,0),g.drawString("DIST (KM)",60,32),g.drawString("TIME",180,32),g.drawString("PACE",60,92),g.drawString("HEART",180,92),g.drawString("STEPS",60,152),g.drawString("CADENCE",180,152),i(t),Bangle.drawWidgets()}function a(n){n.status===t.Stopped&&function(t){const n=(new Date).toISOString().replace(/[-:]/g,""),e=`banglerun_${n.substr(2,6)}_${n.substr(9,6)}`;t.file=require("Storage").open(e,"w"),t.fileWritten=!1}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Running,i(n)}const r={fix:NaN,lat:NaN,lon:NaN,alt:NaN,vel:NaN,dop:NaN,gpsValid:!1,x:NaN,y:NaN,z:NaN,t:NaN,timeSinceLog:0,hr:60,hrError:100,file:null,fileWritten:!1,drawing:!1,status:t.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var o;o=r,Bangle.on("GPS",n=>function(n,e){n.lat=e.lat,n.lon=e.lon,n.alt=e.alt,n.vel=e.speed/3.6,n.fix=e.fix,n.dop=e.hdop,n.gpsValid=n.fix>0,function(n){const e=Date.now();let i=(e-n.t)/1e3;if(isFinite(i)||(i=0),n.t=e,n.timeSinceLog+=i,n.status===t.Running&&(n.duration+=i),!n.gpsValid)return;const s=6371008.8+n.alt,a=n.lat*Math.PI/180,r=n.lon*Math.PI/180,o=s*Math.cos(a)*Math.cos(r),g=s*Math.cos(a)*Math.sin(r),d=s*Math.sin(a);if(!n.x)return n.x=o,n.y=g,void(n.z=d);const u=o-n.x,l=g-n.y,c=d-n.z,f=Math.sqrt(u*u+l*l+c*c);n.x=o,n.y=g,n.z=d,n.status===t.Running&&(n.distance+=f,n.speed=n.distance/n.duration||0,n.cadence=60*n.steps/n.duration||0)}(n),i(n),n.gpsValid&&n.status===t.Running&&n.timeSinceLog>5&&(n.timeSinceLog=0,function(t){t.fileWritten||(t.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n"),t.fileWritten=!0),t.file.write([Date.now().toFixed(0),t.lat.toFixed(6),t.lon.toFixed(6),t.alt.toFixed(2),t.duration.toFixed(0),t.distance.toFixed(2),t.hr.toFixed(0),t.steps.toFixed(0)].join(",")+"\n")}(n))}(o,n)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,i=Math.abs(e)+101-n.confidence,s=t.hrError/(t.hrError+i)||0;t.hr+=e*s,t.hrError+=(i-t.hrError)*s}(t,n)),Bangle.setHRMPower(1)}(r),function(n){Bangle.on("step",()=>function(n){n.status===t.Running&&(n.steps+=1)}(n))}(r),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&s(t)}),s(t)}(r),setWatch(()=>a(r),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(n){n.status===t.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(n),n.status===t.Running?n.status=t.Paused:n.status=t.Stopped,i(n)}(r),BTN3,{repeat:!0,edge:"falling"})}();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AppState } from './state';
|
import { ActivityStatus, AppState } from './state';
|
||||||
|
|
||||||
declare var Bangle: any;
|
declare var Bangle: any;
|
||||||
declare var g: any;
|
declare var g: any;
|
||||||
|
@ -26,11 +26,11 @@ function drawBackground(): void {
|
||||||
g.setFont('6x8', 2);
|
g.setFont('6x8', 2);
|
||||||
g.setFontAlign(0, -1, 0);
|
g.setFontAlign(0, -1, 0);
|
||||||
g.drawString('DIST (KM)', 60, 32);
|
g.drawString('DIST (KM)', 60, 32);
|
||||||
g.drawString('TIME', 180, 32);
|
g.drawString('TIME', 172, 32);
|
||||||
g.drawString('PACE', 60, 92);
|
g.drawString('PACE', 60, 92);
|
||||||
g.drawString('HEART', 180, 92);
|
g.drawString('HEART', 172, 92);
|
||||||
g.drawString('STEPS', 60, 152);
|
g.drawString('STEPS', 60, 152);
|
||||||
g.drawString('CADENCE', 180, 152);
|
g.drawString('CADENCE', 172, 152);
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawValue(value: string, x: number, y: number) {
|
function drawValue(value: string, x: number, y: number) {
|
||||||
|
@ -45,11 +45,11 @@ function draw(state: AppState): void {
|
||||||
g.setFontAlign(0, -1, 0);
|
g.setFontAlign(0, -1, 0);
|
||||||
|
|
||||||
drawValue(formatDistance(state.distance), 60, 55);
|
drawValue(formatDistance(state.distance), 60, 55);
|
||||||
drawValue(formatTime(state.duration), 180, 55);
|
drawValue(formatTime(state.duration), 172, 55);
|
||||||
drawValue(formatPace(state.speed), 60, 115);
|
drawValue(formatPace(state.speed), 60, 115);
|
||||||
drawValue(state.hr.toFixed(0), 180, 115);
|
drawValue(state.hr.toFixed(0), 172, 115);
|
||||||
drawValue(state.steps.toFixed(0), 60, 175);
|
drawValue(state.steps.toFixed(0), 60, 175);
|
||||||
drawValue(state.cadence.toFixed(0), 180, 175);
|
drawValue(state.cadence.toFixed(0), 172, 175);
|
||||||
|
|
||||||
g.setFont('6x8', 2);
|
g.setFont('6x8', 2);
|
||||||
|
|
||||||
|
@ -64,9 +64,18 @@ function draw(state: AppState): void {
|
||||||
g.drawString(formatClock(new Date()), 120, 220);
|
g.drawString(formatClock(new Date()), 120, 220);
|
||||||
|
|
||||||
g.setColor(STATUS_COLORS[state.status]);
|
g.setColor(STATUS_COLORS[state.status]);
|
||||||
g.fillRect(160, 216, 240, 240);
|
g.fillRect(160, 216, 230, 240);
|
||||||
g.setColor(0x0000);
|
g.setColor(0x0000);
|
||||||
g.drawString(state.status, 200, 220);
|
g.drawString(state.status, 200, 220);
|
||||||
|
|
||||||
|
g.setFont("6x8").setFontAlign(0,0,1).setColor(-1);
|
||||||
|
if (state.status === ActivityStatus.Paused) {
|
||||||
|
g.drawString("START",236,60,1).drawString(" CLEAR ",236,180,1);
|
||||||
|
} else if (state.status === ActivityStatus.Running) {
|
||||||
|
g.drawString(" PAUSE ",236,60,1).drawString(" PAUSE ",236,180,1);
|
||||||
|
} else {
|
||||||
|
g.drawString("START",236,60,1).drawString(" ",236,180,1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAll(state: AppState) {
|
function drawAll(state: AppState) {
|
||||||
|
|
|
@ -14,8 +14,6 @@ interface GpsEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const EARTH_RADIUS = 6371008.8;
|
const EARTH_RADIUS = 6371008.8;
|
||||||
const POS_ACCURACY = 2.5;
|
|
||||||
const VEL_ACCURACY = 0.05;
|
|
||||||
|
|
||||||
function initGps(state: AppState): void {
|
function initGps(state: AppState): void {
|
||||||
Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps));
|
Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps));
|
||||||
|
@ -29,13 +27,17 @@ function readGps(state: AppState, gps: GpsEvent): void {
|
||||||
state.vel = gps.speed / 3.6;
|
state.vel = gps.speed / 3.6;
|
||||||
state.fix = gps.fix;
|
state.fix = gps.fix;
|
||||||
state.dop = gps.hdop;
|
state.dop = gps.hdop;
|
||||||
|
state.gpsValid = state.fix > 0;
|
||||||
state.gpsValid = state.fix > 0 && state.dop <= 5;
|
|
||||||
|
|
||||||
updateGps(state);
|
updateGps(state);
|
||||||
draw(state);
|
draw(state);
|
||||||
|
|
||||||
if (state.gpsValid && state.status === ActivityStatus.Running) {
|
/* Only log GPS data every 5 secs if we
|
||||||
|
have a fix and we're running. */
|
||||||
|
if (state.gpsValid &&
|
||||||
|
state.status === ActivityStatus.Running &&
|
||||||
|
state.timeSinceLog > 5) {
|
||||||
|
state.timeSinceLog = 0;
|
||||||
updateLog(state);
|
updateLog(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,9 +46,8 @@ function updateGps(state: AppState): void {
|
||||||
const t = Date.now();
|
const t = Date.now();
|
||||||
let dt = (t - state.t) / 1000;
|
let dt = (t - state.t) / 1000;
|
||||||
if (!isFinite(dt)) dt=0;
|
if (!isFinite(dt)) dt=0;
|
||||||
|
|
||||||
state.t = t;
|
state.t = t;
|
||||||
state.dt += dt;
|
state.timeSinceLog += dt;
|
||||||
|
|
||||||
if (state.status === ActivityStatus.Running) {
|
if (state.status === ActivityStatus.Running) {
|
||||||
state.duration += dt;
|
state.duration += dt;
|
||||||
|
@ -62,52 +63,25 @@ function updateGps(state: AppState): void {
|
||||||
const x = r * Math.cos(lat) * Math.cos(lon);
|
const x = r * Math.cos(lat) * Math.cos(lon);
|
||||||
const y = r * Math.cos(lat) * Math.sin(lon);
|
const y = r * Math.cos(lat) * Math.sin(lon);
|
||||||
const z = r * Math.sin(lat);
|
const z = r * Math.sin(lat);
|
||||||
const v = state.vel;
|
|
||||||
|
|
||||||
if (!state.x) {
|
if (!state.x) {
|
||||||
state.x = x;
|
state.x = x;
|
||||||
state.y = y;
|
state.y = y;
|
||||||
state.z = z;
|
state.z = z;
|
||||||
state.v = v;
|
|
||||||
state.pError = state.dop * POS_ACCURACY;
|
|
||||||
state.vError = state.dop * VEL_ACCURACY;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dx = x - state.x;
|
const dx = x - state.x;
|
||||||
const dy = y - state.y;
|
const dy = y - state.y;
|
||||||
const dz = z - state.z;
|
const dz = z - state.z;
|
||||||
const dv = v - state.v;
|
|
||||||
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
const dvMag = Math.abs(dv);
|
|
||||||
|
|
||||||
state.pError += state.v * state.dt;
|
state.x = x;
|
||||||
state.dt = 0;
|
state.y = y;
|
||||||
|
state.z = z;
|
||||||
const pError = dpMag + state.dop * POS_ACCURACY;
|
|
||||||
const vError = dvMag + state.dop * VEL_ACCURACY;
|
|
||||||
|
|
||||||
const pGain = (state.pError / (state.pError + pError)) || 0;
|
|
||||||
const vGain = (state.vError / (state.vError + vError)) || 0;
|
|
||||||
|
|
||||||
state.x += dx * pGain;
|
|
||||||
state.y += dy * pGain;
|
|
||||||
state.z += dz * pGain;
|
|
||||||
state.v += dv * vGain;
|
|
||||||
state.pError += (pError - state.pError) * pGain;
|
|
||||||
state.vError += (vError - state.vError) * vGain;
|
|
||||||
|
|
||||||
/*// we're not currently updating lat/lon with the kalman filter
|
|
||||||
// as it seems not to update them correctly at the moment
|
|
||||||
// and we only use them for logging (where it makes sense to use
|
|
||||||
// raw GPS coordinates)
|
|
||||||
const pMag = Math.sqrt(state.x * state.x + state.y * state.y + state.z * state.z);
|
|
||||||
state.lat = (Math.asin(state.z / pMag) * 180 / Math.PI) || 0;
|
|
||||||
state.lon = (Math.atan2(state.y, state.x) * 180 / Math.PI) || 0;
|
|
||||||
state.alt = pMag - EARTH_RADIUS;*/
|
|
||||||
|
|
||||||
if (state.status === ActivityStatus.Running) {
|
if (state.status === ActivityStatus.Running) {
|
||||||
state.distance += dpMag * pGain;
|
state.distance += dpMag;
|
||||||
state.speed = (state.distance / state.duration) || 0;
|
state.speed = (state.distance / state.duration) || 0;
|
||||||
state.cadence = (60 * state.steps / state.duration) || 0;
|
state.cadence = (60 * state.steps / state.duration) || 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ function initLog(state: AppState): void {
|
||||||
const time = datetime.substr(9, 6);
|
const time = datetime.substr(9, 6);
|
||||||
const filename = `banglerun_${date}_${time}`;
|
const filename = `banglerun_${date}_${time}`;
|
||||||
state.file = require('Storage').open(filename, 'w');
|
state.file = require('Storage').open(filename, 'w');
|
||||||
|
state.fileWritten = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLog(state: AppState): void {
|
||||||
|
if (!state.fileWritten) {
|
||||||
state.file.write([
|
state.file.write([
|
||||||
'timestamp',
|
'timestamp',
|
||||||
'latitude',
|
'latitude',
|
||||||
|
@ -18,9 +23,8 @@ function initLog(state: AppState): void {
|
||||||
'heartrate',
|
'heartrate',
|
||||||
'steps',
|
'steps',
|
||||||
].join(',') + '\n');
|
].join(',') + '\n');
|
||||||
}
|
state.fileWritten = true;
|
||||||
|
}
|
||||||
function updateLog(state: AppState): void {
|
|
||||||
state.file.write([
|
state.file.write([
|
||||||
Date.now().toFixed(0),
|
Date.now().toFixed(0),
|
||||||
state.lat.toFixed(6),
|
state.lat.toFixed(6),
|
||||||
|
|
|
@ -14,15 +14,14 @@ interface AppState {
|
||||||
dop: number;
|
dop: number;
|
||||||
gpsValid: boolean;
|
gpsValid: boolean;
|
||||||
|
|
||||||
// GPS Kalman data
|
// Absolute position data
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
v: number;
|
// Last fix time
|
||||||
t: number;
|
t: number;
|
||||||
dt: number;
|
// Last time we saved log info
|
||||||
pError: number;
|
timeSinceLog : number;
|
||||||
vError: number;
|
|
||||||
|
|
||||||
// HRM data
|
// HRM data
|
||||||
hr: number,
|
hr: number,
|
||||||
|
@ -30,6 +29,7 @@ interface AppState {
|
||||||
|
|
||||||
// Logger data
|
// Logger data
|
||||||
file: File;
|
file: File;
|
||||||
|
fileWritten: boolean;
|
||||||
|
|
||||||
// Drawing data
|
// Drawing data
|
||||||
drawing: boolean;
|
drawing: boolean;
|
||||||
|
@ -62,16 +62,14 @@ function initState(): AppState {
|
||||||
x: NaN,
|
x: NaN,
|
||||||
y: NaN,
|
y: NaN,
|
||||||
z: NaN,
|
z: NaN,
|
||||||
v: NaN,
|
|
||||||
t: NaN,
|
t: NaN,
|
||||||
dt: NaN,
|
timeSinceLog : 0,
|
||||||
pError: NaN,
|
|
||||||
vError: NaN,
|
|
||||||
|
|
||||||
hr: 60,
|
hr: 60,
|
||||||
hrError: 100,
|
hrError: 100,
|
||||||
|
|
||||||
file: null,
|
file: null,
|
||||||
|
fileWritten: false,
|
||||||
|
|
||||||
drawing: false,
|
drawing: false,
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Initial version
|
|
@ -0,0 +1,12 @@
|
||||||
|
# countdown-timer
|
||||||
|
|
||||||
|
A basic bangle.js timer with a focus on usability.
|
||||||
|
|
||||||
|
* Start or Pause the timer with BTN1
|
||||||
|
* Reset the timer with BTN2
|
||||||
|
* Exit the application with BTN3
|
||||||
|
* Touch the right side of the screen to increase the time amount by 1 second
|
||||||
|
* Touch the left side of the scren to decrease the time amount by 1 second
|
||||||
|
* Touching and holding the screen will increase or decrease the time amount by 60 seconds at a time.
|
||||||
|
|
||||||
|
Icons made by [Freepik](https://www.freepik.com) from [Flaticon](https://www.flaticon.com/).
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwgMJhvd7tEogkSC4lAC6xWUgUgNysiC6hGBL58BiMQLoYXDMJkRAAIWEC4gYJFwIABCwgXFDBAWCpoXLMYwuCigUD6czmUSmQYKC4QuD6f//8xiUzmckJJIuECwQXEDAgXGFwc/C4XykYXCmZIJCwVPCwQABCwczmgwHXYXUCwgXFGBAXC74XLGAQXELowXIVgYXF6QWF+Z3EJAhGFUganHJAoXFRooXLMAQXCLwwXHMAQXUMAQXCRwQWFC9HSiMvC40iCocxiKoEC4cTC4sRiLBDC5MiF4wXFmQXHL4/zkRHEO6H/OwwXFX5IXfd5HfC5s0C4/TC5skRwZ4LLxAXHMAxeIC4hIJLxYXEJAxGMJAgwFFw4XGGAZhD+UjLoxGEC4owDmMSIoouGJAgYBGIIXDCwYuGGAoABIQMiaIQuKDA/dCoguJJIwXHCxQYGCyIYFCyRjELZYA="))
|
|
@ -0,0 +1,232 @@
|
||||||
|
const heatshrinkDecompress = require("heatshrink").decompress;
|
||||||
|
|
||||||
|
const playIcon = heatshrinkDecompress(atob("jEYwhC/gFwBZV3BhV3u4LLBhILCEpALCBhALDu9gBaojKHZZrVQZSbLAG4A="));
|
||||||
|
const pauseIcon = heatshrinkDecompress(atob("jEYwhC/xGIAYoL/Bf4LfAHA="));
|
||||||
|
const resetIcon = heatshrinkDecompress(atob("jEYwg30h3u93gAgIKHBgXuBYgIBoEEBoQWFAgQMCBYgrBE4giEBYYjGAgY+DBY4AHBZlABZQ7DLIpTFAo5ZJLYYDFTZKzLAGQA=="));
|
||||||
|
const closeIcon = heatshrinkDecompress(atob("jEYwhC/4AEDhgKEhnMAofMCIgGECAoHFCwwIDCw4YDCxAYCCxALMEZY7KKZZrKQZibKAHIA="));
|
||||||
|
|
||||||
|
const timerState = {
|
||||||
|
IDLE: 0,
|
||||||
|
RUNNING: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentState = timerState.IDLE;
|
||||||
|
let remainingSeconds = 0;
|
||||||
|
let countdownInterval = null;
|
||||||
|
let increasingInterval = null;
|
||||||
|
let decreasingInterval = null;
|
||||||
|
let isDecreasingRemainingSeconds = false;
|
||||||
|
let isIncreasingRemainingSeconds = false;
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
g.clear();
|
||||||
|
g.setFont("Vector", 40);
|
||||||
|
g.setFontAlign(0, 0);
|
||||||
|
|
||||||
|
registerInputHandlers();
|
||||||
|
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerInputHandlers() {
|
||||||
|
setWatch(onPrimaryButtonPressed, BTN1, { repeat: true });
|
||||||
|
setWatch(onResetButtonPressed, BTN2, { repeat: true });
|
||||||
|
setWatch(onExitButtonPressed, BTN3, { repeat: true });
|
||||||
|
setWatch(onDecreaseRemainingSecondsPressed, BTN4, { repeat: true, edge: "rising" });
|
||||||
|
setWatch(onIncreaseRemainingSecondsPressed, BTN5, { repeat: true, edge: "rising" });
|
||||||
|
setWatch(onDecreaseRemainingSecondsReleased, BTN4, { repeat: true, edge: "falling" });
|
||||||
|
setWatch(onIncreaseRemainingSecondsReleased, BTN5, { repeat: true, edge: "falling" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
g.clearRect(200, 0, 240, 240);
|
||||||
|
g.clearRect(0, 0, 240, 80);
|
||||||
|
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
|
||||||
|
g.drawImage(resetIcon, 216, 108);
|
||||||
|
g.drawImage(closeIcon, 216, 188);
|
||||||
|
|
||||||
|
if (currentState == timerState.IDLE) {
|
||||||
|
g.drawImage(playIcon, 216, 28);
|
||||||
|
} else {
|
||||||
|
g.drawImage(pauseIcon, 216, 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.flip();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawRemainingSecondsPanel() {
|
||||||
|
g.clearRect(0, 100, 200, 140);
|
||||||
|
g.drawString(formatRemainingSeconds(), 105, 120);
|
||||||
|
|
||||||
|
if (currentState == timerState.IDLE) {
|
||||||
|
drawSubtractRemainingSeconds();
|
||||||
|
drawIncreaseRemainingSeconds();
|
||||||
|
} else {
|
||||||
|
g.setColor(0.4, 0.4, 0.4);
|
||||||
|
drawSubtractRemainingSeconds();
|
||||||
|
drawIncreaseRemainingSeconds();
|
||||||
|
g.setColor(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSubtractRemainingSeconds() {
|
||||||
|
if (isDecreasingRemainingSeconds) {
|
||||||
|
drawFilledCircle(22, 117, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.drawString("-", 25, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawIncreaseRemainingSeconds() {
|
||||||
|
if (isIncreasingRemainingSeconds) {
|
||||||
|
drawFilledCircle(182, 117, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.drawString("+", 185, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawFilledCircle(x, y, radians) {
|
||||||
|
g.setColor(0.1, 0.37, 0.87);
|
||||||
|
g.fillCircle(x, y, radians);
|
||||||
|
g.setColor(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRemainingSeconds() {
|
||||||
|
const minutes = Math.floor(remainingSeconds / 60);
|
||||||
|
const minutesTens = Math.floor(minutes / 10);
|
||||||
|
const minutesUnits = minutes % 10;
|
||||||
|
|
||||||
|
const seconds = remainingSeconds % 60;
|
||||||
|
const secondsTens = Math.floor(seconds / 10);
|
||||||
|
const secondsUnits = seconds % 10;
|
||||||
|
|
||||||
|
return `${minutesTens}${minutesUnits}:${secondsTens}${secondsUnits}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPrimaryButtonPressed() {
|
||||||
|
if (isIncreasingRemainingSeconds || isDecreasingRemainingSeconds) return;
|
||||||
|
|
||||||
|
if (currentState == timerState.IDLE) {
|
||||||
|
if (remainingSeconds == 0) return;
|
||||||
|
currentState = timerState.RUNNING;
|
||||||
|
beginCountdown();
|
||||||
|
draw();
|
||||||
|
} else {
|
||||||
|
currentState = timerState.IDLE;
|
||||||
|
stopCountdown();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginCountdown() {
|
||||||
|
countdownInterval = setInterval(countdown, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countdown() {
|
||||||
|
--remainingSeconds;
|
||||||
|
|
||||||
|
if (remainingSeconds <= 0) {
|
||||||
|
remainingSeconds = 0;
|
||||||
|
stopCountdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
|
||||||
|
if (remainingSeconds <= 0) {
|
||||||
|
drawStopMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStopMessage() {
|
||||||
|
draw();
|
||||||
|
Bangle.buzz(800);
|
||||||
|
g.setFont("Vector", 30);
|
||||||
|
g.setColor(1.0, 0.91, 0);
|
||||||
|
g.drawString("Time's Up!", 105, 40);
|
||||||
|
g.setColor(-1);
|
||||||
|
g.setFont("Vector", 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCountdown() {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
countdownInterval = null;
|
||||||
|
currentState = timerState.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResetButtonPressed() {
|
||||||
|
currentState = timerState.IDLE;
|
||||||
|
remainingSeconds = 0;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExitButtonPressed() {
|
||||||
|
Bangle.showLauncher();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIncreaseRemainingSecondsPressed() {
|
||||||
|
if (currentState == timerState.RUNNING) return;
|
||||||
|
incremementRemainingSeconds();
|
||||||
|
|
||||||
|
increasingInterval = setInterval(() => {
|
||||||
|
remainingSeconds += 60;
|
||||||
|
|
||||||
|
if (remainingSeconds >= 5999) {
|
||||||
|
remainingSeconds = 5999;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
isIncreasingRemainingSeconds = true;
|
||||||
|
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function incremementRemainingSeconds() {
|
||||||
|
if (remainingSeconds >= 5999) return;
|
||||||
|
++remainingSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIncreaseRemainingSecondsReleased() {
|
||||||
|
if (currentState == timerState.RUNNING) return;
|
||||||
|
clearInterval(increasingInterval);
|
||||||
|
isIncreasingRemainingSeconds = false;
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDecreaseRemainingSecondsPressed() {
|
||||||
|
if (currentState == timerState.RUNNING) return;
|
||||||
|
decreaseRemainingSeconds();
|
||||||
|
|
||||||
|
decreasingInterval = setInterval(() => {
|
||||||
|
remainingSeconds -= 60;
|
||||||
|
|
||||||
|
if (remainingSeconds < 0) {
|
||||||
|
remainingSeconds = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
isDecreasingRemainingSeconds = true;
|
||||||
|
|
||||||
|
drawRemainingSecondsPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreaseRemainingSeconds() {
|
||||||
|
if (remainingSeconds <= 0) return;
|
||||||
|
--remainingSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDecreaseRemainingSecondsReleased() {
|
||||||
|
if (currentState == timerState.RUNNING) return;
|
||||||
|
|
||||||
|
clearInterval(decreasingInterval);
|
||||||
|
|
||||||
|
isDecreasingRemainingSeconds = false;
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -20,3 +20,4 @@
|
||||||
0.18: Added reporting of step count and HRM (new Gadgetbridges can now log this)
|
0.18: Added reporting of step count and HRM (new Gadgetbridges can now log this)
|
||||||
0.19: Support for call incoming/start/end
|
0.19: Support for call incoming/start/end
|
||||||
0.20: Reduce memory usage
|
0.20: Reduce memory usage
|
||||||
|
0.21: Fix HRM setting
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
"Find Phone" : function() { E.showMenu(findPhone); },
|
"Find Phone" : function() { E.showMenu(findPhone); },
|
||||||
"Record HRM" : {
|
"Record HRM" : {
|
||||||
value: settings().hrm,
|
value: !!settings().hrm,
|
||||||
format: v => v?"Yes":"No",
|
format: v => v?"Yes":"No",
|
||||||
onchange: v => updateSetting('hrm', v)
|
onchange: v => updateSetting('hrm', v)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
require("heatshrink").decompress(atob("mEw4kA///t0up0upsutsuIm0RiIWUgIXBiAFDC58hC4MRkA0CDgQuOJAQ0FLpoSDDYYukC4YuSC4guSCQouRC4guSR4cSFySQGFyLAFD4Z5UDo4vNJqYuJGBouJGBoNEC6iTGC54MDC6IKFC6KDFC6TsFC9zXRC5a+NPJIWKC5DfFC6QAOC7UgC6wAIEBYXLDBYXMNRQXNbJMBGC8hC6w0NJBIX/C/4XTAEw"))
|
require("heatshrink").decompress(atob("mEw4MA///t0uChkD4AFDg/guAFCh/4/AFCj/xAoc/8fwAoV/4/gAoePEgd/j+AAoV+n4vDv1+Aoc//gFDj4uDHYPwjBHE/4LD4P/FQUfwf/GwU/g//IQU+Aok8h4FDnAFEwAFE4AFE/gdEAo0DGoYFCIIReBJoYFBngFEAAYFF/wFEZQIjDAoQ1CAoSuCAoSLCv4FBEwU/AoiEB/4sDAsYAJ"))
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Use HRM data and calculations from Bangle.js (don't access hardware directly)
|
0.02: Use HRM data and calculations from Bangle.js (don't access hardware directly)
|
||||||
|
0.03: Fix timing issues, and use 1/2 scale to keep graph on screen
|
||||||
|
|
|
@ -12,7 +12,10 @@ function onHRM(h) {
|
||||||
hrmInfo = h;
|
hrmInfo = h;
|
||||||
hrmOffset = 0;
|
hrmOffset = 0;
|
||||||
if (hrmInterval) clearInterval(hrmInterval);
|
if (hrmInterval) clearInterval(hrmInterval);
|
||||||
hrmInterval = setInterval(readHRM,40);
|
hrmInterval = undefined;
|
||||||
|
setTimeout(function() {
|
||||||
|
hrmInterval = setInterval(readHRM,41);
|
||||||
|
}, 40);
|
||||||
|
|
||||||
var px = g.getWidth()/2;
|
var px = g.getWidth()/2;
|
||||||
g.setFontAlign(0,0);
|
g.setFontAlign(0,0);
|
||||||
|
@ -35,7 +38,6 @@ function countDown() {
|
||||||
countDown();
|
countDown();
|
||||||
|
|
||||||
|
|
||||||
var min=0,max=0;
|
|
||||||
var wasHigh = 0, wasLow = 0;
|
var wasHigh = 0, wasLow = 0;
|
||||||
var lastHigh = getTime();
|
var lastHigh = getTime();
|
||||||
var hrmList = [];
|
var hrmList = [];
|
||||||
|
@ -51,9 +53,7 @@ function readHRM() {
|
||||||
for (var i=0;i<2;i++) {
|
for (var i=0;i<2;i++) {
|
||||||
var a = hrmInfo.raw[hrmOffset];
|
var a = hrmInfo.raw[hrmOffset];
|
||||||
hrmOffset++;
|
hrmOffset++;
|
||||||
min=Math.min(min*0.97+a*0.03,a);
|
y = E.clip(170 - (a*2),100,230);
|
||||||
max=Math.max(max*0.97+a*0.03,a);
|
|
||||||
y = E.clip(170 - (a*4),100,230);
|
|
||||||
g.setColor(1,1,1);
|
g.setColor(1,1,1);
|
||||||
g.lineTo(hrmOffset, y);
|
g.lineTo(hrmOffset, y);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,3 +3,4 @@
|
||||||
0.03: Pass `area{x,y,w,h}` to render callback instead of just `y`
|
0.03: Pass `area{x,y,w,h}` to render callback instead of just `y`
|
||||||
0.05: Adjust position of notification src text
|
0.05: Adjust position of notification src text
|
||||||
0.06: Support background color
|
0.06: Support background color
|
||||||
|
0.07: Auto-calculate height, and pad text down even when there's no title (so it stays on-screen)
|
||||||
|
|
|
@ -10,7 +10,7 @@ other applications or widgets to display messages.
|
||||||
```JS
|
```JS
|
||||||
options = {
|
options = {
|
||||||
on : bool, // turn screen on, default true
|
on : bool, // turn screen on, default true
|
||||||
size : int, // height of notification, default 80 (max)
|
size : int, // height of notification, default is fit to height (80 max)
|
||||||
title : string, // optional title
|
title : string, // optional title
|
||||||
id // optional notification ID, used with hide()
|
id // optional notification ID, used with hide()
|
||||||
src : string, // optional source name
|
src : string, // optional source name
|
||||||
|
|
|
@ -70,8 +70,18 @@ exports.show = function(options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
if (options.on===undefined) options.on = true;
|
if (options.on===undefined) options.on = true;
|
||||||
id = ("id" in options)?options.id:null;
|
id = ("id" in options)?options.id:null;
|
||||||
let size = options.size || 80;
|
let w = 240;
|
||||||
if (size>80) {size = 80}
|
let text = [];
|
||||||
|
let size = options.size;
|
||||||
|
if (options.body) {
|
||||||
|
const bh = (size || 80) - 20,
|
||||||
|
maxRows=Math.floor((bh-4)/8), // font=6x8
|
||||||
|
maxChars=Math.floor(w/6)-2;
|
||||||
|
text=fitWords(options.body, maxRows, maxChars);
|
||||||
|
// set size based on newlines
|
||||||
|
if (!size) size = 28 + (text.match(/\n/g).length+1)*8;
|
||||||
|
} else size = 20;
|
||||||
|
if (size>80) size = 80;
|
||||||
const oldMode = Bangle.getLCDMode();
|
const oldMode = Bangle.getLCDMode();
|
||||||
// TODO: throw exception if double-buffered?
|
// TODO: throw exception if double-buffered?
|
||||||
// TODO: throw exception if size>80?
|
// TODO: throw exception if size>80?
|
||||||
|
@ -80,7 +90,6 @@ exports.show = function(options) {
|
||||||
// drawing area
|
// drawing area
|
||||||
let x = 0,
|
let x = 0,
|
||||||
y = 320-size,
|
y = 320-size,
|
||||||
w = 240,
|
|
||||||
h = size,
|
h = size,
|
||||||
b = y+h-1, r = x+w-1; // bottom,right
|
b = y+h-1, r = x+w-1; // bottom,right
|
||||||
g.setClipRect(x,y, r,b);
|
g.setClipRect(x,y, r,b);
|
||||||
|
@ -99,20 +108,18 @@ exports.show = function(options) {
|
||||||
g.setFont("6x8", 1).setFontAlign(1, 1, 0);
|
g.setFont("6x8", 1).setFontAlign(1, 1, 0);
|
||||||
g.drawString(options.src.substring(0, 10), g.getWidth()-23,y+18);
|
g.drawString(options.src.substring(0, 10), g.getWidth()-23,y+18);
|
||||||
}
|
}
|
||||||
y += 20;h -= 20;
|
|
||||||
}
|
}
|
||||||
|
// we always need to pad because of the curved edges of the screen
|
||||||
|
y += 20; h -= 20;
|
||||||
if (options.icon) {
|
if (options.icon) {
|
||||||
let i = options.icon, iw;
|
let i = options.icon, iw;
|
||||||
g.drawImage(i, x,y+4);
|
g.drawImage(i, x,y+4);
|
||||||
if ("string"==typeof i) {iw = i.charCodeAt(0)}
|
if ("string"==typeof i) iw = i.charCodeAt(0);
|
||||||
else {iw = i[0]}
|
else iw = i[0];
|
||||||
x += iw;w -= iw;
|
x += iw;w -= iw;
|
||||||
}
|
}
|
||||||
// body text
|
// body text
|
||||||
if (options.body) {
|
if (options.body) {
|
||||||
const maxRows=Math.floor((h-4)/8), // font=6x8
|
|
||||||
maxChars=Math.floor(w/6)-2,
|
|
||||||
text=fitWords(options.body, maxRows, maxChars);
|
|
||||||
g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4);
|
g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +140,7 @@ exports.show = function(options) {
|
||||||
}
|
}
|
||||||
anim();
|
anim();
|
||||||
Bangle.on("touch", exports.hide);
|
Bangle.on("touch", exports.hide);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
options = {
|
options = {
|
||||||
|
@ -152,4 +159,4 @@ exports.hide = function(options) {
|
||||||
if (pos < 0) setTimeout(anim, 10);
|
if (pos < 0) setTimeout(anim, 10);
|
||||||
}
|
}
|
||||||
anim();
|
anim();
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
0.01: First version of the Walkers Clock
|
0.01: First version of the Walkers Clock
|
||||||
0.02: Fixed screen flicker
|
0.02: Fixed screen flicker
|
||||||
|
0.03: Added display of GPS fix lat/lon and course
|
|
@ -34,8 +34,10 @@ const GPS_SATS = "gps_sats";
|
||||||
const GPS_RUNNING = "gps_running";
|
const GPS_RUNNING = "gps_running";
|
||||||
|
|
||||||
const GDISP_OS = "g_osref";
|
const GDISP_OS = "g_osref";
|
||||||
|
const GDISP_LATLN = "g_latln";
|
||||||
const GDISP_SPEED = "g_speed";
|
const GDISP_SPEED = "g_speed";
|
||||||
const GDISP_ALT = "g_alt";
|
const GDISP_ALT = "g_alt";
|
||||||
|
const GDISP_COURSE = "g_course";
|
||||||
|
|
||||||
const Y_TIME = 40;
|
const Y_TIME = 40;
|
||||||
const Y_ACTIVITY = 120;
|
const Y_ACTIVITY = 120;
|
||||||
|
@ -138,14 +140,21 @@ function drawActivity() {
|
||||||
case GDISP_OS:
|
case GDISP_OS:
|
||||||
activityStr = ref;
|
activityStr = ref;
|
||||||
break;
|
break;
|
||||||
|
case GDISP_LATLN:
|
||||||
|
g.setFontVector(26);
|
||||||
|
activityStr = last_fix.lat.toFixed(4) + ", " + last_fix.lon.toFixed(4);
|
||||||
|
break;
|
||||||
case GDISP_SPEED:
|
case GDISP_SPEED:
|
||||||
speed = last_fix.speed;
|
speed = last_fix.speed;
|
||||||
speed = speed.toFixed(1);
|
speed = speed.toFixed(1);
|
||||||
activityStr = speed + "kph"
|
activityStr = speed + "kph";
|
||||||
break;
|
break;
|
||||||
case GDISP_ALT:
|
case GDISP_ALT:
|
||||||
activityStr = last_fix.alt + "m";
|
activityStr = last_fix.alt + "m";
|
||||||
break;
|
break;
|
||||||
|
case GDISP_COURSE:
|
||||||
|
activityStr = last_fix.course;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1);
|
g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1);
|
||||||
|
@ -203,12 +212,18 @@ function drawInfo() {
|
||||||
case GDISP_OS:
|
case GDISP_OS:
|
||||||
str = "GPS: Grid";
|
str = "GPS: Grid";
|
||||||
break;
|
break;
|
||||||
|
case GDISP_LATLN:
|
||||||
|
str = "GPS: Lat,Lon";
|
||||||
|
break;
|
||||||
case GDISP_SPEED:
|
case GDISP_SPEED:
|
||||||
str = "GPS: Speed";
|
str = "GPS: Speed";
|
||||||
break;
|
break;
|
||||||
case GDISP_ALT:
|
case GDISP_ALT:
|
||||||
str = "GPS: Alt";
|
str = "GPS: Alt";
|
||||||
break;
|
break;
|
||||||
|
case GDISP_COURSE:
|
||||||
|
str = "GPS: Course";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
drawModeLine(str,col);
|
drawModeLine(str,col);
|
||||||
return;
|
return;
|
||||||
|
@ -280,6 +295,12 @@ function changeInfoMode() {
|
||||||
gpsDisplay = GDISP_ALT;
|
gpsDisplay = GDISP_ALT;
|
||||||
break;
|
break;
|
||||||
case GDISP_ALT:
|
case GDISP_ALT:
|
||||||
|
gpsDisplay = GDISP_COURSE;
|
||||||
|
break;
|
||||||
|
case GDISP_COURSE:
|
||||||
|
gpsDisplay = GDISP_LATLN;
|
||||||
|
break;
|
||||||
|
case GDISP_LATLN:
|
||||||
default:
|
default:
|
||||||
gpsDisplay = GDISP_OS;
|
gpsDisplay = GDISP_OS;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
# Waypointer - navigate to waypoints
|
||||||
|
|
||||||
|
The app is aimed at navigation whilst walking. Please note that it
|
||||||
|
would be foolish in the extreme to rely on this as your only
|
||||||
|
navigation aid!
|
||||||
|
|
||||||
|
Please refer to the section on calibration of the compass. This
|
||||||
|
should be done each time the app is going to be used.
|
||||||
|
|
||||||
|
The main part of the display is a compass arrow that points in the
|
||||||
|
direction you need to walk in. Once you have selected a waypoint a
|
||||||
|
bearing from your current position (received from a GPS fix) is
|
||||||
|
calculated and the compass is set to point in that direction. If the
|
||||||
|
arrow is pointing to the left, turning left should straighten the arrow
|
||||||
|
up so that it is pointing straight ahead.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The large digits are the bearing from the current position. On the
|
||||||
|
left is the distance to the waypoint in local units. The top of the
|
||||||
|
display is a circular compass which displays the direction you will
|
||||||
|
need to travel in to reach the selected waypoint. The blue text is
|
||||||
|
the name of the current waypoint. NONE means that there is no
|
||||||
|
waypoint set and so bearing and distance will remain at 0. To select
|
||||||
|
a waypoint, press BTN2 (middle) and wait for the blue text to turn
|
||||||
|
white. Then use BTN1 and BTN3 to select a waypoint. The waypoint
|
||||||
|
choice is fixed by pressing BTN2 again. In the screen shot below a
|
||||||
|
waypoint giving the location of Stone Henge has been selected.
|
||||||
|
|
||||||
|
The screenshot above shows that Stone Henge is 259.9 miles from the
|
||||||
|
current location. To travel towards Stone Henge I need to turn
|
||||||
|
slightly left until the arrow is pointing straight ahead. As you
|
||||||
|
continue to walk in the pointed direction you should see the distance
|
||||||
|
to the waypoint reduce. The frequency of updates will depend on
|
||||||
|
which settings you have used in the GPS.
|
||||||
|
|
||||||
|
At the top of the screen you can see two widgets. These are the [GPS
|
||||||
|
Power
|
||||||
|
Widget](https://github.com/espruino/BangleApps/tree/master/apps/widgps)
|
||||||
|
and the [Compass Power Indicator Widget]. These can be installed
|
||||||
|
seperately and provide you a indication of when the GPS and Compass
|
||||||
|
are switched on and drawing power.
|
||||||
|
|
||||||
|
|
||||||
|
## Marking Waypoints
|
||||||
|
|
||||||
|
The app lets you mark your current location as follows. There are
|
||||||
|
vacant slots in the waypoint file which can be allocated a
|
||||||
|
location. In the distributed waypoint file these are labelled WP0 to
|
||||||
|
WP4. Select one of these - WP2 is shown below.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Bearing and distance are both zero as WP2 has currently no GPS
|
||||||
|
location associated with it. To mark the location, press BTN2.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The app indicates that WP2 is now marked by adding the prefix @ to
|
||||||
|
it's name. The distance should be small as shown in the screen shot
|
||||||
|
as you have just marked your current location.
|
||||||
|
|
||||||
|
## Waypoint JSON file
|
||||||
|
|
||||||
|
When the app is loaded from the app loader, a file named
|
||||||
|
`waypoints.json` is loaded along with the javascript etc. The file
|
||||||
|
has the following contents:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name":"NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"No10",
|
||||||
|
"lat":51.5032,
|
||||||
|
"lon":-0.1269
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Stone",
|
||||||
|
"lat":51.1788,
|
||||||
|
"lon":-1.8260
|
||||||
|
},
|
||||||
|
{ "name":"WP0" },
|
||||||
|
{ "name":"WP1" },
|
||||||
|
{ "name":"WP2" },
|
||||||
|
{ "name":"WP3" },
|
||||||
|
{ "name":"WP4" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The file contains the initial NONE waypoint which is useful if you
|
||||||
|
just want to display course and speed. The next two entries are
|
||||||
|
waypoints to No 10 Downing Street and to Stone Henge - obtained from
|
||||||
|
Google Maps. The last five entries are entries which can be *marked*.
|
||||||
|
|
||||||
|
You add and delete entries using the Web IDE to load and then save
|
||||||
|
the file from and to watch storage. The app itself does not limit the
|
||||||
|
number of entries although it does load the entire file into RAM
|
||||||
|
which will obviously limit this.
|
||||||
|
|
||||||
|
|
||||||
|
## Waypoint Editor
|
||||||
|
|
||||||
|
Clicking on the download icon of gpsnav in the app loader invokes the
|
||||||
|
waypoint editor. The editor downloads and displays the current
|
||||||
|
`waypoints.json` file. Clicking the `Edit` button beside an entry
|
||||||
|
causes the entry to be deleted from the list and displayed in the
|
||||||
|
edit boxes. It can be restored - by clicking the `Add waypoint`
|
||||||
|
button. A new markable entry is created by using the `Add name`
|
||||||
|
button. The edited `waypoints.json` file is uploaded to the Bangle by
|
||||||
|
clicking the `Upload` button.
|
||||||
|
|
||||||
|
|
||||||
|
## Calibration of the Compass
|
||||||
|
|
||||||
|
The Compass should be calibrated before using the App to navigate to
|
||||||
|
a waypoint (or a series of waypoints). To do this use either the
|
||||||
|
Arrow Compass or the [Navigation
|
||||||
|
Compass](https://github.com/espruino/BangleApps/tree/master/apps/magnav).
|
||||||
|
Open the compass app and clicking on BTN3. The calibration process
|
||||||
|
takes 30 seconds during which you should move the watch slowly
|
||||||
|
through figures of 8. It is important that during calibration the
|
||||||
|
watch is fully rotated around each of it axes. If the app does give
|
||||||
|
the correct direction heading or is not stable with respect to tilt
|
||||||
|
and roll - redo the calibration by pressing *BTN3*. Calibration data
|
||||||
|
is recorded in a storage file named `magnav.json`.
|
||||||
|
|
||||||
|
|
||||||
|
## Advantages and Disadvantages
|
||||||
|
|
||||||
|
This approach has some advantages and disadvantages. First following
|
||||||
|
the arrow is fairly easy to do and once the bearing has been
|
||||||
|
established it does not matter if there is not another GPS fix for a
|
||||||
|
while as the compass will continue to point in the general direction.
|
||||||
|
Second the GPS will only supply a course to the waypoint (a bearing)
|
||||||
|
once you are travelling above 8m/s or 28kph. This is not a practical
|
||||||
|
walking speed. 5kmph is considered a marching pace.
|
||||||
|
|
||||||
|
One disadvantage is that the compass is not very accurate. I have
|
||||||
|
observed it being 20-30 degrees off when compared to a hiking
|
||||||
|
compass. Sometime its is necessary to walk in the opposite direction
|
||||||
|
for a bit to establish the correct direction to go in. The accuracy
|
||||||
|
of the compass is impacted by the magnetic clamps on the charging
|
||||||
|
cable, so it is particularly important that you recalibtrate the
|
||||||
|
compass after the watch has been charged. That said I have found I
|
||||||
|
am successfully able to follow a chain of waypoints as a route.
|
||||||
|
|
||||||
|
|
||||||
|
## Possible Future Enhancements
|
||||||
|
|
||||||
|
- Buzz when the GPS establishes its first fix.
|
||||||
|
|
||||||
|
- Add a small LED to show the status of the GPS during the phase of
|
||||||
|
establishing a first fix.
|
||||||
|
|
||||||
|
- Add an option to calibrate the Compass without having to use the
|
||||||
|
Arrow Compass or the Navigation Compass.
|
||||||
|
|
||||||
|
- Investigate the accuracy of the Compass and how it changes
|
||||||
|
throughout the day after the watch battery has been fully charged.
|
||||||
|
|
||||||
|
- Investigate the possibility of setting the GPS in low speed mode so
|
||||||
|
that a current course value can be obtained.
|
||||||
|
|
||||||
|
- Buzz when you arrive within 20m of a waypoint to signify arrival
|
||||||
|
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
The majority of the code in this application is a merge of
|
||||||
|
[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS
|
||||||
|
Navigation and Compass Navigation Applications.
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
var pal_by = new Uint16Array([0x0000,0xFFC0],0,1); // black, yellow
|
||||||
|
var pal_bw = new Uint16Array([0x0000,0xffff],0,1); // black, white
|
||||||
|
var pal_bb = new Uint16Array([0x0000,0x07ff],0,1); // black, blue
|
||||||
|
|
||||||
|
// having 3 2 color pallette keeps the memory requirement lower
|
||||||
|
var buf1 = Graphics.createArrayBuffer(160,160,1, {msb:true});
|
||||||
|
var buf2 = Graphics.createArrayBuffer(80,40,1, {msb:true});
|
||||||
|
var arrow_img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo="));
|
||||||
|
|
||||||
|
function flip1(x,y) {
|
||||||
|
g.drawImage({width:160,height:160,bpp:1,buffer:buf1.buffer, palette:pal_by},x,y);
|
||||||
|
buf1.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip2_bw(x,y) {
|
||||||
|
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bw},x,y);
|
||||||
|
buf2.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flip2_bb(x,y) {
|
||||||
|
g.drawImage({width:80,height:40,bpp:1,buffer:buf2.buffer, palette:pal_bb},x,y);
|
||||||
|
buf2.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
var candraw = true;
|
||||||
|
var wp_bearing = 0;
|
||||||
|
var direction = 0;
|
||||||
|
var wpindex=0;
|
||||||
|
var loc = require("locale");
|
||||||
|
var selected = false;
|
||||||
|
|
||||||
|
var previous = {
|
||||||
|
bs: '',
|
||||||
|
dst: '',
|
||||||
|
wp_name: '',
|
||||||
|
course: 0,
|
||||||
|
selected: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// clear the attributes that control the display refresh
|
||||||
|
function clear_previous() {
|
||||||
|
previous.bs = '-';
|
||||||
|
previous.dst = '-';
|
||||||
|
previous.wp_name = '-';
|
||||||
|
previous.course = -999;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawCompass(course) {
|
||||||
|
if(!candraw) return;
|
||||||
|
if (Math.abs(previous.course - course) < 9) return; // reduce number of draws due to compass jitter
|
||||||
|
previous.course = course;
|
||||||
|
|
||||||
|
buf1.setColor(1);
|
||||||
|
buf1.fillCircle(80,80,79,79);
|
||||||
|
buf1.setColor(0);
|
||||||
|
buf1.fillCircle(80,80,69,69);
|
||||||
|
buf1.setColor(1);
|
||||||
|
buf1.drawImage(arrow_img, 80, 80, {scale:3, rotate:radians(course)} );
|
||||||
|
flip1(40, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***** COMPASS CODE ***********/
|
||||||
|
|
||||||
|
var heading = 0;
|
||||||
|
function newHeading(m,h){
|
||||||
|
var s = Math.abs(m - h);
|
||||||
|
var delta = (m>h)?1:-1;
|
||||||
|
if (s>=180){s=360-s; delta = -delta;}
|
||||||
|
if (s<2) return h;
|
||||||
|
var hd = h + delta*(1 + Math.round(s/5));
|
||||||
|
if (hd<0) hd+=360;
|
||||||
|
if (hd>360)hd-= 360;
|
||||||
|
return hd;
|
||||||
|
}
|
||||||
|
|
||||||
|
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
|
||||||
|
|
||||||
|
function tiltfixread(O,S){
|
||||||
|
var start = Date.now();
|
||||||
|
var m = Bangle.getCompass();
|
||||||
|
var g = Bangle.getAccel();
|
||||||
|
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
|
||||||
|
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
|
||||||
|
if (d<0) d+=360;
|
||||||
|
var phi = Math.atan(-g.x/-g.z);
|
||||||
|
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
|
||||||
|
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
|
||||||
|
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
|
||||||
|
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
|
||||||
|
var yh = m.dz*sinphi - m.dx*cosphi;
|
||||||
|
var psi = Math.atan2(yh,xh)*180/Math.PI;
|
||||||
|
if (psi<0) psi+=360;
|
||||||
|
return psi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note actual mag is 360-m, error in firmware
|
||||||
|
function read_compass() {
|
||||||
|
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
|
||||||
|
heading = newHeading(d,heading);
|
||||||
|
direction = wp_bearing - heading;
|
||||||
|
if (direction < 0) direction += 360;
|
||||||
|
if (direction > 360) direction -= 360;
|
||||||
|
drawCompass(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***** END Compass ***********/
|
||||||
|
|
||||||
|
var speed = 0;
|
||||||
|
var satellites = 0;
|
||||||
|
var wp;
|
||||||
|
var dist=0;
|
||||||
|
|
||||||
|
function radians(a) {
|
||||||
|
return a*Math.PI/180;
|
||||||
|
}
|
||||||
|
|
||||||
|
function degrees(a) {
|
||||||
|
var d = a*180/Math.PI;
|
||||||
|
return (d+360)%360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearing(a,b){
|
||||||
|
var delta = radians(b.lon-a.lon);
|
||||||
|
var alat = radians(a.lat);
|
||||||
|
var blat = radians(b.lat);
|
||||||
|
var y = Math.sin(delta) * Math.cos(blat);
|
||||||
|
var x = Math.cos(alat)*Math.sin(blat) -
|
||||||
|
Math.sin(alat)*Math.cos(blat)*Math.cos(delta);
|
||||||
|
return Math.round(degrees(Math.atan2(y, x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(a,b){
|
||||||
|
var x = radians(a.lon-b.lon) * Math.cos(radians((a.lat+b.lat)/2));
|
||||||
|
var y = radians(b.lat-a.lat);
|
||||||
|
return Math.round(Math.sqrt(x*x + y*y) * 6371000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function drawN(){
|
||||||
|
buf2.setFont("Vector",24);
|
||||||
|
var bs = wp_bearing.toString();
|
||||||
|
bs = wp_bearing<10?"00"+bs : wp_bearing<100 ?"0"+bs : bs;
|
||||||
|
var dst = loc.distance(dist);
|
||||||
|
|
||||||
|
// -1=left (default), 0=center, 1=right
|
||||||
|
|
||||||
|
// show distance on the left
|
||||||
|
if (previous.dst !== dst) {
|
||||||
|
previous.dst = dst
|
||||||
|
buf2.setColor(1);
|
||||||
|
buf2.setFontAlign(-1,-1);
|
||||||
|
buf2.setFont("Vector", 20);
|
||||||
|
buf2.drawString(dst,0,0);
|
||||||
|
flip2_bw(0, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bearing, place in middle at bottom of compass
|
||||||
|
if (previous.bs !== bs) {
|
||||||
|
previous.bs = bs;
|
||||||
|
buf2.setColor(1);
|
||||||
|
buf2.setFontAlign(0, -1);
|
||||||
|
buf2.setFont("Vector",38);
|
||||||
|
buf2.drawString(bs,40,0);
|
||||||
|
flip2_bw(80, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// waypoint name on right
|
||||||
|
if (previous.wp_name !== wp.name || previous.selected !== selected) {
|
||||||
|
previous.selected = selected;
|
||||||
|
buf2.setColor(1);
|
||||||
|
buf2.setFontAlign(1,-1); // right, bottom
|
||||||
|
buf2.setFont("Vector", 20);
|
||||||
|
buf2.drawString(wp.name, 80, 0);
|
||||||
|
|
||||||
|
if (selected)
|
||||||
|
flip2_bw(160, 200);
|
||||||
|
else
|
||||||
|
flip2_bb(160, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedfix;
|
||||||
|
|
||||||
|
function onGPS(fix) {
|
||||||
|
savedfix = fix;
|
||||||
|
if (fix!==undefined){
|
||||||
|
satellites = fix.satellites;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candraw) {
|
||||||
|
if (fix!==undefined && fix.fix==1){
|
||||||
|
dist = distance(fix,wp);
|
||||||
|
if (isNaN(dist)) dist = 0;
|
||||||
|
wp_bearing = bearing(fix,wp);
|
||||||
|
if (isNaN(wp_bearing)) wp_bearing = 0;
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var intervalRef;
|
||||||
|
|
||||||
|
function stopdraw() {
|
||||||
|
candraw=false;
|
||||||
|
prev_course = -1;
|
||||||
|
if(intervalRef) {clearInterval(intervalRef);}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTimers() {
|
||||||
|
candraw=true;
|
||||||
|
intervalRefSec = setInterval(function() {
|
||||||
|
read_compass();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawAll(){
|
||||||
|
g.setColor(1,1,1);
|
||||||
|
drawN();
|
||||||
|
drawCompass(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startdraw(){
|
||||||
|
g.clear();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
startTimers();
|
||||||
|
candraw=true;
|
||||||
|
drawAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setButtons(){
|
||||||
|
setWatch(nextwp.bind(null,-1), BTN1, {repeat:true,edge:"falling"});
|
||||||
|
setWatch(doselect, BTN2, {repeat:true,edge:"falling"});
|
||||||
|
setWatch(nextwp.bind(null,1), BTN3, {repeat:true,edge:"falling"});
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on('lcdPower',function(on) {
|
||||||
|
if (on) {
|
||||||
|
clear_previous();
|
||||||
|
startdraw();
|
||||||
|
} else {
|
||||||
|
stopdraw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}];
|
||||||
|
wp=waypoints[0];
|
||||||
|
|
||||||
|
function nextwp(inc){
|
||||||
|
if (!selected) return;
|
||||||
|
wpindex+=inc;
|
||||||
|
if (wpindex>=waypoints.length) wpindex=0;
|
||||||
|
if (wpindex<0) wpindex = waypoints.length-1;
|
||||||
|
wp = waypoints[wpindex];
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doselect(){
|
||||||
|
if (selected && wpindex!=0 && waypoints[wpindex].lat===undefined && savedfix.fix) {
|
||||||
|
waypoints[wpindex] ={name:"@"+wp.name, lat:savedfix.lat, lon:savedfix.lon};
|
||||||
|
wp = waypoints[wpindex];
|
||||||
|
require("Storage").writeJSON("waypoints.json", waypoints);
|
||||||
|
}
|
||||||
|
selected=!selected;
|
||||||
|
drawN();
|
||||||
|
}
|
||||||
|
|
||||||
|
Bangle.on('kill',()=>{
|
||||||
|
Bangle.setCompassPower(0);
|
||||||
|
Bangle.setGPSPower(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
Bangle.setLCDBrightness(1);
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
// load widgets can turn off GPS
|
||||||
|
Bangle.setGPSPower(1);
|
||||||
|
Bangle.setCompassPower(1);
|
||||||
|
drawAll();
|
||||||
|
startTimers();
|
||||||
|
Bangle.on('GPS', onGPS);
|
||||||
|
setButtons();
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwwhC/AFcBiAWViMRDCkBiUhC68RC64AFGxsRC4UiAAY2HOAQAEC4MSn//AAXzGAwWGC4czC4f/mIwEFwIlEBoIXDBQnyGAkRiYWE/8yLAIXBGAhgEFw5WBC4R0BkYaBmRfFF44XCNI6OGGAQlBAAIXIX4yPJaBq/JC5oeHC/4X/C/4X/C/4X/C/4X/C88RiIXUDAIWVAH4AVA="))
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 74 KiB |
|
@ -0,0 +1,170 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||||
|
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h4>List of waypoints</h4>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Lat.</th>
|
||||||
|
<th>Long.</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="waypoints">
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<h4>Add a new waypoint</h4>
|
||||||
|
<form id="add_waypoint_form">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-3 col-xs-8">
|
||||||
|
<input class="form-input input-sm" type="text" id="add_waypoint_name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="column col-3 col-xs-8">
|
||||||
|
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_latitude" placeholder="Lat">
|
||||||
|
</div>
|
||||||
|
<div class="column col-3 col-xs-8">
|
||||||
|
<input class="form-input input-sm" value="0.0000" type="number" step="any" id="add_longtitude" placeholder="Long">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column col-3 col-xs-8">
|
||||||
|
<button id="add_name_button" class="btn btn-primary btn-sm">Add Name Only</button>
|
||||||
|
</div>
|
||||||
|
<div class="column col-3 col-xs-8">
|
||||||
|
<button id="add_waypoint_button" class="btn btn-primary btn-sm">Add Waypoint</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
|
<button id="Download" class="btn btn-error">Reload</button> <button id="Upload" class="btn btn-primary">Upload</button>
|
||||||
|
|
||||||
|
<script src="../../core/lib/interface.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var waypoints = []
|
||||||
|
|
||||||
|
var $name = document.getElementById('add_waypoint_name')
|
||||||
|
var $form = document.getElementById('add_waypoint_form')
|
||||||
|
var $button = document.getElementById('add_waypoint_button')
|
||||||
|
var $name_button = document.getElementById('add_name_button')
|
||||||
|
var $latitude = document.getElementById('add_latitude')
|
||||||
|
var $longtitude = document.getElementById('add_longtitude')
|
||||||
|
var $list = document.getElementById('waypoints')
|
||||||
|
|
||||||
|
function compare(a, b){
|
||||||
|
var x = a.name.toLowerCase();
|
||||||
|
var y = b.name.toLowerCase();
|
||||||
|
if (x=="none") {return -1};
|
||||||
|
if (y=="none") {return 1};
|
||||||
|
if (x < y) {return -1;}
|
||||||
|
if (x > y) {return 1;}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$button.addEventListener('click', event => {
|
||||||
|
event.preventDefault()
|
||||||
|
var name = $name.value.trim()
|
||||||
|
if(!name) return;
|
||||||
|
var lat = parseFloat($latitude.value).toPrecision(5);
|
||||||
|
var lon = parseFloat($longtitude.value).toPrecision(5);
|
||||||
|
|
||||||
|
waypoints.push({
|
||||||
|
name, lat,lon,
|
||||||
|
});
|
||||||
|
|
||||||
|
waypoints.sort(compare);
|
||||||
|
|
||||||
|
renderWaypoints()
|
||||||
|
$name.value = ''
|
||||||
|
$latitude.value = (0).toPrecision(5);
|
||||||
|
$longtitude.value = (0).toPrecision(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
$name_button.addEventListener('click', event => {
|
||||||
|
event.preventDefault()
|
||||||
|
var name = $name.value.trim()
|
||||||
|
if(!name) return;
|
||||||
|
|
||||||
|
waypoints.push({
|
||||||
|
name
|
||||||
|
});
|
||||||
|
waypoints.sort(compare);
|
||||||
|
|
||||||
|
renderWaypoints()
|
||||||
|
$name.value = ''
|
||||||
|
$latitude.value = 0.0000
|
||||||
|
$longtitude.value = 0.0000
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function removeWaypoint(index){
|
||||||
|
$name.value = waypoints[index].name
|
||||||
|
$latitude.value = waypoints[index].lat
|
||||||
|
$longtitude.value = waypoints[index].lon
|
||||||
|
waypoints = waypoints.filter((p,i) => i!==index)
|
||||||
|
renderWaypoints()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWaypoints(){
|
||||||
|
$list.innerHTML = ''
|
||||||
|
waypoints.forEach((waypoint,index) => {
|
||||||
|
var $waypoint = document.createElement('tr')
|
||||||
|
if (index==0){
|
||||||
|
$waypoint.innerHTML = `<td>${waypoint.name}</td>`
|
||||||
|
} else if(waypoint.lat==undefined){
|
||||||
|
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>------</td><td>-----</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
|
||||||
|
} else {
|
||||||
|
$waypoint.innerHTML = `<td>${waypoint.name}</td><td>${waypoint.lat}</td><td>${waypoint.lon}</td><td><button class="btn btn-action btn-primary" onclick="removeWaypoint(${index})"><i class="icon icon-edit"></i></button></td>`
|
||||||
|
}
|
||||||
|
$list.appendChild($waypoint)
|
||||||
|
})
|
||||||
|
$name.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadJSONfile(fileid, callback) {
|
||||||
|
Puck.write(`\x10(function() {
|
||||||
|
var pts = require("Storage").readJSON("${fileid}")||[{name:"NONE"}];
|
||||||
|
Bluetooth.print(JSON.stringify(pts));
|
||||||
|
})()\n`,contents=>{
|
||||||
|
var storedpts = JSON.parse(contents);
|
||||||
|
callback(storedpts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadFile(fileid, contents) {
|
||||||
|
Puck.write(`\x10(function() {
|
||||||
|
require("Storage").write("${fileid}",'${contents}');
|
||||||
|
Bluetooth.print("OK");
|
||||||
|
})()\n`,ret=>{
|
||||||
|
console.log("uploadFile",ret);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotStored(pts){
|
||||||
|
waypoints = pts;
|
||||||
|
renderWaypoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInit() {
|
||||||
|
downloadJSONfile("waypoints.json", gotStored);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("Download").addEventListener("click", function() {
|
||||||
|
downloadJSONfile("waypoints.json", gotStored);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("Upload").addEventListener("click", function() {
|
||||||
|
var data = JSON.stringify(waypoints);
|
||||||
|
uploadFile("waypoints.json",data);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,20 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name":"NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"No10",
|
||||||
|
"lat":51.5032,
|
||||||
|
"lon":-0.1269
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Stone",
|
||||||
|
"lat":51.1788,
|
||||||
|
"lon":-1.8260
|
||||||
|
},
|
||||||
|
{ "name":"WP0" },
|
||||||
|
{ "name":"WP1" },
|
||||||
|
{ "name":"WP2" },
|
||||||
|
{ "name":"WP3" },
|
||||||
|
{ "name":"WP4" }
|
||||||
|
]
|
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 73 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
# Compass Power Status Widget
|
||||||
|
|
||||||
|
A simple widget that shows the on/off status of the compass.
|
||||||
|
|
||||||
|
The compass draws around 1mA when on. Whilst this is not a big draw
|
||||||
|
on the battery it is still easy to have it switched on and not be
|
||||||
|
aware.
|
||||||
|
|
||||||
|
- Uses Bangle.isCompassOn(), requires firmware v2.08.167 or later
|
||||||
|
- Shows in grey when the compass is off
|
||||||
|
- Shows in amber when the compass is on
|
|
@ -0,0 +1,30 @@
|
||||||
|
(function(){
|
||||||
|
//var img = E.toArrayBuffer(atob("FBSBAAAAAAAAA/wAf+AP/wH/2D/zw/w8PwfD9nw+b8Pg/Dw/w8/8G/+A//AH/gA/wAAAAAAA"));
|
||||||
|
//var img = E.toArrayBuffer(atob("GBiBAAB+AAP/wAeB4A4AcBgAGDAADHAADmABhmAHhsAfA8A/A8BmA8BmA8D8A8D4A2HgBmGABnAADjAADBgAGA4AcAeB4AP/wAB+AA=="));
|
||||||
|
var img = E.toArrayBuffer(atob("FBSBAAH4AH/gHAODgBwwAMYABkAMLAPDwPg8CYPBkDwfA8PANDACYABjAAw4AcHAOAf+AB+A"));
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
g.reset();
|
||||||
|
if (Bangle.isCompassOn()) {
|
||||||
|
g.setColor(1,0.8,0); // on = amber
|
||||||
|
} else {
|
||||||
|
g.setColor(0.3,0.3,0.3); // off = grey
|
||||||
|
}
|
||||||
|
g.drawImage(img, 10+this.x, 2+this.var);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timerInterval;
|
||||||
|
Bangle.on('lcdPower', function(on) {
|
||||||
|
if (on) {
|
||||||
|
WIDGETS.compass.draw();
|
||||||
|
if (!timerInterval) timerInterval = setInterval(()=>WIDGETS.compass.draw(), 2000);
|
||||||
|
} else {
|
||||||
|
if (timerInterval) {
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
timerInterval = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
WIDGETS.compass={area:"tr",width:24,draw:draw};
|
||||||
|
})();
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -229,7 +229,7 @@ while(fileA=allFiles.pop()) {
|
||||||
if (globA.test(nameB)||globB.test(nameA)) {
|
if (globA.test(nameB)||globB.test(nameA)) {
|
||||||
if (isGlob(nameA)||isGlob(nameB))
|
if (isGlob(nameA)||isGlob(nameB))
|
||||||
ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`)
|
ERROR(`App ${fileB.app} ${typeB} file ${nameB} matches app ${fileA.app} ${typeB} file ${nameA}`)
|
||||||
else ERROR(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
|
else WARN(`App ${fileB.app} ${typeB} file ${nameB} is also listed as ${typeA} file for app ${fileA.app}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|