Merge pull request #4 from espruino/master

update from upstream
pull/683/head
dapgo 2021-03-07 19:29:44 +01:00 committed by GitHub
commit e0084e6333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1324 additions and 94 deletions

View File

@ -80,7 +80,7 @@
"name": "Notifications (default)",
"shortName":"Notifications",
"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",
"tags": "widget",
"type": "notify",
@ -139,7 +139,7 @@
{ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
"version":"0.20",
"version":"0.21",
"description": "The default notification handler for Gadgetbridge notifications from Android",
"tags": "tool,system,android,widget",
"readme": "README.md",
@ -584,7 +584,7 @@
{ "id": "hrm",
"name": "Heart Rate Monitor",
"icon": "heartrate.png",
"version":"0.02",
"version":"0.03",
"description": "Measure your heart rate and see live sensor data",
"tags": "health",
"storage": [
@ -1590,7 +1590,7 @@
"name": "BangleRun",
"shortName": "BangleRun",
"icon": "banglerun.png",
"version": "0.09",
"version": "0.10",
"interface": "interface.html",
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
"tags": "run,running,fitness,outdoors",
@ -2822,8 +2822,8 @@
"name": "Walkers Clock",
"shortName":"Walkers Clock",
"icon": "walkersclock48.png",
"version":"0.02",
"description": "A larg font watch, displays steps, can switch GPS on/off, displays grid reference",
"version":"0.03",
"description": "A large font watch, displays steps, can switch GPS on/off, displays grid reference",
"type":"clock",
"tags": "clock, gps, tools, outdoors",
"readme": "README.md",
@ -2856,6 +2856,18 @@
{"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",
"name": "hello,world!",
"shortName":"helloworld",
@ -2868,5 +2880,44 @@
{"name":"helloworld.app.js","url":"app.js"},
{"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}
]
}
]

41
apps/arrow/README.md Normal file
View File

@ -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.*
![](arrow_screenshot.jpg)
## 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)

179
apps/arrow/app.js Normal file
View File

@ -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();

BIN
apps/arrow/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

1
apps/arrow/icon.js Normal file
View File

@ -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="))

View File

@ -7,3 +7,7 @@
0.07: Fixed GPS update, added guards against NaN values
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.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

View File

@ -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"})}();

View File

@ -1,4 +1,4 @@
import { AppState } from './state';
import { ActivityStatus, AppState } from './state';
declare var Bangle: any;
declare var g: any;
@ -26,11 +26,11 @@ function drawBackground(): void {
g.setFont('6x8', 2);
g.setFontAlign(0, -1, 0);
g.drawString('DIST (KM)', 60, 32);
g.drawString('TIME', 180, 32);
g.drawString('TIME', 172, 32);
g.drawString('PACE', 60, 92);
g.drawString('HEART', 180, 92);
g.drawString('HEART', 172, 92);
g.drawString('STEPS', 60, 152);
g.drawString('CADENCE', 180, 152);
g.drawString('CADENCE', 172, 152);
}
function drawValue(value: string, x: number, y: number) {
@ -45,11 +45,11 @@ function draw(state: AppState): void {
g.setFontAlign(0, -1, 0);
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(state.hr.toFixed(0), 180, 115);
drawValue(state.hr.toFixed(0), 172, 115);
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);
@ -64,9 +64,18 @@ function draw(state: AppState): void {
g.drawString(formatClock(new Date()), 120, 220);
g.setColor(STATUS_COLORS[state.status]);
g.fillRect(160, 216, 240, 240);
g.fillRect(160, 216, 230, 240);
g.setColor(0x0000);
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) {

View File

@ -14,8 +14,6 @@ interface GpsEvent {
}
const EARTH_RADIUS = 6371008.8;
const POS_ACCURACY = 2.5;
const VEL_ACCURACY = 0.05;
function initGps(state: AppState): void {
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.fix = gps.fix;
state.dop = gps.hdop;
state.gpsValid = state.fix > 0 && state.dop <= 5;
state.gpsValid = state.fix > 0;
updateGps(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);
}
}
@ -44,9 +46,8 @@ function updateGps(state: AppState): void {
const t = Date.now();
let dt = (t - state.t) / 1000;
if (!isFinite(dt)) dt=0;
state.t = t;
state.dt += dt;
state.timeSinceLog += dt;
if (state.status === ActivityStatus.Running) {
state.duration += dt;
@ -62,52 +63,25 @@ function updateGps(state: AppState): void {
const x = r * Math.cos(lat) * Math.cos(lon);
const y = r * Math.cos(lat) * Math.sin(lon);
const z = r * Math.sin(lat);
const v = state.vel;
if (!state.x) {
state.x = x;
state.y = y;
state.z = z;
state.v = v;
state.pError = state.dop * POS_ACCURACY;
state.vError = state.dop * VEL_ACCURACY;
return;
}
const dx = x - state.x;
const dy = y - state.y;
const dz = z - state.z;
const dv = v - state.v;
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
const dvMag = Math.abs(dv);
state.pError += state.v * state.dt;
state.dt = 0;
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;*/
state.x = x;
state.y = y;
state.z = z;
if (state.status === ActivityStatus.Running) {
state.distance += dpMag * pGain;
state.distance += dpMag;
state.speed = (state.distance / state.duration) || 0;
state.cadence = (60 * state.steps / state.duration) || 0;
}

View File

@ -8,19 +8,23 @@ function initLog(state: AppState): void {
const time = datetime.substr(9, 6);
const filename = `banglerun_${date}_${time}`;
state.file = require('Storage').open(filename, 'w');
state.file.write([
'timestamp',
'latitude',
'longitude',
'altitude',
'duration',
'distance',
'heartrate',
'steps',
].join(',') + '\n');
state.fileWritten = false;
}
function updateLog(state: AppState): void {
if (!state.fileWritten) {
state.file.write([
'timestamp',
'latitude',
'longitude',
'altitude',
'duration',
'distance',
'heartrate',
'steps',
].join(',') + '\n');
state.fileWritten = true;
}
state.file.write([
Date.now().toFixed(0),
state.lat.toFixed(6),

View File

@ -14,15 +14,14 @@ interface AppState {
dop: number;
gpsValid: boolean;
// GPS Kalman data
// Absolute position data
x: number;
y: number;
z: number;
v: number;
// Last fix time
t: number;
dt: number;
pError: number;
vError: number;
// Last time we saved log info
timeSinceLog : number;
// HRM data
hr: number,
@ -30,6 +29,7 @@ interface AppState {
// Logger data
file: File;
fileWritten: boolean;
// Drawing data
drawing: boolean;
@ -62,16 +62,14 @@ function initState(): AppState {
x: NaN,
y: NaN,
z: NaN,
v: NaN,
t: NaN,
dt: NaN,
pError: NaN,
vError: NaN,
timeSinceLog : 0,
hr: 60,
hrError: 100,
file: null,
fileWritten: false,
drawing: false,

View File

@ -0,0 +1 @@
0.01: Initial version

View File

@ -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/).

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwgMJhvd7tEogkSC4lAC6xWUgUgNysiC6hGBL58BiMQLoYXDMJkRAAIWEC4gYJFwIABCwgXFDBAWCpoXLMYwuCigUD6czmUSmQYKC4QuD6f//8xiUzmckJJIuECwQXEDAgXGFwc/C4XykYXCmZIJCwVPCwQABCwczmgwHXYXUCwgXFGBAXC74XLGAQXELowXIVgYXF6QWF+Z3EJAhGFUganHJAoXFRooXLMAQXCLwwXHMAQXUMAQXCRwQWFC9HSiMvC40iCocxiKoEC4cTC4sRiLBDC5MiF4wXFmQXHL4/zkRHEO6H/OwwXFX5IXfd5HfC5s0C4/TC5skRwZ4LLxAXHMAxeIC4hIJLxYXEJAxGMJAgwFFw4XGGAZhD+UjLoxGEC4owDmMSIoouGJAgYBGIIXDCwYuGGAoABIQMiaIQuKDA/dCoguJJIwXHCxQYGCyIYFCyRjELZYA="))

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -20,3 +20,4 @@
0.18: Added reporting of step count and HRM (new Gadgetbridges can now log this)
0.19: Support for call incoming/start/end
0.20: Reduce memory usage
0.21: Fix HRM setting

View File

@ -31,7 +31,7 @@
},
"Find Phone" : function() { E.showMenu(findPhone); },
"Record HRM" : {
value: settings().hrm,
value: !!settings().hrm,
format: v => v?"Yes":"No",
onchange: v => updateSetting('hrm', v)
},

View File

@ -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"))

View File

@ -1,2 +1,3 @@
0.01: New App!
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

View File

@ -12,7 +12,10 @@ function onHRM(h) {
hrmInfo = h;
hrmOffset = 0;
if (hrmInterval) clearInterval(hrmInterval);
hrmInterval = setInterval(readHRM,40);
hrmInterval = undefined;
setTimeout(function() {
hrmInterval = setInterval(readHRM,41);
}, 40);
var px = g.getWidth()/2;
g.setFontAlign(0,0);
@ -35,7 +38,6 @@ function countDown() {
countDown();
var min=0,max=0;
var wasHigh = 0, wasLow = 0;
var lastHigh = getTime();
var hrmList = [];
@ -51,9 +53,7 @@ function readHRM() {
for (var i=0;i<2;i++) {
var a = hrmInfo.raw[hrmOffset];
hrmOffset++;
min=Math.min(min*0.97+a*0.03,a);
max=Math.max(max*0.97+a*0.03,a);
y = E.clip(170 - (a*4),100,230);
y = E.clip(170 - (a*2),100,230);
g.setColor(1,1,1);
g.lineTo(hrmOffset, y);
}

View File

@ -3,3 +3,4 @@
0.03: Pass `area{x,y,w,h}` to render callback instead of just `y`
0.05: Adjust position of notification src text
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)

View File

@ -10,13 +10,13 @@ other applications or widgets to display messages.
```JS
options = {
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
id // optional notification ID, used with hide()
src : string, // optional source name
body : string, // optional body text
icon : string, // optional icon (image string)
render function(area) {} // function callback to render in area{x,y,w,h}
render function(area) {} // function callback to render in area{x,y,w,h}
};
// eg... show notification
require("notify").show({title:"Test", body:"Hello"});

View File

@ -70,8 +70,18 @@ exports.show = function(options) {
options = options || {};
if (options.on===undefined) options.on = true;
id = ("id" in options)?options.id:null;
let size = options.size || 80;
if (size>80) {size = 80}
let w = 240;
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();
// TODO: throw exception if double-buffered?
// TODO: throw exception if size>80?
@ -80,7 +90,6 @@ exports.show = function(options) {
// drawing area
let x = 0,
y = 320-size,
w = 240,
h = size,
b = y+h-1, r = x+w-1; // bottom,right
g.setClipRect(x,y, r,b);
@ -99,20 +108,18 @@ exports.show = function(options) {
g.setFont("6x8", 1).setFontAlign(1, 1, 0);
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) {
let i = options.icon, iw;
g.drawImage(i, x,y+4);
if ("string"==typeof i) {iw = i.charCodeAt(0)}
else {iw = i[0]}
if ("string"==typeof i) iw = i.charCodeAt(0);
else iw = i[0];
x += iw;w -= iw;
}
// body text
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);
}
@ -133,7 +140,7 @@ exports.show = function(options) {
}
anim();
Bangle.on("touch", exports.hide);
}
};
/**
options = {
@ -152,4 +159,4 @@ exports.hide = function(options) {
if (pos < 0) setTimeout(anim, 10);
}
anim();
}
};

View File

@ -1,2 +1,3 @@
0.01: First version of the Walkers Clock
0.02: Fixed screen flicker
0.03: Added display of GPS fix lat/lon and course

View File

@ -34,8 +34,10 @@ const GPS_SATS = "gps_sats";
const GPS_RUNNING = "gps_running";
const GDISP_OS = "g_osref";
const GDISP_LATLN = "g_latln";
const GDISP_SPEED = "g_speed";
const GDISP_ALT = "g_alt";
const GDISP_COURSE = "g_course";
const Y_TIME = 40;
const Y_ACTIVITY = 120;
@ -138,14 +140,21 @@ function drawActivity() {
case GDISP_OS:
activityStr = ref;
break;
case GDISP_LATLN:
g.setFontVector(26);
activityStr = last_fix.lat.toFixed(4) + ", " + last_fix.lon.toFixed(4);
break;
case GDISP_SPEED:
speed = last_fix.speed;
speed = speed.toFixed(1);
activityStr = speed + "kph"
activityStr = speed + "kph";
break;
case GDISP_ALT:
activityStr = last_fix.alt + "m";
break;
case GDISP_COURSE:
activityStr = last_fix.course;
break;
}
g.clearRect(0, Y_ACTIVITY, 239, Y_MODELINE - 1);
@ -203,12 +212,18 @@ function drawInfo() {
case GDISP_OS:
str = "GPS: Grid";
break;
case GDISP_LATLN:
str = "GPS: Lat,Lon";
break;
case GDISP_SPEED:
str = "GPS: Speed";
break;
case GDISP_ALT:
str = "GPS: Alt";
break;
case GDISP_COURSE:
str = "GPS: Course";
break;
}
drawModeLine(str,col);
return;
@ -280,6 +295,12 @@ function changeInfoMode() {
gpsDisplay = GDISP_ALT;
break;
case GDISP_ALT:
gpsDisplay = GDISP_COURSE;
break;
case GDISP_COURSE:
gpsDisplay = GDISP_LATLN;
break;
case GDISP_LATLN:
default:
gpsDisplay = GDISP_OS;
break;

176
apps/waypointer/README.md Normal file
View File

@ -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.
![](waypointer_screenshot.jpg)
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.
![](wp2_screenshot.jpg)
Bearing and distance are both zero as WP2 has currently no GPS
location associated with it. To mark the location, press BTN2.
![](wp2_saved.jpg)
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.

283
apps/waypointer/app.js Normal file
View File

@ -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();

1
apps/waypointer/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AFcBiAWViMRDCkBiUhC68RC64AFGxsRC4UiAAY2HOAQAEC4MSn//AAXzGAwWGC4czC4f/mIwEFwIlEBoIXDBQnyGAkRiYWE/8yLAIXBGAhgEFw5WBC4R0BkYaBmRfFF44XCNI6OGGAQlBAAIXIX4yPJaBq/JC5oeHC/4X/C/4X/C/4X/C/4X/C88RiIXUDAIWVAH4AVA="))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -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>

View File

@ -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" }
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

11
apps/widcom/README.md Normal file
View File

@ -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

30
apps/widcom/widget.js Normal file
View File

@ -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};
})();

BIN
apps/widcom/widget.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -229,7 +229,7 @@ while(fileA=allFiles.pop()) {
if (globA.test(nameB)||globB.test(nameA)) {
if (isGlob(nameA)||isGlob(nameB))
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}`)
}
})
}