Merge branch 'master' of github.com:espruino/BangleApps

master
Gordon Williams 2020-09-01 11:37:50 +01:00
commit 87d12d72a3
24 changed files with 561 additions and 327 deletions

View File

@ -4,3 +4,4 @@ core/lib/qrcode.min.js
core/lib/heatshrink.js
core/lib/marked.min.js
apps/animclk/V29.LBM.js
apps/banglerun/rollup.config.js

View File

@ -361,7 +361,7 @@
{ "id": "gpsnav",
"name": "GPS Navigation",
"icon": "icon.png",
"version":"0.03",
"version":"0.04",
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording, now with waypoint editor",
"tags": "tool,outdoors,gps",
"readme": "README.md",
@ -1465,7 +1465,7 @@
"name": "BangleRun",
"shortName": "BangleRun",
"icon": "banglerun.png",
"version": "0.04",
"version": "0.05",
"description": "An app for running sessions.",
"tags": "run,running,fitness,outdoors",
"allow_emulator": false,
@ -2176,11 +2176,12 @@
"name": "World Clock - 4 time zones",
"shortName":"World Clock",
"icon": "app.png",
"version":"0.01",
"version":"0.02",
"description": "Current time zone plus up to four others",
"tags": "clock",
"type" : "clock",
"custom": "custom.html",
"readme": "README.md",
"storage": [
{"name":"worldclock.app.js","url":"app.js"},
{"name":"worldclock.settings.json"},

1
apps/banglerun/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -2,3 +2,4 @@
0.02: Bugfix time: Reset minutes to 0 when hitting 60
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

View File

@ -1,319 +1 @@
/** Global constants */
const DEG_TO_RAD = Math.PI / 180;
const EARTH_RADIUS = 6371008.8;
/** Utilities for handling vectors */
class Vector {
static magnitude(a) {
let sum = 0;
for (const key of Object.keys(a)) {
sum += a[key] * a[key];
}
return Math.sqrt(sum);
}
static add(a, b) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] + b[key];
}
return result;
}
static sub(a, b) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] - b[key];
}
return result;
}
static multiplyScalar(a, x) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] * x;
}
return result;
}
static divideScalar(a, x) {
const result = {};
for (const key of Object.keys(a)) {
result[key] = a[key] / x;
}
return result;
}
}
/** Interquartile range filter, to detect outliers */
class IqrFilter {
constructor(size, threshold) {
const q = Math.floor(size / 4);
this._buffer = [];
this._size = 4 * q + 2;
this._i1 = q;
this._i3 = 3 * q + 1;
this._threshold = threshold;
}
isReady() {
return this._buffer.length === this._size;
}
isOutlier(point) {
let result = true;
if (this._buffer.length === this._size) {
result = false;
for (const key of Object.keys(point)) {
const data = this._buffer.map(item => item[key]);
data.sort((a, b) => (a - b) / Math.abs(a - b));
const q1 = data[this._i1];
const q3 = data[this._i3];
const iqr = q3 - q1;
const lower = q1 - this._threshold * iqr;
const upper = q3 + this._threshold * iqr;
if (point[key] < lower || point[key] > upper) {
result = true;
break;
}
}
}
this._buffer.push(point);
this._buffer = this._buffer.slice(-this._size);
return result;
}
}
/** Process GPS data */
class Gps {
constructor() {
this._lastCall = Date.now();
this._lastValid = 0;
this._coords = null;
this._filter = new IqrFilter(10, 1.5);
this._shift = { x: 0, y: 0, z: 0 };
}
isReady() {
return this._filter.isReady();
}
getDistance(gps) {
const time = Date.now();
const interval = (time - this._lastCall) / 1000;
this._lastCall = time;
if (!gps.fix) {
return { t: interval, d: 0 };
}
const p = gps.lat * DEG_TO_RAD;
const q = gps.lon * DEG_TO_RAD;
const coords = {
x: EARTH_RADIUS * Math.sin(p) * Math.cos(q),
y: EARTH_RADIUS * Math.sin(p) * Math.sin(q),
z: EARTH_RADIUS * Math.cos(p),
};
if (!this._coords) {
this._coords = coords;
this._lastValid = time;
return { t: interval, d: 0 };
}
const ds = Vector.sub(coords, this._coords);
const dt = (time - this._lastValid) / 1000;
const v = Vector.divideScalar(ds, dt);
if (this._filter.isOutlier(v)) {
return { t: interval, d: 0 };
}
this._shift = Vector.add(this._shift, ds);
const length = Vector.magnitude(this._shift);
const remainder = length % 10;
const distance = length - remainder;
this._coords = coords;
this._lastValid = time;
if (distance > 0) {
this._shift = Vector.multiplyScalar(this._shift, remainder / length);
}
return { t: interval, d: distance };
}
}
/** Process step counter data */
class Step {
constructor(size) {
this._buffer = [];
this._size = size;
}
getCadence() {
this._buffer.push(Date.now() / 1000);
this._buffer = this._buffer.slice(-this._size);
const interval = this._buffer[this._buffer.length - 1] - this._buffer[0];
return interval ? Math.round(60 * (this._buffer.length - 1) / interval) : 0;
}
}
const gps = new Gps();
const step = new Step(10);
let totDist = 0;
let totTime = 0;
let totSteps = 0;
let speed = 0;
let cadence = 0;
let heartRate = 0;
let gpsReady = false;
let hrmReady = false;
let running = false;
let b = Graphics.createArrayBuffer(240,210,2,{msb:true});
let bpal = new Uint16Array([0,0xF800,0x07E0,0xFFFF]);
let COL = { RED:1,GREEN:2,WHITE:3 };
let bimg = {width:240,height:210,bpp:2,buffer:b.buffer,palette:bpal};
function formatClock(date) {
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
}
function formatDistance(m) {
return (m / 1000).toFixed(2) + ' km';
}
function formatTime(s) {
const hrs = Math.floor(s / 3600);
const min = Math.floor(s / 60) % 60;
const sec = Math.floor(s % 60);
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
function formatSpeed(kmh) {
if (kmh <= 0.6) {
return `__'__"`;
}
const skm = 3600 / kmh;
const min = Math.floor(skm / 60);
const sec = Math.floor(skm % 60);
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
}
function drawBackground() {
b.clear();
b.setColor(COL.WHITE);
b.setFontAlign(0, -1, 0);
b.setFont('6x8', 2);
b.drawString('DISTANCE', 120, 20);
b.drawString('TIME', 60, 70);
b.drawString('PACE', 180, 70);
b.drawString('STEPS', 60, 120);
b.drawString('STP/m', 180, 120);
b.drawString('SPEED', 40, 170);
b.drawString('HEART', 120, 170);
b.drawString('CADENCE', 200, 170);
}
function draw() {
const totSpeed = totTime ? 3.6 * totDist / totTime : 0;
const totCadence = totTime ? Math.round(60 * totSteps / totTime) : 0;
b.clearRect(0, 00, 240, 20);
b.clearRect(0, 40, 240, 70);
b.clearRect(0, 90, 240, 120);
b.clearRect(0, 140, 240, 170);
b.clearRect(0, 190, 240, 210);
b.setFont('6x8', 2);
b.setFontAlign(-1, -1, 0);
b.setColor(gpsReady ? COL.GREEN : COL.RED);
b.drawString(' GPS', 6, 0);
b.setFontAlign(1, -1, 0);
b.setColor(COL.WHITE);
b.drawString(formatClock(new Date()), 234, 0);
b.setFontAlign(0, -1, 0);
b.setFontVector(20);
b.drawString(formatDistance(totDist), 120, 40);
b.drawString(formatTime(totTime), 60, 90);
b.drawString(formatSpeed(totSpeed), 180, 90);
b.drawString(totSteps, 60, 140);
b.drawString(totCadence, 180, 140);
b.setFont('6x8', 2);
b.drawString(formatSpeed(speed), 40,190);
b.setColor(hrmReady ? COL.GREEN : COL.RED);
b.drawString(heartRate, 120, 190);
b.setColor(COL.WHITE);
b.drawString(cadence, 200, 190);
g.drawImage(bimg,0,30);
}
function handleGps(coords) {
const step = gps.getDistance(coords);
gpsReady = coords.fix > 0 && gps.isReady();
speed = isFinite(gps.speed) ? gps.speed : 0;
if (running) {
totDist += step.d;
totTime += step.t;
}
}
function handleHrm(hrm) {
hrmReady = hrm.confidence > 50;
heartRate = hrm.bpm;
}
function handleStep() {
cadence = step.getCadence();
if (running) {
totSteps += 1;
}
}
function start() {
running = true;
drawBackground();
draw();
}
function stop() {
if (!running) {
totDist = 0;
totTime = 0;
totSteps = 0;
}
running = false;
drawBackground();
draw();
}
Bangle.on('GPS', handleGps);
Bangle.on('HRM', handleHrm);
Bangle.on('step', handleStep);
Bangle.setGPSPower(1);
Bangle.setHRMPower(1);
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawBackground();
draw();
setInterval(draw, 500);
setWatch(start, BTN1, { repeat: true });
setWatch(stop, BTN3, { repeat: true });
!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"})}();

View File

@ -0,0 +1,6 @@
{
"spec_dir": "test",
"spec_files": [
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,24 @@
{
"name": "banglerun",
"version": "0.5.0",
"description": "Bangle.js app for running sessions",
"main": "app.js",
"types": "app.d.ts",
"scripts": {
"build": "rollup -c",
"test": "ts-node -P tsconfig.spec.json node_modules/jasmine/bin/jasmine --config=jasmine.json"
},
"author": "Stefano Baldan",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-typescript": "^4.1.1",
"@types/jasmine": "^3.5.10",
"jasmine": "^3.5.0",
"rollup": "^2.10.2",
"rollup-plugin-terser": "^5.3.0",
"terser": "^4.7.0",
"ts-node": "^8.10.2",
"tslib": "^2.0.0",
"typescript": "^3.9.2"
}
}

View File

@ -0,0 +1,15 @@
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
export default {
input: './src/app.ts',
output: {
dir: '.',
format: 'iife',
name: 'banglerun'
},
plugins: [
typescript(),
terser(),
]
};

View File

@ -0,0 +1,41 @@
import { draw } from './display';
import { initLog } from './log';
import { ActivityStatus, AppState } from './state';
function startActivity(state: AppState): void {
if (state.status === ActivityStatus.Stopped) {
initLog(state);
}
if (state.status === ActivityStatus.Running) {
state.status = ActivityStatus.Paused;
} else {
state.status = ActivityStatus.Running;
}
draw(state);
}
function stopActivity(state: AppState): void {
if (state.status === ActivityStatus.Paused) {
clearActivity(state);
}
if (state.status === ActivityStatus.Running) {
state.status = ActivityStatus.Paused;
} else {
state.status = ActivityStatus.Stopped;
}
draw(state);
}
function clearActivity(state: AppState): void {
state.duration = 0;
state.distance = 0;
state.speed = 0;
state.steps = 0;
state.cadence = 0;
}
export { clearActivity, startActivity, stopActivity };

20
apps/banglerun/src/app.ts Normal file
View File

@ -0,0 +1,20 @@
import { startActivity, stopActivity } from './activity';
import { initDisplay } from './display';
import { initGps } from './gps';
import { initHrm } from './hrm';
import { initState } from './state';
import { initStep } from './step';
declare var BTN1: any;
declare var BTN3: any;
declare var setWatch: any;
const appState = initState();
initGps(appState);
initHrm(appState);
initStep(appState);
initDisplay(appState);
setWatch(() => startActivity(appState), BTN1, { repeat: true, edge: 'falling' });
setWatch(() => stopActivity(appState), BTN3, { repeat: true, edge: 'falling' });

View File

@ -0,0 +1,114 @@
import { AppState } from './state';
declare var Bangle: any;
declare var g: any;
const STATUS_COLORS = {
'STOP': 0xF800,
'PAUSE': 0xFFE0,
'RUN': 0x07E0,
}
function initDisplay(state: AppState): void {
Bangle.loadWidgets();
Bangle.on('lcdPower', (on: boolean) => {
state.drawing = on;
if (on) {
drawAll(state);
}
});
drawAll(state);
}
function drawBackground(): void {
g.clear();
g.setColor(0xC618);
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);
}
function drawValue(value: string, x: number, y: number) {
g.setColor(0x0000);
g.fillRect(x - 60, y, x + 60, y + 30);
g.setColor(0xFFFF);
g.drawString(value, x, y);
}
function draw(state: AppState): void {
g.setFontVector(30);
g.setFontAlign(0, -1, 0);
drawValue(formatDistance(state.distance), 60, 55);
drawValue(formatTime(state.duration), 180, 55);
drawValue(formatPace(state.speed), 60, 115);
drawValue(state.hr.toFixed(0), 180, 115);
drawValue(state.steps.toFixed(0), 60, 175);
drawValue(state.cadence.toFixed(0), 180, 175);
g.setFont('6x8', 2);
g.setColor(state.gpsValid ? 0x07E0 : 0xF800);
g.fillRect(0, 216, 80, 240);
g.setColor(0x0000);
g.drawString('GPS', 40, 220);
g.setColor(0xFFFF);
g.fillRect(80, 216, 160, 240);
g.setColor(0x0000);
g.drawString(formatClock(new Date()), 120, 220);
g.setColor(STATUS_COLORS[state.status]);
g.fillRect(160, 216, 240, 240);
g.setColor(0x0000);
g.drawString(state.status, 200, 220);
}
function drawAll(state: AppState) {
drawBackground();
draw(state);
Bangle.drawWidgets();
}
function formatClock(date: Date): string {
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
}
function formatDistance(meters: number): string {
return (meters / 1000).toFixed(2);
}
function formatPace(speed: number): string {
if (speed < 0.1667) {
return `__'__"`;
}
const pace = Math.round(1000 / speed);
const min = Math.floor(pace / 60);
const sec = pace % 60;
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
}
function formatTime(time: number): string {
const seconds = Math.round(time);
const hrs = Math.floor(seconds / 3600);
const min = Math.floor(seconds / 60) % 60;
const sec = seconds % 60;
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
}
export {
draw,
drawAll,
drawBackground,
drawValue,
formatClock,
formatDistance,
formatPace,
formatTime,
initDisplay,
};

120
apps/banglerun/src/gps.ts Normal file
View File

@ -0,0 +1,120 @@
import { draw } from './display';
import { updateLog } from './log';
import { ActivityStatus, AppState } from './state';
declare var Bangle: any;
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.setGPSPower(1);
}
function parseCoordinate(coordinate: string): number {
const pivot = coordinate.indexOf('.') - 2;
const degrees = parseInt(coordinate.substr(0, pivot));
const minutes = parseFloat(coordinate.substr(pivot)) / 60;
return (degrees + minutes) * Math.PI / 180;
}
function parseNmea(state: AppState, nmea: string): void {
const tokens = nmea.split(',');
const sentence = tokens[0].substr(3, 3);
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 updateGps(state: AppState): void {
const t = Date.now();
const dt = (t - state.t) / 1000;
state.t = t;
state.dt += dt;
if (state.status === ActivityStatus.Running) {
state.duration += dt;
}
if (!state.gpsValid) {
return;
}
const r = EARTH_RADIUS + state.alt;
const x = r * Math.cos(state.lat) * Math.cos(state.lon);
const y = r * Math.cos(state.lat) * Math.sin(state.lon);
const z = r * Math.sin(state.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);
const vGain = state.vError / (state.vError + vError);
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;
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;
state.lon = (Math.atan2(state.y, state.x) * 180 / Math.PI) || 0;
state.alt = pMag - EARTH_RADIUS;
if (state.status === ActivityStatus.Running) {
state.distance += dpMag * pGain;
state.speed = (state.distance / state.duration) || 0;
state.cadence = (60 * state.steps / state.duration) || 0;
}
}
export { initGps, parseCoordinate, parseNmea, updateGps };

29
apps/banglerun/src/hrm.ts Normal file
View File

@ -0,0 +1,29 @@
import { AppState } from './state';
interface HrmData {
bpm: number;
confidence: number;
raw: string;
}
declare var Bangle: any;
function initHrm(state: AppState) {
Bangle.on('HRM', (hrm: HrmData) => updateHrm(state, hrm));
Bangle.setHRMPower(1);
}
function updateHrm(state: AppState, hrm: HrmData) {
if (hrm.confidence === 0) {
return;
}
const dHr = hrm.bpm - state.hr;
const hrError = Math.abs(dHr) + 101 - hrm.confidence;
const hrGain = state.hrError / (state.hrError + hrError);
state.hr += dHr * hrGain;
state.hrError += (hrError - state.hrError) * hrGain;
}
export { initHrm, updateHrm };

37
apps/banglerun/src/log.ts Normal file
View File

@ -0,0 +1,37 @@
import { AppState } from './state';
declare var require: any;
function initLog(state: AppState): void {
const datetime = new Date().toISOString().replace(/[-:]/g, '');
const date = datetime.substr(2, 6);
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(','));
}
function updateLog(state: AppState): void {
state.file.write('\n');
state.file.write([
Date.now().toFixed(0),
state.lat.toFixed(6),
state.lon.toFixed(6),
state.alt.toFixed(2),
state.duration.toFixed(0),
state.distance.toFixed(2),
state.hr.toFixed(0),
state.steps.toFixed(0),
].join(','));
}
export { initLog, updateLog };

