mirror of https://github.com/espruino/BangleApps
pace: initial app
parent
36a50d1cee
commit
d447b5c2e6
|
@ -0,0 +1 @@
|
|||
0.01: New app!
|
|
@ -0,0 +1,6 @@
|
|||
# Description
|
||||
|
||||
A running pace app, useful for races. Will also record your splits and display them to you on the pause menu.
|
||||
|
||||
Drag up/down on the pause menu to scroll through your splits.
|
||||
Press the button to pause/resume - when resumed, pressing the button will pause instantly, regardless of whether the screen is locked.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkBiIA/AH8QRYoX/AAcAZqwXuMIQYCiAcOO456OCwwACF5orEDghzOE4oZCLxw+GDAKsXeSIqEGBKKGBwIRFGBCfIC7p5RC7oGHC8RxFC453JAw4Xda5AIEC5IAPO6AXmO5BuHC67OIUA4aERyIXEIxAAJiAuFC5gTEcgpDNBwoWCIxp4CCwp1OCIYAEOiDFNDBwVQAH4AvA="))
|
|
@ -0,0 +1,179 @@
|
|||
{
|
||||
var Layout_1 = require("Layout");
|
||||
var state_1 = 1;
|
||||
var drawTimeout_1;
|
||||
var lastUnlazy_1 = 0;
|
||||
var lastResumeTime_1 = Date.now();
|
||||
var splitTime_1 = 0;
|
||||
var totalTime_1 = 0;
|
||||
var splits_1 = [];
|
||||
var splitDist_1 = 0;
|
||||
var splitOffset_1 = 0, splitOffsetPx_1 = 0;
|
||||
var lastGPS_1 = 0;
|
||||
var GPS_TIMEOUT_MS_1 = 30000;
|
||||
var layout_1 = new Layout_1({
|
||||
type: "v",
|
||||
c: [
|
||||
{
|
||||
type: "txt",
|
||||
font: "6x8:2",
|
||||
label: "Pace",
|
||||
id: "paceLabel",
|
||||
pad: 4
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:40",
|
||||
label: "",
|
||||
id: "pace",
|
||||
halign: 0
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "6x8:2",
|
||||
label: "Time",
|
||||
id: "timeLabel",
|
||||
pad: 4
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:40",
|
||||
label: "",
|
||||
id: "time",
|
||||
halign: 0
|
||||
},
|
||||
]
|
||||
}, {
|
||||
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) {
|
||||
if (dist === 0)
|
||||
return 0;
|
||||
return time / dist / 1000 / 60;
|
||||
};
|
||||
var draw_1 = function () {
|
||||
if (state_1 === 1) {
|
||||
drawSplits_1();
|
||||
return;
|
||||
}
|
||||
if (drawTimeout_1)
|
||||
clearTimeout(drawTimeout_1);
|
||||
drawTimeout_1 = setTimeout(draw_1, 1000);
|
||||
var now = Date.now();
|
||||
var elapsedTime = formatTime_1(totalTime_1 + (state_1 === 0 ? now - lastResumeTime_1 : 0));
|
||||
var pace;
|
||||
if (now - lastGPS_1 <= GPS_TIMEOUT_MS_1) {
|
||||
pace = calculatePace_1(thisSplitTime_1(), splitDist_1).toFixed(2);
|
||||
}
|
||||
else {
|
||||
pace = "No GPS";
|
||||
}
|
||||
layout_1["time"].label = elapsedTime;
|
||||
layout_1["pace"].label = pace;
|
||||
layout_1.render();
|
||||
if (now - lastUnlazy_1 > 30000)
|
||||
layout_1.forgetLazyState(), lastUnlazy_1 = now;
|
||||
};
|
||||
var drawSplits_1 = function () {
|
||||
g.clearRect(Bangle.appRect);
|
||||
var barSize = 20;
|
||||
var barSpacing = 10;
|
||||
var w = g.getWidth();
|
||||
var h = g.getHeight();
|
||||
var max = splits_1.reduce(function (a, x) { return Math.max(a, x); }, 0);
|
||||
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
||||
var i = 0;
|
||||
for (;; i++) {
|
||||
var split = splits_1[i + splitOffset_1];
|
||||
if (split == null)
|
||||
break;
|
||||
var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2;
|
||||
if (y > h)
|
||||
break;
|
||||
var size = w * split / max;
|
||||
g.setColor("#00f").fillRect(0, y, size, y + barSize);
|
||||
var splitPace = calculatePace_1(split, 1);
|
||||
g.setColor("#fff").drawString("".concat(i + 1 + splitOffset_1, " @ ").concat(splitPace.toFixed(2)), 0, y);
|
||||
}
|
||||
var splitTime = thisSplitTime_1();
|
||||
var pace = calculatePace_1(splitTime, splitDist_1);
|
||||
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 () {
|
||||
state_1 = 1;
|
||||
var now = Date.now();
|
||||
totalTime_1 += now - lastResumeTime_1;
|
||||
splitTime_1 += now - lastResumeTime_1;
|
||||
Bangle.setGPSPower(0, "pace");
|
||||
Bangle.removeListener('GPS', onGPS_1);
|
||||
draw_1();
|
||||
};
|
||||
var resumeRun_1 = function () {
|
||||
state_1 = 0;
|
||||
lastResumeTime_1 = Date.now();
|
||||
Bangle.setGPSPower(1, "pace");
|
||||
Bangle.on('GPS', onGPS_1);
|
||||
g.clearRect(Bangle.appRect);
|
||||
layout_1.forgetLazyState();
|
||||
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 () {
|
||||
switch (state_1) {
|
||||
case 0:
|
||||
pauseRun_1();
|
||||
break;
|
||||
case 1:
|
||||
resumeRun_1();
|
||||
break;
|
||||
}
|
||||
};
|
||||
Bangle.on('lock', function (locked) {
|
||||
if (!locked && state_1 == 0)
|
||||
onButton_1();
|
||||
});
|
||||
setWatch(function () { return onButton_1(); }, BTN1, { repeat: true });
|
||||
Bangle.on('drag', function (e) {
|
||||
if (state_1 !== 1 || e.b === 0)
|
||||
return;
|
||||
splitOffsetPx_1 -= e.dy;
|
||||
if (splitOffsetPx_1 > 20) {
|
||||
if (splitOffset_1 < splits_1.length - 3)
|
||||
splitOffset_1++, Bangle.buzz(30);
|
||||
splitOffsetPx_1 = 0;
|
||||
}
|
||||
else if (splitOffsetPx_1 < -20) {
|
||||
if (splitOffset_1 > 0)
|
||||
splitOffset_1--, Bangle.buzz(30);
|
||||
splitOffsetPx_1 = 0;
|
||||
}
|
||||
draw_1();
|
||||
});
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
g.clearRect(Bangle.appRect);
|
||||
draw_1();
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 715 B |
|
@ -0,0 +1,217 @@
|
|||
{
|
||||
const Layout = require("Layout");
|
||||
|
||||
const enum RunState {
|
||||
RUNNING,
|
||||
PAUSED
|
||||
}
|
||||
|
||||
let state = RunState.PAUSED;
|
||||
let drawTimeout: TimeoutId | undefined;
|
||||
let lastUnlazy = 0;
|
||||
|
||||
let lastResumeTime = Date.now();
|
||||
let splitTime = 0;
|
||||
let totalTime = 0;
|
||||
|
||||
const splits: number[] = [];
|
||||
let splitDist = 0;
|
||||
let splitOffset = 0, splitOffsetPx = 0;
|
||||
|
||||
let lastGPS = 0;
|
||||
const GPS_TIMEOUT_MS = 30000;
|
||||
|
||||
const layout = new Layout({
|
||||
type: "v",
|
||||
c: [
|
||||
{
|
||||
type: "txt",
|
||||
font: "6x8:2",
|
||||
label: "Pace",
|
||||
id: "paceLabel",
|
||||
pad: 4
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:40",
|
||||
label: "",
|
||||
id: "pace",
|
||||
halign: 0
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "6x8:2",
|
||||
label: "Time",
|
||||
id: "timeLabel",
|
||||
pad: 4
|
||||
},
|
||||
{
|
||||
type: "txt",
|
||||
font: "Vector:40",
|
||||
label: "",
|
||||
id: "time",
|
||||
halign: 0
|
||||
},
|
||||
]
|
||||
}, {
|
||||
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) => {
|
||||
if (dist === 0) return 0;
|
||||
return time / dist / 1000 / 60;
|
||||
};
|
||||
|
||||
const draw = () => {
|
||||
if (state === RunState.PAUSED) {
|
||||
// no draw-timeout here, only on user interaction
|
||||
drawSplits();
|
||||
return;
|
||||
}
|
||||
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(draw, 1000);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const elapsedTime = formatTime(totalTime + (state === RunState.RUNNING ? now - lastResumeTime : 0));
|
||||
|
||||
let pace: string;
|
||||
if (now - lastGPS <= GPS_TIMEOUT_MS) {
|
||||
pace = calculatePace(thisSplitTime(), splitDist).toFixed(2);
|
||||
}else{
|
||||
pace = "No GPS";
|
||||
}
|
||||
|
||||
layout["time"]!.label = elapsedTime;
|
||||
layout["pace"]!.label = pace;
|
||||
layout.render();
|
||||
|
||||
if (now - lastUnlazy > 30000)
|
||||
layout.forgetLazyState(), lastUnlazy = now;
|
||||
};
|
||||
|
||||
const drawSplits = () => {
|
||||
g.clearRect(Bangle.appRect);
|
||||
|
||||
const barSize = 20;
|
||||
const barSpacing = 10;
|
||||
const w = g.getWidth();
|
||||
const h = g.getHeight();
|
||||
|
||||
const max = splits.reduce((a, x) => Math.max(a, x), 0);
|
||||
|
||||
g.setFont("6x8", 2).setFontAlign(-1, -1);
|
||||
|
||||
let i = 0;
|
||||
for(; ; i++) {
|
||||
const split = splits[i + splitOffset];
|
||||
if (split == null) break;
|
||||
|
||||
const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2;
|
||||
if (y > h) break;
|
||||
|
||||
const size = w * split / max; // Scale bar height based on pace
|
||||
g.setColor("#00f").fillRect(0, y, size, y + barSize);
|
||||
|
||||
const splitPace = calculatePace(split, 1); // Pace per km
|
||||
g.setColor("#fff").drawString(`${i + 1 + splitOffset} @ ${splitPace.toFixed(2)}`, 0, y);
|
||||
}
|
||||
|
||||
const splitTime = thisSplitTime();
|
||||
const pace = calculatePace(splitTime, splitDist);
|
||||
g.setColor("#fff").drawString(
|
||||
`${i + 1 + splitOffset} @ ${pace} (${(splitTime / 1000).toFixed(2)})`,
|
||||
0,
|
||||
Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2,
|
||||
);
|
||||
};
|
||||
|
||||
const thisSplitTime = () => {
|
||||
if (state === RunState.PAUSED) return splitTime;
|
||||
return Date.now() - lastResumeTime + splitTime;
|
||||
};
|
||||
|
||||
const pauseRun = () => {
|
||||
state = RunState.PAUSED;
|
||||
const now = Date.now();
|
||||
totalTime += now - lastResumeTime;
|
||||
splitTime += now - lastResumeTime;
|
||||
Bangle.setGPSPower(0, "pace")
|
||||
Bangle.removeListener('GPS', onGPS);
|
||||
draw();
|
||||
};
|
||||
|
||||
const resumeRun = () => {
|
||||
state = RunState.RUNNING;
|
||||
lastResumeTime = Date.now();
|
||||
Bangle.setGPSPower(1, "pace");
|
||||
Bangle.on('GPS', onGPS);
|
||||
|
||||
g.clearRect(Bangle.appRect); // splits -> layout, clear. layout -> splits, fine
|
||||
layout.forgetLazyState();
|
||||
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 = () => {
|
||||
switch (state) {
|
||||
case RunState.RUNNING:
|
||||
pauseRun();
|
||||
break;
|
||||
case RunState.PAUSED:
|
||||
resumeRun();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on('lock', locked => {
|
||||
// treat an unlock (while running) as a pause
|
||||
if(!locked && state == RunState.RUNNING) onButton();
|
||||
});
|
||||
|
||||
setWatch(() => onButton(), BTN1, { repeat: true });
|
||||
|
||||
Bangle.on('drag', e => {
|
||||
if (state !== RunState.PAUSED || e.b === 0) return;
|
||||
|
||||
splitOffsetPx -= e.dy;
|
||||
if (splitOffsetPx > 20) {
|
||||
if (splitOffset < splits.length-3) splitOffset++, Bangle.buzz(30);
|
||||
splitOffsetPx = 0;
|
||||
} else if (splitOffsetPx < -20) {
|
||||
if (splitOffset > 0) splitOffset--, Bangle.buzz(30);
|
||||
splitOffsetPx = 0;
|
||||
}
|
||||
draw();
|
||||
});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
g.clearRect(Bangle.appRect);
|
||||
draw();
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "pace",
|
||||
"name": "Pace",
|
||||
"version": "0.01",
|
||||
"description": "Show pace and time running splits",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{ "name": "pace.app.js","url": "app.js" },
|
||||
{ "name": "pace.img","url": "app-icon.js","evaluate": true }
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue