diff --git a/apps.json b/apps.json index 17ad9eff2..d034460ec 100644 --- a/apps.json +++ b/apps.json @@ -2894,10 +2894,10 @@ ] }, { "id": "arrow", - "name": "Arrow", + "name": "Arrow Compass", "icon": "arrow.png", "type":"app", - "version":"0.01", + "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", @@ -2905,5 +2905,19 @@ {"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} + ] +} ] diff --git a/apps/arrow/README.md b/apps/arrow/README.md index 975e72b1c..3b439711c 100644 --- a/apps/arrow/README.md +++ b/apps/arrow/README.md @@ -1,4 +1,4 @@ -# Moving Arrow Compass +# Arrow Compass A variation of jeffmer's Navigation Compass. The compass points North and shows the current heading. diff --git a/apps/arrow/app.js b/apps/arrow/app.js index 115f16210..657cd2595 100644 --- a/apps/arrow/app.js +++ b/apps/arrow/app.js @@ -2,6 +2,7 @@ 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; @@ -27,8 +28,6 @@ function radians(d) { function drawCompass(course) { if(!candraw) return; - var img = require("heatshrink").decompress(atob("lEowIPMjAEDngEDvwED/4DCgP/wAEBgf/4AEBg//8AEBh//+AEBj///AEBn///gEBv///wmCAAImCAAIoBFggE/AkaaEABo=")); - buf1.setColor(1); buf1.fillCircle(80,80,79,79); buf1.setColor(0); diff --git a/apps/waypointer/README.md b/apps/waypointer/README.md new file mode 100644 index 000000000..3f0f529b8 --- /dev/null +++ b/apps/waypointer/README.md @@ -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. + diff --git a/apps/waypointer/app.js b/apps/waypointer/app.js new file mode 100644 index 000000000..636d4fca8 --- /dev/null +++ b/apps/waypointer/app.js @@ -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(); diff --git a/apps/waypointer/icon.js b/apps/waypointer/icon.js new file mode 100644 index 000000000..a5be96818 --- /dev/null +++ b/apps/waypointer/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AFcBiAWViMRDCkBiUhC68RC64AFGxsRC4UiAAY2HOAQAEC4MSn//AAXzGAwWGC4czC4f/mIwEFwIlEBoIXDBQnyGAkRiYWE/8yLAIXBGAhgEFw5WBC4R0BkYaBmRfFF44XCNI6OGGAQlBAAIXIX4yPJaBq/JC5oeHC/4X/C/4X/C/4X/C/4X/C88RiIXUDAIWVAH4AVA=")) diff --git a/apps/waypointer/waypointer.png b/apps/waypointer/waypointer.png new file mode 100644 index 000000000..b72f9313c Binary files /dev/null and b/apps/waypointer/waypointer.png differ diff --git a/apps/waypointer/waypointer_screenshot.jpg b/apps/waypointer/waypointer_screenshot.jpg new file mode 100644 index 000000000..ba7d9f492 Binary files /dev/null and b/apps/waypointer/waypointer_screenshot.jpg differ diff --git a/apps/waypointer/waypoints.html b/apps/waypointer/waypoints.html new file mode 100644 index 000000000..d02260732 --- /dev/null +++ b/apps/waypointer/waypoints.html @@ -0,0 +1,170 @@ + + + + + + + +

List of waypoints

+ + + + + + + + + + + + +
NameLat.Long.Actions
+
+

Add a new waypoint

+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ + + + + + + diff --git a/apps/waypointer/waypoints.json b/apps/waypointer/waypoints.json new file mode 100644 index 000000000..98a670c0d --- /dev/null +++ b/apps/waypointer/waypoints.json @@ -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" } +] \ No newline at end of file diff --git a/apps/waypointer/wp2_saved.jpg b/apps/waypointer/wp2_saved.jpg new file mode 100644 index 000000000..abec34b41 Binary files /dev/null and b/apps/waypointer/wp2_saved.jpg differ diff --git a/apps/waypointer/wp2_screenshot.jpg b/apps/waypointer/wp2_screenshot.jpg new file mode 100644 index 000000000..a6df13c93 Binary files /dev/null and b/apps/waypointer/wp2_screenshot.jpg differ