From 8865959851824fc2bcbde9af712a7b44904319ce Mon Sep 17 00:00:00 2001 From: singintime Date: Wed, 13 Jan 2021 23:34:49 +0100 Subject: [PATCH] banglerun v0.06 - HDOP from GPS, added README --- apps/banglerun/ChangeLog | 1 + apps/banglerun/README.md | 25 +++++++++++++++++ apps/banglerun/app.js | 2 +- apps/banglerun/package.json | 5 +++- apps/banglerun/src/gps.ts | 54 +++++++++++++------------------------ 5 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 apps/banglerun/README.md diff --git a/apps/banglerun/ChangeLog b/apps/banglerun/ChangeLog index b96e5af7a..6358ef5bd 100755 --- a/apps/banglerun/ChangeLog +++ b/apps/banglerun/ChangeLog @@ -3,3 +3,4 @@ 0.03: Fix distance >=10 km (fix #529) 0.04: Use offscreen buffer for flickerless updates 0.05: Complete rewrite. New UI, GPS & HRM Kalman filters, activity logging +0.06: Reading HDOP directly from the GPS event (needs Espruino 2v07 or above) diff --git a/apps/banglerun/README.md b/apps/banglerun/README.md new file mode 100644 index 000000000..80e984bfa --- /dev/null +++ b/apps/banglerun/README.md @@ -0,0 +1,25 @@ +# BangleRun + +An app for running sessions. Displays info and logs your run for later viewing. + +## Compilation + +The app is written in Typescript, and needs to be transpiled in order to be +run on the BangleJS. The easiest way to perform this step is by using the +ubiquitous [NPM package manager](https://www.npmjs.com/get-npm). + +After having installed NPM for your platform, checkout the `BangleApps` repo, +open a terminal, and navigate into the `apps/banglerun` folder. Then issue: + +``` +npm i +``` + +to install the project's build tools, and: + +``` +npm run build +``` + +To build the app. The last command will generate the `app.js` file, containing +the transpiled code for the BangleJS. diff --git a/apps/banglerun/app.js b/apps/banglerun/app.js index 07e8aa135..6e02b744b 100644 --- a/apps/banglerun/app.js +++ b/apps/banglerun/app.js @@ -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,a=n%60;return(r?r+":":"")+("0"+e).substr(-2)+":"+("0"+a).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 a;function o(t){t.status===a.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(","))}(t),t.status===a.Running?t.status=a.Paused:t.status=a.Running,r(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(a||(a={}));function s(t){const n=t.indexOf(".")-2;return(parseInt(t.substr(0,n))+parseFloat(t.substr(n))/60)*Math.PI/180}const i={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:a.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var d;d=i,Bangle.on("GPS-raw",t=>function(t,n){const e=n.split(",");switch(e[0].substr(3,3)){case"GGA":t.lat=s(e[2])*("N"===e[3]?1:-1),t.lon=s(e[4])*("E"===e[5]?1:-1),t.alt=parseFloat(e[9]);break;case"VTG":t.vel=parseFloat(e[7])/3.6;break;case"GSA":t.fix=parseInt(e[2]),t.dop=parseFloat(e[15]);break;case"GLL":t.gpsValid=3===t.fix&&t.dop<=5,function(t){const n=Date.now(),r=(n-t.t)/1e3;if(t.t=n,t.dt+=r,t.status===a.Running&&(t.duration+=r),!t.gpsValid)return;const e=6371008.8+t.alt,o=e*Math.cos(t.lat)*Math.cos(t.lon),s=e*Math.cos(t.lat)*Math.sin(t.lon),i=e*Math.sin(t.lat),d=t.vel;if(!t.x)return t.x=o,t.y=s,t.z=i,t.v=d,t.pError=2.5*t.dop,void(t.vError=.05*t.dop);const u=o-t.x,l=s-t.y,g=i-t.z,c=d-t.v,p=Math.sqrt(u*u+l*l+g*g),f=Math.abs(c);t.pError+=t.v*t.dt,t.dt=0;const N=p+2.5*t.dop,h=f+.05*t.dop,S=t.pError/(t.pError+N),E=t.vError/(t.vError+h);t.x+=u*S,t.y+=l*S,t.z+=g*S,t.v+=c*E,t.pError+=(N-t.pError)*S,t.vError+=(h-t.vError)*E;const w=Math.sqrt(t.x*t.x+t.y*t.y+t.z*t.z);t.lat=180*Math.asin(t.z/w)/Math.PI,t.lon=180*Math.atan2(t.y,t.x)/Math.PI||0,t.alt=w-6371008.8,t.status===a.Running&&(t.distance+=p*S,t.speed=t.distance/t.duration||0,t.cadence=60*t.steps/t.duration||0)}(t),r(t),t.gpsValid&&t.status===a.Running&&function(t){t.file.write("\n"),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(","))}(t)}}(d,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,a=t.hrError/(t.hrError+e);t.hr+=r*a,t.hrError+=(e-t.hrError)*a}(t,n)),Bangle.setHRMPower(1)}(i),function(t){Bangle.on("step",()=>function(t){t.status===a.Running&&(t.steps+=1)}(t))}(i),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&e(t)}),e(t)}(i),setWatch(()=>o(i),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===a.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===a.Running?t.status=a.Paused:t.status=a.Stopped,r(t)}(i),BTN3,{repeat:!0,edge:"falling"})}(); +!function(){"use strict";const t={STOP:63488,PAUSE:65504,RUN:2016};function n(t,n,e){g.setColor(0),g.fillRect(n-60,e,n+60,e+30),g.setColor(65535),g.drawString(t,n,e)}function e(e){var r;g.setFontVector(30),g.setFontAlign(0,-1,0),n((e.distance/1e3).toFixed(2),60,55),n(function(t){const n=Math.round(t),e=Math.floor(n/3600),r=Math.floor(n/60)%60,a=n%60;return(e?e+":":"")+("0"+r).substr(-2)+":"+("0"+a).substr(-2)}(e.duration),180,55),n(function(t){if(t<.1667)return"__'__\"";const n=Math.round(1e3/t),e=Math.floor(n/60),r=n%60;return("0"+e).substr(-2)+"'"+("0"+r).substr(-2)+'"'}(e.speed),60,115),n(e.hr.toFixed(0),180,115),n(e.steps.toFixed(0),60,175),n(e.cadence.toFixed(0),180,175),g.setFont("6x8",2),g.setColor(e.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"+(r=new Date).getHours()).substr(-2)+":"+("0"+r.getMinutes()).substr(-2),120,220),g.setColor(t[e.status]),g.fillRect(160,216,240,240),g.setColor(0),g.drawString(e.status,200,220)}function r(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),e(t),Bangle.drawWidgets()}var a;function s(t){t.status===a.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.file.write(["timestamp","latitude","longitude","altitude","duration","distance","heartrate","steps"].join(",")+"\n")}(t),t.status===a.Running?t.status=a.Paused:t.status=a.Running,e(t)}!function(t){t.Stopped="STOP",t.Paused="PAUSE",t.Running="RUN"}(a||(a={}));const o={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:a.Stopped,duration:0,distance:0,speed:0,steps:0,cadence:0};var i;i=o,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}(i,t)),Bangle.setGPSPower(1),function(t){Bangle.on("HRM",n=>function(t,n){if(0===n.confidence)return;const e=n.bpm-t.hr,r=Math.abs(e)+101-n.confidence,a=t.hrError/(t.hrError+r);t.hr+=e*a,t.hrError+=(r-t.hrError)*a}(t,n)),Bangle.setHRMPower(1)}(o),function(t){Bangle.on("step",()=>function(t){t.status===a.Running&&(t.steps+=1)}(t))}(o),function(t){Bangle.loadWidgets(),Bangle.on("lcdPower",n=>{t.drawing=n,n&&r(t)}),r(t)}(o),setWatch(()=>s(o),BTN1,{repeat:!0,edge:"falling"}),setWatch(()=>function(t){t.status===a.Paused&&function(t){t.duration=0,t.distance=0,t.speed=0,t.steps=0,t.cadence=0}(t),t.status===a.Running?t.status=a.Paused:t.status=a.Stopped,e(t)}(o),BTN3,{repeat:!0,edge:"falling"})}(); diff --git a/apps/banglerun/package.json b/apps/banglerun/package.json index ba75bd8fe..1f5cc677b 100644 --- a/apps/banglerun/package.json +++ b/apps/banglerun/package.json @@ -8,7 +8,10 @@ "build": "rollup -c", "test": "ts-node -P tsconfig.spec.json node_modules/jasmine/bin/jasmine --config=jasmine.json" }, - "author": "Stefano Baldan", + "author": { + "name": "Stefano Baldan", + "email": "singintime@gmail.com" + }, "license": "ISC", "devDependencies": { "@rollup/plugin-typescript": "^4.1.1", diff --git a/apps/banglerun/src/gps.ts b/apps/banglerun/src/gps.ts index bad6fd1c0..42e4476e0 100644 --- a/apps/banglerun/src/gps.ts +++ b/apps/banglerun/src/gps.ts @@ -1,15 +1,22 @@ -import { draw } from './display'; -import { updateLog } from './log'; import { ActivityStatus, AppState } from './state'; declare var Bangle: any; +interface GpsEvent { + lat: number; + lon: number; + alt: number; + speed: number; + hdop: number; + fix: number; +} + const EARTH_RADIUS = 6371008.8; const POS_ACCURACY = 2.5; const VEL_ACCURACY = 0.05; function initGps(state: AppState): void { - Bangle.on('GPS-raw', (nmea: string) => parseNmea(state, nmea)); + Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps)); Bangle.setGPSPower(1); } @@ -20,38 +27,13 @@ function parseCoordinate(coordinate: string): number { return (degrees + minutes) * Math.PI / 180; } -function parseNmea(state: AppState, nmea: string): void { - const tokens = nmea.split(','); - const sentence = tokens[0].substr(3, 3); - - // FIXME: Bangle.js reports HDOP from GGA - can this be used instead - // of manually parsing all of the raw GPS data, which can cause FIFO_FULL - // errors? - - switch (sentence) { - case 'GGA': - state.lat = parseCoordinate(tokens[2]) * (tokens[3] === 'N' ? 1 : -1); - state.lon = parseCoordinate(tokens[4]) * (tokens[5] === 'E' ? 1 : -1); - state.alt = parseFloat(tokens[9]); - break; - case 'VTG': - state.vel = parseFloat(tokens[7]) / 3.6; - break; - case 'GSA': - state.fix = parseInt(tokens[2]); - state.dop = parseFloat(tokens[15]); - break; - case 'GLL': - state.gpsValid = state.fix === 3 && state.dop <= 5; - updateGps(state); - draw(state); - if (state.gpsValid && state.status === ActivityStatus.Running) { - updateLog(state); - } - break; - default: - break; - } +function readGps(state: AppState, gps: GpsEvent): void { + state.lat = gps.lat; + state.lon = gps.lon; + state.alt = gps.alt; + state.vel = gps.speed / 3.6; + state.fix = gps.fix; + state.dop = gps.hdop; } function updateGps(state: AppState): void { @@ -121,4 +103,4 @@ function updateGps(state: AppState): void { } } -export { initGps, parseCoordinate, parseNmea, updateGps }; +export { initGps, parseCoordinate, readGps, updateGps };