Remove BangleRun as it's unmaintained and no longer works nicely with PR #1052 for Typescript support
Remove Travis builds as we're now switching to GitHub actionsmaster
parent
a375853e46
commit
52d54c9234
|
|
@ -1,3 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
0.01: First release
|
||||
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
|
||||
0.06: Reading HDOP directly from the GPS event (needs Espruino 2v07 or above)
|
||||
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
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stopActivity = exports.startActivity = exports.clearActivity = void 0;
|
||||
const display_1 = require("./display");
|
||||
const log_1 = require("./log");
|
||||
const state_1 = require("./state");
|
||||
function startActivity(state) {
|
||||
if (state.status === state_1.ActivityStatus.Stopped) {
|
||||
(0, log_1.initLog)(state);
|
||||
}
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.status = state_1.ActivityStatus.Paused;
|
||||
}
|
||||
else {
|
||||
state.status = state_1.ActivityStatus.Running;
|
||||
}
|
||||
(0, display_1.draw)(state);
|
||||
}
|
||||
exports.startActivity = startActivity;
|
||||
function stopActivity(state) {
|
||||
if (state.status === state_1.ActivityStatus.Paused) {
|
||||
clearActivity(state);
|
||||
}
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.status = state_1.ActivityStatus.Paused;
|
||||
}
|
||||
else {
|
||||
state.status = state_1.ActivityStatus.Stopped;
|
||||
}
|
||||
(0, display_1.draw)(state);
|
||||
}
|
||||
exports.stopActivity = stopActivity;
|
||||
function clearActivity(state) {
|
||||
state.duration = 0;
|
||||
state.distance = 0;
|
||||
state.speed = 0;
|
||||
state.steps = 0;
|
||||
state.cadence = 0;
|
||||
}
|
||||
exports.clearActivity = clearActivity;
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
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 };
|
||||
|
|
@ -1 +0,0 @@
|
|||
require("heatshrink").decompress(atob("mEwwIHEuAEDgP8ApMDAqAXBjAGD/E8AgUcgF8CAX/BgIFBn//wAFCv//8PwAoP///5Aon/8AcB+IFB4AFB8P/34FBgfj/8fwAFB4f+g4cBg/H/w/Cg+HKQcPx4FEh4/CAoMfAocOj4/CKYRwELIIFDLII6BAoZSBLIYeCgP+v4FD/k/GAQFBHgcD/ABBIIX4gIFBSYPwAoUPAog/B8AFEwAFDDQQCBQoQFCZYYFigCKEgFwgAA=="))
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const activity_1 = require("./activity");
|
||||
const display_1 = require("./display");
|
||||
const gps_1 = require("./gps");
|
||||
const hrm_1 = require("./hrm");
|
||||
const state_1 = require("./state");
|
||||
const step_1 = require("./step");
|
||||
const appState = (0, state_1.initState)();
|
||||
(0, gps_1.initGps)(appState);
|
||||
(0, hrm_1.initHrm)(appState);
|
||||
(0, step_1.initStep)(appState);
|
||||
(0, display_1.initDisplay)(appState);
|
||||
setWatch(() => (0, activity_1.startActivity)(appState), BTN1, { repeat: true, edge: 'falling' });
|
||||
setWatch(() => (0, activity_1.stopActivity)(appState), BTN3, { repeat: true, edge: 'falling' });
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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' });
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -1,106 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initDisplay = exports.formatTime = exports.formatPace = exports.formatDistance = exports.formatClock = exports.drawValue = exports.drawBackground = exports.drawAll = exports.draw = void 0;
|
||||
const state_1 = require("./state");
|
||||
const STATUS_COLORS = {
|
||||
'STOP': 0xF800,
|
||||
'PAUSE': 0xFFE0,
|
||||
'RUN': 0x07E0,
|
||||
};
|
||||
function initDisplay(state) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
state.drawing = on;
|
||||
if (on) {
|
||||
drawAll(state);
|
||||
}
|
||||
});
|
||||
drawAll(state);
|
||||
}
|
||||
exports.initDisplay = initDisplay;
|
||||
function drawBackground() {
|
||||
g.clear();
|
||||
g.setColor(0xC618);
|
||||
g.setFont('6x8', 2);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
g.drawString('DIST (KM)', 60, 32);
|
||||
g.drawString('TIME', 172, 32);
|
||||
g.drawString('PACE', 60, 92);
|
||||
g.drawString('HEART', 172, 92);
|
||||
g.drawString('STEPS', 60, 152);
|
||||
g.drawString('CADENCE', 172, 152);
|
||||
}
|
||||
exports.drawBackground = drawBackground;
|
||||
function drawValue(value, x, y) {
|
||||
g.setColor(0x0000);
|
||||
g.fillRect(x - 60, y, x + 60, y + 30);
|
||||
g.setColor(0xFFFF);
|
||||
g.drawString(value, x, y);
|
||||
}
|
||||
exports.drawValue = drawValue;
|
||||
function draw(state) {
|
||||
g.setFontVector(30);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
drawValue(formatDistance(state.distance), 60, 55);
|
||||
drawValue(formatTime(state.duration), 172, 55);
|
||||
drawValue(formatPace(state.speed), 60, 115);
|
||||
drawValue(state.hr.toFixed(0), 172, 115);
|
||||
drawValue(state.steps.toFixed(0), 60, 175);
|
||||
drawValue(state.cadence.toFixed(0), 172, 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, 230, 240);
|
||||
g.setColor(0x0000);
|
||||
g.drawString(state.status, 200, 220);
|
||||
g.setFont("6x8").setFontAlign(0, 0, 1).setColor(-1);
|
||||
if (state.status === state_1.ActivityStatus.Paused) {
|
||||
g.drawString("START", 236, 60, 1).drawString(" CLEAR ", 236, 180, 1);
|
||||
}
|
||||
else if (state.status === state_1.ActivityStatus.Running) {
|
||||
g.drawString(" PAUSE ", 236, 60, 1).drawString(" PAUSE ", 236, 180, 1);
|
||||
}
|
||||
else {
|
||||
g.drawString("START", 236, 60, 1).drawString(" ", 236, 180, 1);
|
||||
}
|
||||
}
|
||||
exports.draw = draw;
|
||||
function drawAll(state) {
|
||||
drawBackground();
|
||||
draw(state);
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
exports.drawAll = drawAll;
|
||||
function formatClock(date) {
|
||||
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
|
||||
}
|
||||
exports.formatClock = formatClock;
|
||||
function formatDistance(meters) {
|
||||
return (meters / 1000).toFixed(2);
|
||||
}
|
||||
exports.formatDistance = formatDistance;
|
||||
function formatPace(speed) {
|
||||
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) + `"`;
|
||||
}
|
||||
exports.formatPace = formatPace;
|
||||
function formatTime(time) {
|
||||
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);
|
||||
}
|
||||
exports.formatTime = formatTime;
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import { ActivityStatus, 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', 172, 32);
|
||||
g.drawString('PACE', 60, 92);
|
||||
g.drawString('HEART', 172, 92);
|
||||
g.drawString('STEPS', 60, 152);
|
||||
g.drawString('CADENCE', 172, 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), 172, 55);
|
||||
drawValue(formatPace(state.speed), 60, 115);
|
||||
drawValue(state.hr.toFixed(0), 172, 115);
|
||||
drawValue(state.steps.toFixed(0), 60, 175);
|
||||
drawValue(state.cadence.toFixed(0), 172, 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, 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) {
|
||||
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,
|
||||
};
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateGps = exports.readGps = exports.initGps = void 0;
|
||||
const display_1 = require("./display");
|
||||
const log_1 = require("./log");
|
||||
const state_1 = require("./state");
|
||||
const EARTH_RADIUS = 6371008.8;
|
||||
function initGps(state) {
|
||||
Bangle.on('GPS', (gps) => readGps(state, gps));
|
||||
Bangle.setGPSPower(1);
|
||||
}
|
||||
exports.initGps = initGps;
|
||||
function readGps(state, gps) {
|
||||
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;
|
||||
state.gpsValid = state.fix > 0;
|
||||
updateGps(state);
|
||||
(0, display_1.draw)(state);
|
||||
/* Only log GPS data every 5 secs if we
|
||||
have a fix and we're running. */
|
||||
if (state.gpsValid &&
|
||||
state.status === state_1.ActivityStatus.Running &&
|
||||
state.timeSinceLog > 5) {
|
||||
state.timeSinceLog = 0;
|
||||
(0, log_1.updateLog)(state);
|
||||
}
|
||||
}
|
||||
exports.readGps = readGps;
|
||||
function updateGps(state) {
|
||||
const t = Date.now();
|
||||
let dt = (t - state.t) / 1000;
|
||||
if (!isFinite(dt))
|
||||
dt = 0;
|
||||
state.t = t;
|
||||
state.timeSinceLog += dt;
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.duration += dt;
|
||||
}
|
||||
if (!state.gpsValid) {
|
||||
return;
|
||||
}
|
||||
const r = EARTH_RADIUS + state.alt;
|
||||
const lat = state.lat * Math.PI / 180;
|
||||
const lon = state.lon * Math.PI / 180;
|
||||
const x = r * Math.cos(lat) * Math.cos(lon);
|
||||
const y = r * Math.cos(lat) * Math.sin(lon);
|
||||
const z = r * Math.sin(lat);
|
||||
if (!state.x) {
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
return;
|
||||
}
|
||||
const dx = x - state.x;
|
||||
const dy = y - state.y;
|
||||
const dz = z - state.z;
|
||||
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.distance += dpMag;
|
||||
state.speed = (state.distance / state.duration) || 0;
|
||||
state.cadence = (60 * state.steps / state.duration) || 0;
|
||||
}
|
||||
}
|
||||
exports.updateGps = updateGps;
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
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;
|
||||
|
||||
function initGps(state: AppState): void {
|
||||
Bangle.on('GPS', (gps: GpsEvent) => readGps(state, gps));
|
||||
Bangle.setGPSPower(1);
|
||||
}
|
||||
|
||||
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;
|
||||
state.gpsValid = state.fix > 0;
|
||||
|
||||
updateGps(state);
|
||||
draw(state);
|
||||
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
function updateGps(state: AppState): void {
|
||||
const t = Date.now();
|
||||
let dt = (t - state.t) / 1000;
|
||||
if (!isFinite(dt)) dt=0;
|
||||
state.t = t;
|
||||
state.timeSinceLog += dt;
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.duration += dt;
|
||||
}
|
||||
|
||||
if (!state.gpsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const r = EARTH_RADIUS + state.alt;
|
||||
const lat = state.lat * Math.PI / 180;
|
||||
const lon = state.lon * Math.PI / 180;
|
||||
const x = r * Math.cos(lat) * Math.cos(lon);
|
||||
const y = r * Math.cos(lat) * Math.sin(lon);
|
||||
const z = r * Math.sin(lat);
|
||||
|
||||
if (!state.x) {
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
return;
|
||||
}
|
||||
|
||||
const dx = x - state.x;
|
||||
const dy = y - state.y;
|
||||
const dz = z - state.z;
|
||||
const dpMag = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
state.x = x;
|
||||
state.y = y;
|
||||
state.z = z;
|
||||
|
||||
if (state.status === ActivityStatus.Running) {
|
||||
state.distance += dpMag;
|
||||
state.speed = (state.distance / state.duration) || 0;
|
||||
state.cadence = (60 * state.steps / state.duration) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
export { initGps, readGps, updateGps };
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateHrm = exports.initHrm = void 0;
|
||||
function initHrm(state) {
|
||||
Bangle.on('HRM', (hrm) => updateHrm(state, hrm));
|
||||
Bangle.setHRMPower(1);
|
||||
}
|
||||
exports.initHrm = initHrm;
|
||||
function updateHrm(state, hrm) {
|
||||
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)) || 0;
|
||||
state.hr += dHr * hrGain;
|
||||
state.hrError += (hrError - state.hrError) * hrGain;
|
||||
}
|
||||
exports.updateHrm = updateHrm;
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
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)) || 0;
|
||||
|
||||
state.hr += dHr * hrGain;
|
||||
state.hrError += (hrError - state.hrError) * hrGain;
|
||||
}
|
||||
|
||||
export { initHrm, updateHrm };
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="tracks"></div>
|
||||
|
||||
<script src="../../core/lib/interface.js"></script>
|
||||
<script>
|
||||
/* TODO: Calculate cadence from step count */
|
||||
var domTracks = document.getElementById("tracks");
|
||||
|
||||
function saveKML(track,title) {
|
||||
var kml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2">
|
||||
<Document>
|
||||
<Schema id="schema">
|
||||
<gx:SimpleArrayField name="heartrate" type="int">
|
||||
<displayName>Heart Rate</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="steps" type="int">
|
||||
<displayName>Step Count</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="distance" type="float">
|
||||
<displayName>Distance</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
<gx:SimpleArrayField name="cadence" type="int">
|
||||
<displayName>Cadence</displayName>
|
||||
</gx:SimpleArrayField>
|
||||
</Schema>
|
||||
<Folder>
|
||||
<name>Tracks</name>
|
||||
<Placemark>
|
||||
<name>${title}</name>
|
||||
<gx:Track>
|
||||
${track.map(pt=>` <when>${pt.date.toISOString()}</when>\n`).join("")}
|
||||
${track.map(pt=>` <gx:coord>${pt.lon} ${pt.lat} ${pt.alt}</gx:coord>\n`).join("")}
|
||||
<ExtendedData>
|
||||
<SchemaData schemaUrl="#schema">
|
||||
<gx:SimpleArrayData name="heartrate">
|
||||
${track.map(pt=>` <gx:value>${pt.heartrate}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="steps">
|
||||
${track.map(pt=>` <gx:value>${pt.steps}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
<gx:SimpleArrayData name="distance">
|
||||
${track.map(pt=>` <gx:value>${pt.distance}</gx:value>\n`).join("")}
|
||||
</gx:SimpleArrayData>
|
||||
</SchemaData>
|
||||
</ExtendedData>
|
||||
</gx:Track>
|
||||
</Placemark>
|
||||
</Folder>
|
||||
</Document>
|
||||
</kml>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([kml], {type: "application/vnd.google-earth.kml+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".kml";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function saveGPX(track, title) {
|
||||
var gpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx creator="Bangle.js" version="1.1" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3">
|
||||
<metadata>
|
||||
<time>${track[0].date.toISOString()}</time>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>${title}</name>
|
||||
<trkseg>`;
|
||||
track.forEach(pt=>{
|
||||
gpx += `
|
||||
<trkpt lat="${pt.lat}" lon="${pt.lon}">
|
||||
<ele>${pt.alt}</ele>
|
||||
<time>${pt.date.toISOString()}</time>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:hr>${pt.heartrate}</gpxtpx:hr>
|
||||
<gpxtpx:distance>${pt.distance}</gpxtpx:distance>
|
||||
${/* <gpxtpx:cad>65</gpxtpx:cad> */""}
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>`;
|
||||
});
|
||||
gpx += `
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
var a = document.createElement("a"),
|
||||
file = new Blob([gpx], {type: "application/gpx+xml"});
|
||||
var url = URL.createObjectURL(file);
|
||||
a.href = url;
|
||||
a.download = title+".gpx";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function trackLineToObject(l, hasFileName) {
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var t = l.trim().split(",");
|
||||
var n = hasFileName ? 1 : 0;
|
||||
var o = {
|
||||
invalid : t.length < 8,
|
||||
date : new Date(parseInt(t[n+0])),
|
||||
lat : parseFloat(t[n+1]),
|
||||
lon : parseFloat(t[n+2]),
|
||||
alt : parseFloat(t[n+3]),
|
||||
duration : parseFloat(t[n+4]),
|
||||
distance : parseFloat(t[n+5]),
|
||||
heartrate : parseInt(t[n+6]),
|
||||
steps : parseInt(t[n+7]),
|
||||
};
|
||||
if (hasFileName)
|
||||
o.filename = t[0];
|
||||
return o;
|
||||
}
|
||||
|
||||
function downloadTrack(trackid, callback) {
|
||||
Util.showModal("Downloading Track...");
|
||||
Util.readStorageFile(trackid, data=>{
|
||||
Util.hideModal();
|
||||
var trackLines = data.trim().split("\n");
|
||||
trackLines.shift(); // remove first line, which is column header
|
||||
// should be:
|
||||
// "timestamp,latitude,longitude,altitude,duration,distance,heartrate,steps\n"
|
||||
var track = trackLines.map(l=>trackLineToObject(l,false));
|
||||
callback(track);
|
||||
});
|
||||
}
|
||||
function getTrackList() {
|
||||
Util.showModal("Loading Tracks...");
|
||||
domTracks.innerHTML = "";
|
||||
Puck.eval(`require("Storage").list(/banglerun_.*\\x01/).map(fn=>{fn=fn.slice(0,-1);var f=require("Storage").open(fn,"r");f.readLine();return fn+","+f.readLine()})`,trackLines=>{
|
||||
var html = `<div class="container">
|
||||
<div class="columns">\n`;
|
||||
trackLines.forEach(l => {
|
||||
var track = trackLineToObject(l, true /*has filename*/);
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">Track ${track.filename}</div>
|
||||
<div class="card-subtitle text-gray">${track.invalid ? "No Data":track.date.toString().substr(0,24)}</div>
|
||||
</div>
|
||||
${track.invalid?``:`<div class="card-image">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="250"
|
||||
frameborder="0" style="border:0"
|
||||
src="https://www.google.com/maps/embed/v1/place?key=AIzaSyBxTcwrrVOh2piz7EmIs1Xn4FsRxJWeVH4&q=${track.lat},${track.lon}&zoom=10" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="card-body"></div>`}
|
||||
<div class="card-footer">${track.invalid?``:`
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadkml">Download KML</button>
|
||||
<button class="btn btn-primary" trackid="${track.filename}" task="downloadgpx">Download GPX</button>`}
|
||||
<button class="btn btn-default" trackid="${track.filename}" task="delete">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (trackLines.length==0) {
|
||||
html += `
|
||||
<div class="column col-12">
|
||||
<div class="card-header">
|
||||
<div class="card-title h5">No tracks</div>
|
||||
<div class="card-subtitle text-gray">No GPS tracks found</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `
|
||||
</div>
|
||||
</div>`;
|
||||
domTracks.innerHTML = html;
|
||||
Util.hideModal();
|
||||
var buttons = domTracks.querySelectorAll("button");
|
||||
for (var i=0;i<buttons.length;i++) {
|
||||
buttons[i].addEventListener("click",event => {
|
||||
var button = event.currentTarget;
|
||||
var trackid = button.getAttribute("trackid");
|
||||
var task = button.getAttribute("task");
|
||||
if (task=="delete") {
|
||||
Util.showModal("Deleting Track...");
|
||||
Util.eraseStorageFile(trackid,()=>{
|
||||
Util.hideModal();
|
||||
getTrackList();
|
||||
});
|
||||
}
|
||||
if (task=="downloadkml") {
|
||||
downloadTrack(trackid, track => saveKML(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
if (task=="downloadgpx") {
|
||||
downloadTrack(trackid, track => saveGPX(track, `Bangle.js Track ${trackid}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onInit() {
|
||||
getTrackList();
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateLog = exports.initLog = void 0;
|
||||
function initLog(state) {
|
||||
const datetime = new Date().toISOString().replace(/[-:]/g, '');
|
||||
const date = datetime.substr(2, 6);
|
||||
const time = datetime.substr(9, 6);
|
||||
const filename = `banglerun_${date}_${time}`;
|
||||
return Object.assign(Object.assign({}, state), { file: require('Storage').open(filename, 'w'), fileWritten: false });
|
||||
}
|
||||
exports.initLog = initLog;
|
||||
function updateLog(state) {
|
||||
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),
|
||||
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(',') + '\n');
|
||||
}
|
||||
exports.updateLog = updateLog;
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { AppState, AppStateWithLog } from './state';
|
||||
|
||||
declare var require: any;
|
||||
|
||||
function initLog(state: AppState): AppStateWithLog {
|
||||
const datetime = new Date().toISOString().replace(/[-:]/g, '');
|
||||
const date = datetime.substr(2, 6);
|
||||
const time = datetime.substr(9, 6);
|
||||
const filename = `banglerun_${date}_${time}`;
|
||||
return {
|
||||
...state,
|
||||
file: require('Storage').open(filename, 'w'),
|
||||
fileWritten: false,
|
||||
} as AppStateWithLog;
|
||||
}
|
||||
|
||||
function updateLog(state: AppStateWithLog): 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),
|
||||
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(',') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
export { initLog, updateLog };
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"id": "banglerun",
|
||||
"name": "BangleRun",
|
||||
"shortName": "BangleRun",
|
||||
"version": "0.10",
|
||||
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
|
||||
"icon": "banglerun.png",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
"supports": ["BANGLEJS"],
|
||||
"interface": "interface.html",
|
||||
"allow_emulator": false,
|
||||
"storage": [
|
||||
{"name":"banglerun.app.js","url":"app.js"},
|
||||
{"name":"banglerun.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initState = exports.ActivityStatus = void 0;
|
||||
var ActivityStatus;
|
||||
(function (ActivityStatus) {
|
||||
ActivityStatus["Stopped"] = "STOP";
|
||||
ActivityStatus["Paused"] = "PAUSE";
|
||||
ActivityStatus["Running"] = "RUN";
|
||||
})(ActivityStatus || (ActivityStatus = {}));
|
||||
exports.ActivityStatus = ActivityStatus;
|
||||
function initState() {
|
||||
return {
|
||||
fix: NaN,
|
||||
lat: NaN,
|
||||
lon: NaN,
|
||||
alt: NaN,
|
||||
vel: NaN,
|
||||
dop: NaN,
|
||||
gpsValid: false,
|
||||
x: NaN,
|
||||
y: NaN,
|
||||
z: NaN,
|
||||
t: NaN,
|
||||
timeSinceLog: 0,
|
||||
hr: 60,
|
||||
hrError: 100,
|
||||
fileWritten: false,
|
||||
drawing: false,
|
||||
status: ActivityStatus.Stopped,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
speed: 0,
|
||||
steps: 0,
|
||||
cadence: 0,
|
||||
};
|
||||
}
|
||||
exports.initState = initState;
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
enum ActivityStatus {
|
||||
Stopped = 'STOP',
|
||||
Paused = 'PAUSE',
|
||||
Running = 'RUN',
|
||||
}
|
||||
|
||||
interface BasicAppState {
|
||||
// GPS NMEA data
|
||||
fix: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
alt: number;
|
||||
vel: number;
|
||||
dop: number;
|
||||
gpsValid: boolean;
|
||||
|
||||
// Absolute position data
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
// Last fix time
|
||||
t: number;
|
||||
// Last time we saved log info
|
||||
timeSinceLog : number;
|
||||
|
||||
// HRM data
|
||||
hr: number,
|
||||
hrError: number,
|
||||
|
||||
// Logger data
|
||||
fileWritten: boolean;
|
||||
|
||||
// Drawing data
|
||||
drawing: boolean;
|
||||
|
||||
// Activity data
|
||||
duration: number;
|
||||
distance: number;
|
||||
speed: number;
|
||||
steps: number;
|
||||
cadence: number;
|
||||
}
|
||||
|
||||
interface AppStateWithoutLog extends BasicAppState {
|
||||
status: 'STOP';
|
||||
}
|
||||
|
||||
interface AppStateWithLog extends BasicAppState {
|
||||
file: File;
|
||||
status: ActivityStatus;
|
||||
}
|
||||
|
||||
type AppState = AppStateWithLog | AppStateWithoutLog;
|
||||
|
||||
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,
|
||||
t: NaN,
|
||||
timeSinceLog : 0,
|
||||
|
||||
hr: 60,
|
||||
hrError: 100,
|
||||
|
||||
fileWritten: false,
|
||||
|
||||
drawing: false,
|
||||
|
||||
status: ActivityStatus.Stopped,
|
||||
duration: 0,
|
||||
distance: 0,
|
||||
speed: 0,
|
||||
steps: 0,
|
||||
cadence: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export { ActivityStatus, AppState, AppStateWithLog, File, initState };
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.updateStep = exports.initStep = void 0;
|
||||
const state_1 = require("./state");
|
||||
function initStep(state) {
|
||||
Bangle.on('step', () => updateStep(state));
|
||||
}
|
||||
exports.initStep = initStep;
|
||||
function updateStep(state) {
|
||||
if (state.status === state_1.ActivityStatus.Running) {
|
||||
state.steps += 1;
|
||||
}
|
||||
}
|
||||
exports.updateStep = updateStep;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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 };
|
||||
Loading…
Reference in New Issue