View File

@ -0,0 +1,87 @@
enum ActivityStatus {
Stopped = 'STOP',
Paused = 'PAUSE',
Running = 'RUN',
}
interface AppState {
// GPS NMEA data
fix: number;
lat: number;
lon: number;
alt: number;
vel: number;
dop: number;
gpsValid: boolean;
// GPS Kalman data
x: number;
y: number;
z: number;
v: number;
t: number;
dt: number;
pError: number;
vError: number;
// HRM data
hr: number,
hrError: number,
// Logger data
file: File;
// Drawing data
drawing: boolean;
// Activity data
status: ActivityStatus;
duration: number;
distance: number;
speed: number;
steps: number;
cadence: number;
}
interface File {
read: Function;
write: Function;
erase: Function;
}
function initState(): AppState {
return {
fix: NaN,
lat: NaN,
lon: NaN,
alt: NaN,
vel: NaN,
dop: NaN,
gpsValid: false,
x: NaN,
y: NaN,
z: NaN,
v: NaN,
t: NaN,
dt: NaN,
pError: NaN,
vError: NaN,
hr: 60,
hrError: 100,
file: null,
drawing: false,
status: ActivityStatus.Stopped,
duration: 0,
distance: 0,
speed: 0,
steps: 0,
cadence: 0,
}
}
export { ActivityStatus, AppState, File, initState };

