pace: use `exstats` for measurements

pull/3565/head
Rob Pilling 2024-09-22 21:47:20 +01:00
parent 4b08b4b24c
commit 5220bf6f86
2 changed files with 81 additions and 123 deletions

View File

@ -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) {

View File

@ -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) {