diff --git a/apps/wpmoto/README.md b/apps/wpmoto/README.md new file mode 100644 index 000000000..96c72ff55 --- /dev/null +++ b/apps/wpmoto/README.md @@ -0,0 +1,158 @@ +# Waypointer Moto + +Waypointer Moto is a GPS navigation aid intended to be attached to +the handlebars of a motorcycle. +It uses the GPS to find out which direction it's +travelling and shows the direction and distance to the destination +"as the crow flies". It gives you an indication of where to go, +but exploring and navigating the environment is left up to the user. + +![](watch-on-bike.jpeg) + +(Please note that it would be foolish in the extreme to rely on this +as your only navigation aid! Make sure you read this entire document +before using the app for navigation so that you know the drawbacks +and shortcomings.) + +## App usage + +### Main screen + +![](screenshot.png) + +The main screen shows the direction arrow, the distance to the waypoint, +and the name of the selected waypoint. + +It also shows the status of the GPS fix in the colour of the arrow: + + * Red: no GPS fix at all + * Yellow: GPS location, but no GPS course (probably you're moving too slowly); + in this case the direction of travel comes from the compass bearing instead + of the GPS course, but note that the compass is unreliable + * White: GPS fix includes both location and course, and the GPS course is used + to determine the direction of travel + +### Select a waypoint + +![](screenshot-menu.png) + +Press the middle button (`BTN2`) to enter the menu, choose a waypoint using +the up/down arrows, and use the middle button again to select a waypoint and +return to the main screen. + +### Add a waypoint + +Press the middle button (`BTN2`) to enter the menu, and select the "+ Here" +option. This will add a waypoint named "WP*n*" marking your current location, +where "*n*" is the next unused number. + +### Delete a waypoint + +![](screenshot-delete.png) + +Select a waypoint using the menu. Once the waypoint is selected and you're +back on the main screen, press either the top or bottom button (`BTN1` or +`BTN3`). Confirm that you want to delete the waypoint with the middle +button (`BTN2`). + +## Waypoint editor + +With the Bangle.js app loader connected to the watch, find the +Waypointer Moto app and click on the floppy disk icon: + +![](floppy-disk.png) + +This will load up the waypoint editor: + +![](editor.png) + +### Add a waypoint + +Use the map to find your destination. Clicking on the map will +populate the latitude/longitude input boxes with the coordinates +of the point you clicked on. Type in a name for the waypoint and +click "Add Waypoint". Click "Upload" to send the updated list of +waypoints to the watch. + +### Edit a waypoint + +Click on the pencil icon next to the waypoint you wish to edit. +This will remove the waypoint from the list and populate the +input boxes. +Edit the coordinates by hand, or by clicking on the map. Edit +the name if you want. Click "Add Waypoint" to save the waypoint +back to the list. Click "Upload" to send the updated list of +waypoints to the watch. + +### Delete a waypoint + +Click on the pencil icon next to the waypoint you wish to edit. +This will remove the waypoint from the list. +Click "Upload" to send the updated list of waypoints to the watch. + +## Mounting the watch on the bike + +There is a 3d-printable "artificial wrist" which will fit over a 7/8" +handlebar and allow the watch strap to tighten up. +Alternatively, in a pinch you can strap the watch around a glove or a sponge +or anything else that will pad out the space so that the watch is a tight +fit. + +The 3d-printed part should be a snug fit on the handlebar so that it does +not flop around. If it is too loose, line it with a layer or 2 of tape. + +[Download the handlebar mount STL »](handlebar-mount.stl) + +[Download the handlebar mount FreeCAD source »](handlebar-mount.FCStd) + +![](handlebar-mount.png) + +![](handlebar-mount.jpeg) + +## Comparison to Way Pointer + +Compared to the original Way Pointer app, Waypointer Moto: + + * removes the numerical display of compass bearing + * makes the distance text bigger + * uses a higher-resolution arrow icon + * has a visual indication of the GPS status (the arrow colour) + * uses GPS course instead of compass bearing + * has OpenStreetMap integration in the waypoint editor + * uses Bangle.js menus to select waypoints instead of custom UI + * can add new waypoints from inside the app without requiring a blank slot + * can delete waypoints from inside the app without needing the PC + * still uses the same `waypoints.json` file + +## Gotchas + +Waypointer Moto derives your current heading from the GPS course +rather than the compass, whenever GPS course is available. +The compass bearing is based on the angle the watch is held, but +the GPS course is based on the direction it's *travelling*. If the +watch is not aligned with the direction of travel of the vehicle +then the arrow will not point in the correct direction. + +When travelling too slowly, there is no GPS course information, so the +app reverts to using the compass (and draws it in yellow), but +the compass is not very reliable, and I +have especially found it not to be reliable when placed on a motorcyle, +maybe because of all the metal in the immediate vicinity. So if +the arrow is not drawn in white, then you should probably not trust +it. If you're not sure, just ride in a straight line until the arrow +turns white again. + +## Possible Future Enhancements + + - "routes" with multiple waypoints; automatically step from one + waypoint to the next when you get near to it + - some way to manually input coordinates directly on the watch + - make the text & arrow more legible in direct sunlight + - integrate a charging connector into the handlebar mount + - upstream the map integration to the other waypoint apps + +## Acknowledgements + +Waypointer Moto is a project by [James Stanley](https://incoherency.co.uk/). It is a derivative of [Adam Schmalhofer's](https://github.com/adamschmalhofer) Way Pointer app, which is in turn a derivative of +[jeffmer's](https://github.com/jeffmer/JeffsBangleAppsDev) GPS +Navigation and Compass Navigation apps. diff --git a/apps/wpmoto/app.js b/apps/wpmoto/app.js new file mode 100644 index 000000000..8a0e86ef6 --- /dev/null +++ b/apps/wpmoto/app.js @@ -0,0 +1,288 @@ +var loc = require("locale"); + +var waypoints = require("Storage").readJSON("waypoints.json")||[{name:"NONE"}]; +var wp = waypoints[0]; +var wp_bearing = 0; +var candraw = true; + +var direction = 0; +var dist = 0; + +var savedfix; + +var previous = { + dst: '', + wp_name: '', + course: 180, + selected: false, +}; + +/*** Drawing ***/ + +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 +var pal_br = new Uint16Array([0x0000,0xf800],0,1); // black, red +var pal_compass = pal_by; + +var buf = Graphics.createArrayBuffer(160,160,1, {msb:true}); +var arrow_img = require("heatshrink").decompress(atob("vF4wJC/AEMYBxs8Bxt+Bxv/BpkB/+ABxcD//ABxcH//gBxcP//wBxcf//4Bxc///8Bxd///+OxgABOxgABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBR2BAAJ4KOwIABPBQNCPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBR2DPBQNEPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBB2FPBANGPAx2HPAx2HPAx2HPAx2HPAx2HPAx2HeJTeJB34O/B34O/B34O/B34O/B34O/B34O/B34O/B34OTAH4AT")); + +function flip1(x,y,palette) { + g.drawImage({width:160,height:160,bpp:1,buffer:buf.buffer, palette:palette},x,y); + buf.clear(); +} + +function flip2_bw(x,y) { + g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bw},x,y); + buf.clear(); +} + +function flip2_bb(x,y) { + g.drawImage({width:160,height:40,bpp:1,buffer:buf.buffer, palette:pal_bb},x,y); + buf.clear(); +} + +function drawCompass(course) { + if (!candraw) return; + + previous.course = course; + + buf.setColor(1); + buf.fillCircle(80,80, 79); + buf.setColor(0); + buf.fillCircle(80,80, 69); + buf.setColor(1); + buf.drawImage(arrow_img, 80, 80, {rotate:radians(course)} ); + var palette = pal_br; + if (savedfix !== undefined && savedfix.fix !== 0) palette = pal_compass; + flip1(40, 30, palette); +} + +function drawN(force){ + if (!candraw) return; + + buf.setFont("Vector",24); + var dst = loc.distance(dist); + + // distance on left + if (force || previous.dst !== dst) { + previous.dst = dst; + buf.setColor(1); + buf.setFontAlign(-1, -1); + buf.setFont("Vector",40); + buf.drawString(dst,0,0); + flip2_bw(8, 200); + } + + // waypoint name on right + if (force || previous.wp_name !== wp.name) { + previous.wp_name = wp.name; + buf.setColor(1); + buf.setFontAlign(1, -1); + buf.setFont("Vector", 15); + buf.drawString(wp.name, 80, 0); + flip2_bw(160, 220); + } +} + +function drawAll(force) { + if (!candraw) return; + + g.setColor(1,1,1); + drawN(force); + drawCompass(direction); +} + +/*** Heading ***/ + +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; +} + +function read_heading() { + if (savedfix !== undefined && !isNaN(savedfix.course)) { + Bangle.setCompassPower(0); + heading = savedfix.course; + pal_compass = pal_bw; + } else { + var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale); + Bangle.setCompassPower(1); + heading = newHeading(d,heading); + pal_compass = pal_by; + } + + direction = wp_bearing - heading; + if (direction < 0) direction += 360; + if (direction > 360) direction -= 360; + drawCompass(direction); +} + + +/*** Maths ***/ + +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); +} + +/*** Waypoints ***/ + +function addCurrentWaypoint() { + var wpnum = 0; + var ok = false; + // XXX: O(n^2) search for lowest unused WP number + while (!ok) { + ok = true; + for (var i = 0; i < waypoints.length && ok; i++) { + if (waypoints[i].name == ("WP"+wpnum)) { + wpnum++; + ok = false; + } + } + } + + waypoints.push({ + name: "WP" + wpnum, + lat: savedfix.lat, + lon: savedfix.lon, + }); + wp = waypoints[waypoints.length-1]; + saveWaypoints(); +} + +function saveWaypoints() { + require("Storage").writeJSON("waypoints.json", waypoints); +} + +function deleteWaypoint(w) { + for (var i = 0; i < waypoints.length; i++) { + if (waypoints[i] == w) { + waypoints.splice(i, 1); + saveWaypoints(); + wp = {name:"NONE"}; + } + } +} + +/*** Setup ***/ + +function onGPS(fix) { + savedfix = fix; + + 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(); + } +} + +function startTimers() { + setInterval(function() { + Bangle.setLCDPower(1); + read_heading(); + }, 500); +} + +function addWaypointToMenu(menu, i) { + menu[waypoints[i].name] = function() { + wp = waypoints[i]; + mainScreen(); + }; +} + +function mainScreen() { + E.showMenu(); + candraw = true; + drawAll(true); + + Bangle.setUI("updown", function(v) { + if (v === undefined) { + candraw = false; + var menu = { + "": { "title": "-- Waypoints --" }, + }; + for (let i = 0; i < waypoints.length; i++) { + addWaypointToMenu(menu, i); + } + menu["+ Here"] = function() { + addCurrentWaypoint(); + mainScreen(); + }; + menu["< Back"] = mainScreen; + E.showMenu(menu); + } else { + candraw = false; + E.showPrompt("Delete waypoint: " + wp.name + "?").then(function(confirmed) { + var name = wp.name; + if (confirmed) { + deleteWaypoint(wp); + E.showAlert("Waypoint deleted: " + name).then(mainScreen); + } else { + mainScreen(); + } + }); + } + }); +} + +Bangle.on('kill',()=>{ + Bangle.setCompassPower(0); + Bangle.setGPSPower(0); +}); + +g.clear(); +Bangle.setLCDBrightness(1); +Bangle.setGPSPower(1); +startTimers(); +Bangle.on('GPS', onGPS); +mainScreen(); diff --git a/apps/wpmoto/arrow.png b/apps/wpmoto/arrow.png new file mode 100644 index 000000000..870c7b6b2 Binary files /dev/null and b/apps/wpmoto/arrow.png differ diff --git a/apps/wpmoto/editor.png b/apps/wpmoto/editor.png new file mode 100644 index 000000000..0d2f9841b Binary files /dev/null and b/apps/wpmoto/editor.png differ diff --git a/apps/wpmoto/floppy-disk.png b/apps/wpmoto/floppy-disk.png new file mode 100644 index 000000000..dde8500c9 Binary files /dev/null and b/apps/wpmoto/floppy-disk.png differ diff --git a/apps/wpmoto/handlebar-mount.FCStd b/apps/wpmoto/handlebar-mount.FCStd new file mode 100644 index 000000000..a370c1090 Binary files /dev/null and b/apps/wpmoto/handlebar-mount.FCStd differ diff --git a/apps/wpmoto/handlebar-mount.jpeg b/apps/wpmoto/handlebar-mount.jpeg new file mode 100644 index 000000000..c4556a0d0 Binary files /dev/null and b/apps/wpmoto/handlebar-mount.jpeg differ diff --git a/apps/wpmoto/handlebar-mount.png b/apps/wpmoto/handlebar-mount.png new file mode 100644 index 000000000..da5adf7a7 Binary files /dev/null and b/apps/wpmoto/handlebar-mount.png differ diff --git a/apps/wpmoto/handlebar-mount.stl b/apps/wpmoto/handlebar-mount.stl new file mode 100644 index 000000000..966d0c327 Binary files /dev/null and b/apps/wpmoto/handlebar-mount.stl differ diff --git a/apps/wpmoto/icon.js b/apps/wpmoto/icon.js new file mode 100644 index 000000000..fc8eee898 --- /dev/null +++ b/apps/wpmoto/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///tVK/feuekkEh1dSnnn5P2imlgdr221vvv0E5x9z8dqoEpMf4AqgMQCysRiIYUgMd6IXVqc1iIXXAAo2NiIXBivdAAfVGwxwCAAgXBicmswACsVRGAsRqoAEqIXC0QXDs0lGAkBisrBomBC4WqGAloMIkRqW72wNDlvTC4QwEwIvDLoOr3e7KwkzmsdmczxAABwaQFF4QwEC4SAGR4pfBGAO2kQABlQXIX4wlHL4LQNX5IXNDw4XygdZ96MFF58NvP8C6Mzu93uF9+oXQrQWBAAPU/5kPgoWDu+UoIQJ7vR6IGCm4XEz9MC5HMAAvHC4mc4gWHk93zPsC5F58oWHgQOE//8//M//+5mf9vAFxFwOQef5nu9wbB5nOiNQBoInCCYMCk8FjnM7qkB5lEp3//udRoKlBuueDwMyC4MojnJz2dmEA/1EovDn3d6nEiEHv3pzOc4oXB1nJ0Wo7JVBjnE/td6IxBgtQu/u4ci1vs6EAj+S3eynvBgEP92Z/+XzIXBiF59GOte27goB5OrlGDlWcMAP3SgJvBusBgF/61sycrlPFgvJsWZzMrziGB84XDvlBg+excp9N73P1gbQB5//+lEiMAi93vwaBz4XC1ep5OS2f9gPJlWZ93d+lBqNfzn//N5+lAg+cF4OZF4UA52LlWi3RfBSANEAAVBgEHz+W3VzlepVANczW73ep+L4CgNTnrfBAAPc8drUAOcR4MB/PYwefAwIXCjvRdgIABgvO7Ui1PMX4MA5n+96+BC4cRiIXDKAPOzPvIwIABhsc4oPEDAIGFhqhB4K2BACY+EAH4AG")) diff --git a/apps/wpmoto/metadata.json b/apps/wpmoto/metadata.json new file mode 100644 index 000000000..4e6c3497a --- /dev/null +++ b/apps/wpmoto/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "wpmoto", + "name": "Waypointer Moto", + "shortName": "Waypointer Moto", + "version": "0.01", + "description": "Waypoint-based motorcycle navigation aid", + "icon": "wpmoto.png", + "tags": "tool,outdoors,gps", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-menu.png"},{"url":"screenshot-delete.png"}], + "readme": "README.md", + "interface": "wpmoto.html", + "storage": [ + {"name":"wpmoto.app.js","url":"app.js"}, + {"name":"wpmoto.img","url":"icon.js","evaluate":true} + ], + "data": [{"name":"waypoints.json","url":"waypoints.json"}] +} diff --git a/apps/wpmoto/screenshot-delete.png b/apps/wpmoto/screenshot-delete.png new file mode 100644 index 000000000..4669a6cd0 Binary files /dev/null and b/apps/wpmoto/screenshot-delete.png differ diff --git a/apps/wpmoto/screenshot-menu.png b/apps/wpmoto/screenshot-menu.png new file mode 100644 index 000000000..cd4cedf2f Binary files /dev/null and b/apps/wpmoto/screenshot-menu.png differ diff --git a/apps/wpmoto/screenshot.png b/apps/wpmoto/screenshot.png new file mode 100644 index 000000000..756e54118 Binary files /dev/null and b/apps/wpmoto/screenshot.png differ diff --git a/apps/wpmoto/watch-on-bike.jpeg b/apps/wpmoto/watch-on-bike.jpeg new file mode 100644 index 000000000..c97fe9412 Binary files /dev/null and b/apps/wpmoto/watch-on-bike.jpeg differ diff --git a/apps/wpmoto/waypoints.json b/apps/wpmoto/waypoints.json new file mode 100644 index 000000000..8a4ab83b8 --- /dev/null +++ b/apps/wpmoto/waypoints.json @@ -0,0 +1,5 @@ +[ + { + "name":"NONE" + }, +] diff --git a/apps/wpmoto/wpmoto.html b/apps/wpmoto/wpmoto.html new file mode 100644 index 000000000..2fb7c9455 --- /dev/null +++ b/apps/wpmoto/wpmoto.html @@ -0,0 +1,198 @@ + + + + + + + + + +

List of waypoints

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

Add a new waypoint

+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ + + + + + + + diff --git a/apps/wpmoto/wpmoto.png b/apps/wpmoto/wpmoto.png new file mode 100644 index 000000000..fea1d128a Binary files /dev/null and b/apps/wpmoto/wpmoto.png differ