View File

@ -0,0 +1,15 @@
import { ActivityStatus, AppState } from './state';
declare var Bangle: any;
function initStep(state: AppState) {
Bangle.on('step', () => updateStep(state));
}
function updateStep(state: AppState) {
if (state.status === ActivityStatus.Running) {
state.steps += 1;
}
}
export { initStep, updateStep };

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "es2015",
"noImplicitAny": true,
"target": "es2015"
},
"include": [
"src"
]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"target": "es2015"
},
"include": [
"test"
]
}

View File

@ -1,4 +1,4 @@
0.01: New App!
0.02: Add SCREENACCESS interface
0.03: Add Waypoint Editor
0.04: Fix great circle formula

View File

@ -93,9 +93,8 @@ function bearing(a,b){
}
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);
var dsigma = Math.acos(Math.sin(radians(a.lat))*Math.sin(radians(b.lat))+Math.cos(radians(a.lat))*Math.cos(radians(b.lat))*Math.cos(radians(a.lon-b.lon)));
return Math.round(dsigma*6371000);
}
var selected = false;

View File

@ -1 +1,2 @@
0.01: First try
0.02: Update custom.html for refactor; add README

20
apps/worldclock/README.md Normal file
View File

@ -0,0 +1,20 @@
# World Clock - See the time in five locations
In addition to the main clock and date in your current location, you can add up to four other locations. Great for travel or remote working.
![](worldclock.png)
## Usage
Provide names and the UTC offsets for up to four other timezones in the app store. These are stored in a json file on your watch. UTC offsets can be decimal (e.g., 5.5 for India).
The clock does not handle summer time / daylight saving time changes automatically. If one of your four locations changes its UTC offset, you can simply change the setting in the app store and update. Currently the clock only supports 24 hour time format.
## Requests
[Reach out to Scott](https://www.github.com/computermacgyver) if you have feature requests or notice bugs.
## Creator
Made by [Scott Hale](https://www.github.com/computermacgyver), based upon the [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock).

View File

@ -17,7 +17,7 @@
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../lib/customize.js"></script>
<script src="../../core/lib/customize.js"></script>
<script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB