2023-02-22 21:25:56 +00:00
|
|
|
let runInterval;
|
2024-05-14 21:58:02 +00:00
|
|
|
let screen = "main"; // main | karvonen | menu | zoom
|
2023-02-22 21:25:56 +00:00
|
|
|
// Run interface wrapped in a function
|
2024-04-21 14:48:56 +00:00
|
|
|
const ExStats = require("exstats");
|
2023-02-22 21:25:56 +00:00
|
|
|
let B2 = process.env.HWVERSION===2;
|
|
|
|
let Layout = require("Layout");
|
|
|
|
let locale = require("locale");
|
|
|
|
let fontHeading = "6x8:2";
|
|
|
|
let fontValue = B2 ? "6x15:2" : "6x8:3";
|
2024-05-14 21:58:02 +00:00
|
|
|
let zoomFont = "12x20:3";
|
2024-05-16 20:30:12 +00:00
|
|
|
let zoomFontSmall = "12x20:2";
|
2023-02-22 21:25:56 +00:00
|
|
|
let headingCol = "#888";
|
|
|
|
let fixCount = 0;
|
2024-04-21 14:48:56 +00:00
|
|
|
const wu = require("widget_utils");
|
2023-02-22 21:25:56 +00:00
|
|
|
|
|
|
|
g.reset().clear();
|
2023-05-31 19:58:19 +00:00
|
|
|
Bangle.loadWidgets();
|
|
|
|
|
|
|
|
// ---------------------------
|
|
|
|
let settings = Object.assign({
|
|
|
|
record: true,
|
|
|
|
B1: "dist",
|
|
|
|
B2: "time",
|
|
|
|
B3: "pacea",
|
|
|
|
B4: "bpm",
|
|
|
|
B5: "step",
|
|
|
|
B6: "caden",
|
|
|
|
paceLength: 1000,
|
2024-05-28 07:35:02 +00:00
|
|
|
alwaysResume: false,
|
2023-05-31 19:58:19 +00:00
|
|
|
notify: {
|
|
|
|
dist: {
|
|
|
|
value: 0,
|
|
|
|
notifications: [],
|
|
|
|
},
|
|
|
|
step: {
|
|
|
|
value: 0,
|
|
|
|
notifications: [],
|
|
|
|
},
|
|
|
|
time: {
|
|
|
|
value: 0,
|
|
|
|
notifications: [],
|
|
|
|
},
|
|
|
|
},
|
2023-02-22 21:25:56 +00:00
|
|
|
HRM: {
|
|
|
|
min: 55,
|
|
|
|
max: 185,
|
|
|
|
},
|
|
|
|
}, require("Storage").readJSON("runplus.json", 1) || {});
|
|
|
|
let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!=="");
|
|
|
|
let exs = ExStats.getStats(statIDs, settings);
|
2023-05-31 19:58:19 +00:00
|
|
|
// ---------------------------
|
|
|
|
|
|
|
|
function setStatus(running) {
|
2024-05-28 07:35:02 +00:00
|
|
|
layout.button.label = running ? "STOP" : "START";
|
|
|
|
layout.status.label = running ? "RUN" : "STOP";
|
2023-05-31 19:58:19 +00:00
|
|
|
layout.status.bgCol = running ? "#0f0" : "#f00";
|
2024-05-14 21:58:02 +00:00
|
|
|
if (screen === "main") layout.render();
|
2023-05-31 19:58:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Called to start/stop running
|
|
|
|
function onStartStop() {
|
2024-05-15 17:03:19 +00:00
|
|
|
if (screen === "karvonen") {
|
|
|
|
// start/stop on the karvonen screen reverts us to the main screen
|
|
|
|
setScreen("main");
|
2024-05-14 21:58:02 +00:00
|
|
|
}
|
2024-05-14 21:27:17 +00:00
|
|
|
|
2023-05-31 19:58:19 +00:00
|
|
|
var running = !exs.state.active;
|
2024-05-28 07:35:02 +00:00
|
|
|
var shouldResume = settings.alwaysResume;
|
2023-05-31 19:58:19 +00:00
|
|
|
var promise = Promise.resolve();
|
|
|
|
|
2024-05-28 07:35:02 +00:00
|
|
|
if (!shouldResume && running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume?
|
2023-05-31 19:58:19 +00:00
|
|
|
promise = promise.
|
|
|
|
then(() => {
|
2024-05-14 21:26:43 +00:00
|
|
|
screen = "menu";
|
2023-05-31 19:58:19 +00:00
|
|
|
return E.showPrompt("Resume run?",{title:"Run"});
|
|
|
|
}).then(r => {
|
2024-05-14 21:26:43 +00:00
|
|
|
screen = "main";
|
2023-10-13 08:03:27 +00:00
|
|
|
layout.setUI(); // grab our input handling again
|
|
|
|
layout.forgetLazyState();
|
|
|
|
layout.render();
|
|
|
|
shouldResume=r;
|
2023-05-31 19:58:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// start/stop recording
|
|
|
|
// Do this first in case recorder needs to prompt for
|
|
|
|
// an overwrite before we start tracking exstats
|
|
|
|
if (settings.record && WIDGETS["recorder"]) {
|
|
|
|
if (running) {
|
2024-05-14 21:26:43 +00:00
|
|
|
screen = "menu";
|
2023-05-31 19:58:19 +00:00
|
|
|
promise = promise.
|
|
|
|
then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })).
|
|
|
|
then(() => {
|
2024-05-14 21:26:43 +00:00
|
|
|
screen = "main";
|
2023-05-31 19:58:19 +00:00
|
|
|
layout.setUI(); // grab our input handling again
|
|
|
|
layout.forgetLazyState();
|
|
|
|
layout.render();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
promise = promise.then(
|
|
|
|
() => WIDGETS["recorder"].setRecording(false)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-22 18:21:32 +00:00
|
|
|
promise.then(() => {
|
2023-05-31 19:58:19 +00:00
|
|
|
if (running) {
|
|
|
|
if (shouldResume)
|
2024-05-14 21:58:02 +00:00
|
|
|
exs.resume();
|
2023-05-31 19:58:19 +00:00
|
|
|
else
|
|
|
|
exs.start();
|
|
|
|
} else {
|
|
|
|
exs.stop();
|
|
|
|
}
|
|
|
|
// if stopping running, don't clear state
|
|
|
|
// so we can at least refer to what we've done
|
|
|
|
setStatus(running);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-14 21:58:02 +00:00
|
|
|
function zoom(statID) {
|
|
|
|
if (screen !== "main") return;
|
|
|
|
|
|
|
|
setScreen("zoom");
|
|
|
|
|
|
|
|
const onTouch = () => {
|
|
|
|
Bangle.removeListener("touch", onTouch);
|
|
|
|
Bangle.removeListener("twist", onTwist);
|
2024-05-16 20:13:57 +00:00
|
|
|
stat.removeListener("changed", draw);
|
2024-05-14 21:58:02 +00:00
|
|
|
setScreen("main");
|
|
|
|
};
|
2024-05-15 17:07:36 +00:00
|
|
|
Bangle.on("touch", onTouch); // queued after layout's touchHandler (otherwise we'd be removed then instantly re-zoomed)
|
2024-05-14 21:58:02 +00:00
|
|
|
|
|
|
|
const onTwist = () => {
|
|
|
|
Bangle.setLCDPower(1);
|
|
|
|
};
|
|
|
|
Bangle.on("twist", onTwist);
|
|
|
|
|
2024-05-16 20:13:57 +00:00
|
|
|
const draw = stat => {
|
2024-05-14 21:58:02 +00:00
|
|
|
const R = Bangle.appRect;
|
|
|
|
|
|
|
|
g.reset()
|
|
|
|
.clearRect(R)
|
|
|
|
.setFontAlign(0, 0);
|
|
|
|
|
|
|
|
layout.render(layout.bottom);
|
|
|
|
|
2024-05-16 20:30:34 +00:00
|
|
|
const value = exs.state.active ? stat.getString() : "____";
|
|
|
|
|
2024-05-14 21:58:02 +00:00
|
|
|
g
|
2024-05-16 20:30:12 +00:00
|
|
|
.setFont(stat.title.length > 5 ? zoomFontSmall : zoomFont)
|
2024-05-14 21:58:02 +00:00
|
|
|
.setColor(headingCol)
|
|
|
|
.drawString(stat.title.toUpperCase(), R.x+R.w/2, R.y+R.h/3)
|
2024-05-15 17:03:37 +00:00
|
|
|
.setColor(g.theme.fg)
|
2024-05-16 20:30:34 +00:00
|
|
|
.drawString(value, R.x+R.w/2, R.y+R.h*2/3);
|
2024-05-14 21:58:02 +00:00
|
|
|
};
|
|
|
|
layout.lazy = false; // restored when we go back to "main"
|
|
|
|
|
2024-05-16 20:13:57 +00:00
|
|
|
const stat = exs.stats[statID];
|
|
|
|
stat.on("changed", draw);
|
|
|
|
draw(stat);
|
2024-05-14 21:58:02 +00:00
|
|
|
}
|
|
|
|
|
2023-02-22 21:25:56 +00:00
|
|
|
let lc = [];
|
2023-05-31 19:58:19 +00:00
|
|
|
// Load stats in pair by pair
|
2023-02-22 21:25:56 +00:00
|
|
|
for (let i=0;i<statIDs.length;i+=2) {
|
|
|
|
let sa = exs.stats[statIDs[i+0]];
|
|
|
|
let sb = exs.stats[statIDs[i+1]];
|
2024-05-14 21:58:02 +00:00
|
|
|
let cba = zoom.bind(null, statIDs[i]);
|
|
|
|
let cbb = zoom.bind(null, statIDs[i+1]);
|
2023-05-31 19:58:19 +00:00
|
|
|
lc.push({ type:"h", filly:1, c:[
|
2024-05-14 21:58:02 +00:00
|
|
|
sa?{type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol, cb: cba }:{},
|
|
|
|
sb?{type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol, cb: cbb }:{}
|
2023-05-31 19:58:19 +00:00
|
|
|
]}, { type:"h", filly:1, c:[
|
2024-05-14 21:58:02 +00:00
|
|
|
sa?{type:"txt", font:fontValue, label:sa.getString(), id:sa.id, fillx:1, cb: cba }:{},
|
|
|
|
sb?{type:"txt", font:fontValue, label:sb.getString(), id:sb.id, fillx:1, cb: cbb }:{}
|
2023-05-31 19:58:19 +00:00
|
|
|
]});
|
|
|
|
if (sa) sa.on('changed', e=>layout[e.id].label = e.getString());
|
|
|
|
if (sb) sb.on('changed', e=>layout[e.id].label = e.getString());
|
|
|
|
}
|
|
|
|
// At the bottom put time/GPS state/etc
|
2024-05-14 21:58:02 +00:00
|
|
|
lc.push({ type:"h", id:"bottom", filly:1, c:[
|
2023-05-31 19:58:19 +00:00
|
|
|
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
|
|
|
|
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
|
|
|
|
{type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 }
|
|
|
|
]});
|
|
|
|
// Now calculate the layout
|
2023-02-22 21:25:56 +00:00
|
|
|
let layout = new Layout( {
|
2023-05-31 19:58:19 +00:00
|
|
|
type:"v", c: lc
|
2024-05-14 21:27:17 +00:00
|
|
|
},{lazy:true, btns:[{ label:"---", cb: onStartStop, id:"button"}]});
|
2023-05-31 19:58:19 +00:00
|
|
|
delete lc;
|
|
|
|
setStatus(exs.state.active);
|
|
|
|
layout.render();
|
|
|
|
|
|
|
|
function configureNotification(stat) {
|
|
|
|
stat.on('notify', (e)=>{
|
|
|
|
settings.notify[e.id].notifications.reduce(function (promise, buzzPattern) {
|
2023-02-22 21:25:56 +00:00
|
|
|
return promise.then(function () {
|
|
|
|
return Bangle.buzz(buzzPattern[0], buzzPattern[1]);
|
|
|
|
});
|
2023-05-31 19:58:19 +00:00
|
|
|
}, Promise.resolve());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Object.keys(settings.notify).forEach((statType) => {
|
|
|
|
if (settings.notify[statType].increment > 0 && exs.stats[statType]) {
|
2023-02-22 21:25:56 +00:00
|
|
|
configureNotification(exs.stats[statType]);
|
2023-05-31 19:58:19 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Handle GPS state change for icon
|
|
|
|
Bangle.on("GPS", function(fix) {
|
|
|
|
layout.gps.bgCol = fix.fix ? "#0f0" : "#f00";
|
|
|
|
if (!fix.fix) return; // only process actual fixes
|
|
|
|
if (fixCount++ === 0) {
|
|
|
|
Bangle.buzz(); // first fix, does not need to respect quiet mode
|
|
|
|
}
|
|
|
|
});
|
2023-02-22 21:25:56 +00:00
|
|
|
|
2024-05-14 21:58:02 +00:00
|
|
|
function setScreen(to) {
|
2024-05-15 17:03:19 +00:00
|
|
|
if (screen === "karvonen") {
|
|
|
|
require("runplus_karvonen").stop();
|
|
|
|
wu.show();
|
|
|
|
Bangle.drawWidgets();
|
2023-02-22 21:25:56 +00:00
|
|
|
}
|
2024-05-14 21:28:15 +00:00
|
|
|
|
2024-04-21 14:48:56 +00:00
|
|
|
if (runInterval) clearInterval(runInterval);
|
2023-02-22 21:25:56 +00:00
|
|
|
runInterval = undefined;
|
2024-04-21 14:48:56 +00:00
|
|
|
g.reset().clearRect(Bangle.appRect);
|
2024-05-14 21:58:02 +00:00
|
|
|
|
2024-05-14 21:28:15 +00:00
|
|
|
screen = to;
|
2024-05-14 21:58:02 +00:00
|
|
|
switch (screen) {
|
|
|
|
case "main":
|
|
|
|
layout.lazy = false;
|
|
|
|
layout.render();
|
|
|
|
layout.lazy = true;
|
|
|
|
// We always call ourselves once a second to update
|
|
|
|
if (!runInterval){
|
|
|
|
runInterval = setInterval(function() {
|
|
|
|
layout.clock.label = locale.time(new Date(),1);
|
|
|
|
if (screen !== "menu") layout.render();
|
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "karvonen":
|
|
|
|
require("runplus_karvonen").start(settings.HRM, exs.stats.bpm);
|
|
|
|
break;
|
|
|
|
}
|
2023-02-22 21:25:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Define the function to go back and forth between the different UI's
|
|
|
|
function swipeHandler(LR,_) {
|
2024-05-14 21:26:43 +00:00
|
|
|
if (screen !== "menu"){
|
|
|
|
if (LR < 0 && screen == "karvonen")
|
2024-05-14 21:58:02 +00:00
|
|
|
setScreen("main");
|
2024-05-14 21:26:43 +00:00
|
|
|
if (LR > 0 && screen !== "karvonen")
|
2024-05-14 21:58:02 +00:00
|
|
|
setScreen("karvonen"); // stop updating and drawing the traditional run app UI
|
2024-04-21 14:48:56 +00:00
|
|
|
}
|
2023-02-22 21:25:56 +00:00
|
|
|
}
|
2024-05-14 21:58:02 +00:00
|
|
|
|
|
|
|
setScreen("main");
|
|
|
|
|
2023-02-22 21:25:56 +00:00
|
|
|
// Listen for swipes with the swipeHandler
|
|
|
|
Bangle.on("swipe", swipeHandler);
|