mirror of https://github.com/espruino/BangleApps
pace: use `exstats` for measurements
parent
4b08b4b24c
commit
5220bf6f86
|
@ -1,15 +1,16 @@
|
||||||
{
|
{
|
||||||
var Layout_1 = require("Layout");
|
var Layout_1 = require("Layout");
|
||||||
var state_1 = 1;
|
var exs_1 = require("exstats").getStats(["time", "dist", "pacec"], {
|
||||||
|
notify: {
|
||||||
|
dist: {
|
||||||
|
increment: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
var drawTimeout_1;
|
var drawTimeout_1;
|
||||||
var lastUnlazy_1 = 0;
|
var lastUnlazy_1 = 0;
|
||||||
var lastResumeTime_1 = Date.now();
|
|
||||||
var splitTime_1 = 0;
|
|
||||||
var totalTime_1 = 0;
|
|
||||||
var splits_1 = [];
|
var splits_1 = [];
|
||||||
var splitDist_1 = 0;
|
|
||||||
var splitOffset_1 = 0, splitOffsetPx_1 = 0;
|
var splitOffset_1 = 0, splitOffsetPx_1 = 0;
|
||||||
var lastGPS_1 = 0;
|
|
||||||
var GPS_TIMEOUT_MS_1 = 30000;
|
var GPS_TIMEOUT_MS_1 = 30000;
|
||||||
var layout_1 = new Layout_1({
|
var layout_1 = new Layout_1({
|
||||||
type: "v",
|
type: "v",
|
||||||
|
@ -46,19 +47,13 @@
|
||||||
}, {
|
}, {
|
||||||
lazy: true
|
lazy: true
|
||||||
});
|
});
|
||||||
var formatTime_1 = function (ms) {
|
|
||||||
var totalSeconds = Math.floor(ms / 1000);
|
|
||||||
var minutes = Math.floor(totalSeconds / 60);
|
|
||||||
var seconds = totalSeconds % 60;
|
|
||||||
return "".concat(minutes, ":").concat(seconds < 10 ? '0' : '').concat(seconds);
|
|
||||||
};
|
|
||||||
var calculatePace_1 = function (time, dist) {
|
var calculatePace_1 = function (time, dist) {
|
||||||
if (dist === 0)
|
if (dist === 0)
|
||||||
return 0;
|
return 0;
|
||||||
return time / dist / 1000 / 60;
|
return time / dist / 1000 / 60;
|
||||||
};
|
};
|
||||||
var draw_1 = function () {
|
var draw_1 = function () {
|
||||||
if (state_1 === 1) {
|
if (!exs_1.state.active) {
|
||||||
drawSplits_1();
|
drawSplits_1();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -66,15 +61,15 @@
|
||||||
clearTimeout(drawTimeout_1);
|
clearTimeout(drawTimeout_1);
|
||||||
drawTimeout_1 = setTimeout(draw_1, 1000);
|
drawTimeout_1 = setTimeout(draw_1, 1000);
|
||||||
var now = Date.now();
|
var now = Date.now();
|
||||||
var elapsedTime = formatTime_1(totalTime_1 + (state_1 === 0 ? now - lastResumeTime_1 : 0));
|
|
||||||
var pace;
|
var pace;
|
||||||
if (now - lastGPS_1 <= GPS_TIMEOUT_MS_1) {
|
if ("time" in exs_1.state.thisGPS
|
||||||
pace = calculatePace_1(thisSplitTime_1(), splitDist_1).toFixed(2);
|
&& now - exs_1.state.thisGPS.time < GPS_TIMEOUT_MS_1) {
|
||||||
|
pace = exs_1.stats.pacec.getString();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pace = "No GPS";
|
pace = "No GPS";
|
||||||
}
|
}
|
||||||
layout_1["time"].label = elapsedTime;
|
layout_1["time"].label = exs_1.stats.time.getString();
|
||||||
layout_1["pace"].label = pace;
|
layout_1["pace"].label = pace;
|
||||||
layout_1.render();
|
layout_1.render();
|
||||||
if (now - lastUnlazy_1 > 30000)
|
if (now - lastUnlazy_1 > 30000)
|
||||||
|
@ -89,10 +84,12 @@
|
||||||
var max = splits_1.reduce(function (a, x) { return Math.max(a, x); }, 0);
|
var max = splits_1.reduce(function (a, x) { return Math.max(a, x); }, 0);
|
||||||
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
var totalTime = 0;
|
||||||
for (;; i++) {
|
for (;; i++) {
|
||||||
var split = splits_1[i + splitOffset_1];
|
var split = splits_1[i + splitOffset_1];
|
||||||
if (split == null)
|
if (split == null)
|
||||||
break;
|
break;
|
||||||
|
totalTime += split;
|
||||||
var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2;
|
var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2;
|
||||||
if (y > h)
|
if (y > h)
|
||||||
break;
|
break;
|
||||||
|
@ -101,63 +98,47 @@
|
||||||
var splitPace = calculatePace_1(split, 1);
|
var splitPace = calculatePace_1(split, 1);
|
||||||
g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y);
|
g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y);
|
||||||
}
|
}
|
||||||
var splitTime = thisSplitTime_1();
|
var pace = exs_1.stats.pacec.getString();
|
||||||
var pace = calculatePace_1(splitTime, splitDist_1);
|
var splitTime = exs_1.stats.time.getValue() - totalTime;
|
||||||
g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(pace, " (").concat((splitTime / 1000).toFixed(2), ")"), 0, Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2);
|
g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(pace, " (").concat((splitTime / 1000).toFixed(2), ")"), 0, Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2);
|
||||||
};
|
};
|
||||||
var thisSplitTime_1 = function () {
|
|
||||||
if (state_1 === 1)
|
|
||||||
return splitTime_1;
|
|
||||||
return Date.now() - lastResumeTime_1 + splitTime_1;
|
|
||||||
};
|
|
||||||
var pauseRun_1 = function () {
|
var pauseRun_1 = function () {
|
||||||
state_1 = 1;
|
exs_1.stop();
|
||||||
var now = Date.now();
|
|
||||||
totalTime_1 += now - lastResumeTime_1;
|
|
||||||
splitTime_1 += now - lastResumeTime_1;
|
|
||||||
Bangle.setGPSPower(0, "pace");
|
Bangle.setGPSPower(0, "pace");
|
||||||
Bangle.removeListener('GPS', onGPS_1);
|
|
||||||
draw_1();
|
draw_1();
|
||||||
};
|
};
|
||||||
var resumeRun_1 = function () {
|
var resumeRun_1 = function () {
|
||||||
state_1 = 0;
|
exs_1.start();
|
||||||
lastResumeTime_1 = Date.now();
|
|
||||||
Bangle.setGPSPower(1, "pace");
|
Bangle.setGPSPower(1, "pace");
|
||||||
Bangle.on('GPS', onGPS_1);
|
|
||||||
g.clearRect(Bangle.appRect);
|
g.clearRect(Bangle.appRect);
|
||||||
layout_1.forgetLazyState();
|
layout_1.forgetLazyState();
|
||||||
draw_1();
|
draw_1();
|
||||||
};
|
};
|
||||||
var onGPS_1 = function (fix) {
|
|
||||||
if (fix && fix.speed && state_1 === 0) {
|
|
||||||
var now = Date.now();
|
|
||||||
var elapsedTime = now - lastGPS_1;
|
|
||||||
splitDist_1 += fix.speed * elapsedTime / 3600000;
|
|
||||||
while (splitDist_1 >= 1) {
|
|
||||||
splits_1.push(thisSplitTime_1());
|
|
||||||
splitDist_1 -= 1;
|
|
||||||
splitTime_1 = 0;
|
|
||||||
}
|
|
||||||
lastGPS_1 = now;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var onButton_1 = function () {
|
var onButton_1 = function () {
|
||||||
switch (state_1) {
|
if (exs_1.state.active)
|
||||||
case 0:
|
|
||||||
pauseRun_1();
|
pauseRun_1();
|
||||||
break;
|
else
|
||||||
case 1:
|
|
||||||
resumeRun_1();
|
resumeRun_1();
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
exs_1.stats.dist.on("notify", function (dist) {
|
||||||
|
var prev = splits_1[splits_1.length - 1] || 0;
|
||||||
|
var totalDist = dist.getValue();
|
||||||
|
var thisSplit = totalDist - prev;
|
||||||
|
var prevTime = splits_1.reduce(function (a, b) { return a + b; }, 0);
|
||||||
|
var time = exs_1.stats.time.getValue() - prevTime;
|
||||||
|
while (thisSplit > 0) {
|
||||||
|
splits_1.push(time);
|
||||||
|
time = 0;
|
||||||
|
thisSplit -= 1000;
|
||||||
|
}
|
||||||
|
});
|
||||||
Bangle.on('lock', function (locked) {
|
Bangle.on('lock', function (locked) {
|
||||||
if (!locked && state_1 == 0)
|
if (!locked && exs_1.state.active)
|
||||||
onButton_1();
|
onButton_1();
|
||||||
});
|
});
|
||||||
setWatch(function () { return onButton_1(); }, BTN1, { repeat: true });
|
setWatch(function () { return onButton_1(); }, BTN1, { repeat: true });
|
||||||
Bangle.on('drag', function (e) {
|
Bangle.on('drag', function (e) {
|
||||||
if (state_1 !== 1 || e.b === 0)
|
if (exs_1.state.active || e.b === 0)
|
||||||
return;
|
return;
|
||||||
splitOffsetPx_1 -= e.dy;
|
splitOffsetPx_1 -= e.dy;
|
||||||
if (splitOffsetPx_1 > 20) {
|
if (splitOffsetPx_1 > 20) {
|
||||||
|
|
109
apps/pace/app.ts
109
apps/pace/app.ts
|
@ -1,24 +1,22 @@
|
||||||
{
|
{
|
||||||
const Layout = require("Layout");
|
const Layout = require("Layout");
|
||||||
|
const exs = require("exstats").getStats(
|
||||||
|
["time", "dist", "pacec"],
|
||||||
|
{
|
||||||
|
notify: {
|
||||||
|
dist: {
|
||||||
|
increment: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const enum RunState {
|
|
||||||
RUNNING,
|
|
||||||
PAUSED
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = RunState.PAUSED;
|
|
||||||
let drawTimeout: TimeoutId | undefined;
|
let drawTimeout: TimeoutId | undefined;
|
||||||
let lastUnlazy = 0;
|
let lastUnlazy = 0;
|
||||||
|
|
||||||
let lastResumeTime = Date.now();
|
const splits: number[] = []; // times
|
||||||
let splitTime = 0;
|
|
||||||
let totalTime = 0;
|
|
||||||
|
|
||||||
const splits: number[] = [];
|
|
||||||
let splitDist = 0;
|
|
||||||
let splitOffset = 0, splitOffsetPx = 0;
|
let splitOffset = 0, splitOffsetPx = 0;
|
||||||
|
|
||||||
let lastGPS = 0;
|
|
||||||
const GPS_TIMEOUT_MS = 30000;
|
const GPS_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
const layout = new Layout({
|
const layout = new Layout({
|
||||||
|
@ -57,20 +55,13 @@ const layout = new Layout({
|
||||||
lazy: true
|
lazy: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const formatTime = (ms: number) => {
|
|
||||||
let totalSeconds = Math.floor(ms / 1000);
|
|
||||||
let minutes = Math.floor(totalSeconds / 60);
|
|
||||||
let seconds = totalSeconds % 60;
|
|
||||||
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const calculatePace = (time: number, dist: number) => {
|
const calculatePace = (time: number, dist: number) => {
|
||||||
if (dist === 0) return 0;
|
if (dist === 0) return 0;
|
||||||
return time / dist / 1000 / 60;
|
return time / dist / 1000 / 60;
|
||||||
};
|
};
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
if (state === RunState.PAUSED) {
|
if (!exs.state.active) {
|
||||||
// no draw-timeout here, only on user interaction
|
// no draw-timeout here, only on user interaction
|
||||||
drawSplits();
|
drawSplits();
|
||||||
return;
|
return;
|
||||||
|
@ -81,16 +72,16 @@ const draw = () => {
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const elapsedTime = formatTime(totalTime + (state === RunState.RUNNING ? now - lastResumeTime : 0));
|
|
||||||
|
|
||||||
let pace: string;
|
let pace: string;
|
||||||
if (now - lastGPS <= GPS_TIMEOUT_MS) {
|
if ("time" in exs.state.thisGPS
|
||||||
pace = calculatePace(thisSplitTime(), splitDist).toFixed(2);
|
&& now - (exs.state.thisGPS.time as unknown as number) < GPS_TIMEOUT_MS)
|
||||||
|
{
|
||||||
|
pace = exs.stats.pacec.getString()
|
||||||
}else{
|
}else{
|
||||||
pace = "No GPS";
|
pace = "No GPS";
|
||||||
}
|
}
|
||||||
|
|
||||||
layout["time"]!.label = elapsedTime;
|
layout["time"]!.label = exs.stats.time.getString();
|
||||||
layout["pace"]!.label = pace;
|
layout["pace"]!.label = pace;
|
||||||
layout.render();
|
layout.render();
|
||||||
|
|
||||||
|
@ -111,10 +102,13 @@ const drawSplits = () => {
|
||||||
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
let totalTime = 0;
|
||||||
for(; ; i++) {
|
for(; ; i++) {
|
||||||
const split = splits[i + splitOffset];
|
const split = splits[i + splitOffset];
|
||||||
if (split == null) break;
|
if (split == null) break;
|
||||||
|
|
||||||
|
totalTime += split;
|
||||||
|
|
||||||
const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2;
|
const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2;
|
||||||
if (y > h) break;
|
if (y > h) break;
|
||||||
|
|
||||||
|
@ -125,8 +119,9 @@ const drawSplits = () => {
|
||||||
g.setColor("#fff").drawString(`${i + 1 + splitOffset} @ ${splitPace.toFixed(2)}`, 0, y);
|
g.setColor("#fff").drawString(`${i + 1 + splitOffset} @ ${splitPace.toFixed(2)}`, 0, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitTime = thisSplitTime();
|
const pace = exs.stats.pacec.getString();
|
||||||
const pace = calculatePace(splitTime, splitDist);
|
const splitTime = exs.stats.time.getValue() - totalTime;
|
||||||
|
|
||||||
g.setColor("#fff").drawString(
|
g.setColor("#fff").drawString(
|
||||||
`${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`,
|
`${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`,
|
||||||
0,
|
0,
|
||||||
|
@ -134,69 +129,51 @@ const drawSplits = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const thisSplitTime = () => {
|
|
||||||
if (state === RunState.PAUSED) return splitTime;
|
|
||||||
return Date.now() - lastResumeTime + splitTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pauseRun = () => {
|
const pauseRun = () => {
|
||||||
state = RunState.PAUSED;
|
exs.stop();
|
||||||
const now = Date.now();
|
|
||||||
totalTime += now - lastResumeTime;
|
|
||||||
splitTime += now - lastResumeTime;
|
|
||||||
Bangle.setGPSPower(0, "pace")
|
Bangle.setGPSPower(0, "pace")
|
||||||
Bangle.removeListener('GPS', onGPS);
|
|
||||||
draw();
|
draw();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resumeRun = () => {
|
const resumeRun = () => {
|
||||||
state = RunState.RUNNING;
|
exs.start();
|
||||||
lastResumeTime = Date.now();
|
|
||||||
Bangle.setGPSPower(1, "pace");
|
Bangle.setGPSPower(1, "pace");
|
||||||
Bangle.on('GPS', onGPS);
|
|
||||||
|
|
||||||
g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine
|
g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine
|
||||||
layout.forgetLazyState();
|
layout.forgetLazyState();
|
||||||
draw();
|
draw();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onGPS = (fix: GPSFix) => {
|
|
||||||
if (fix && fix.speed && state === RunState.RUNNING) {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
const elapsedTime = now - lastGPS; // ms
|
|
||||||
splitDist += fix.speed * elapsedTime / 3600000; // ms in one hour (fix.speed is in km/h)
|
|
||||||
|
|
||||||
while (splitDist >= 1) {
|
|
||||||
splits.push(thisSplitTime());
|
|
||||||
splitDist -= 1;
|
|
||||||
splitTime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastGPS = now;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onButton = () => {
|
const onButton = () => {
|
||||||
switch (state) {
|
if (exs.state.active)
|
||||||
case RunState.RUNNING:
|
|
||||||
pauseRun();
|
pauseRun();
|
||||||
break;
|
else
|
||||||
case RunState.PAUSED:
|
|
||||||
resumeRun();
|
resumeRun();
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exs.stats.dist.on("notify", (dist) => {
|
||||||
|
const prev = splits[splits.length - 1] || 0;
|
||||||
|
const totalDist = dist.getValue();
|
||||||
|
let thisSplit = totalDist - prev;
|
||||||
|
const prevTime = splits.reduce((a, b) => a + b, 0);
|
||||||
|
let time = exs.stats.time.getValue() - prevTime;
|
||||||
|
|
||||||
|
while(thisSplit > 0) {
|
||||||
|
splits.push(time);
|
||||||
|
time = 0; // if we've jumped more than 1k, credit the time to the first split
|
||||||
|
thisSplit -= 1000;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Bangle.on('lock', locked => {
|
Bangle.on('lock', locked => {
|
||||||
// treat an unlock (while running) as a pause
|
// treat an unlock (while running) as a pause
|
||||||
if(!locked && state == RunState.RUNNING) onButton();
|
if(!locked && exs.state.active) onButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
setWatch(() => onButton(), BTN1, { repeat: true });
|
setWatch(() => onButton(), BTN1, { repeat: true });
|
||||||
|
|
||||||
Bangle.on('drag', e => {
|
Bangle.on('drag', e => {
|
||||||
if (state !== RunState.PAUSED || e.b === 0) return;
|
if (exs.state.active || e.b === 0) return;
|
||||||
|
|
||||||
splitOffsetPx -= e.dy;
|
splitOffsetPx -= e.dy;
|
||||||
if (splitOffsetPx > 20) {
|
if (splitOffsetPx > 20) {
|
||||||
|
|
Loading…
Reference in New Issue