Merge pull request #3206 from Erovia/c25k

New app: C25k
pull/3237/head
Rob Pilling 2024-02-26 08:50:40 +00:00 committed by GitHub
commit 4281bf3ae0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 421 additions and 0 deletions

4
apps/c25k/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: New App!
0.02: Add rep info to time screen
0.03: Add option to pause/resume workout (Bangle.js 1 only)
0.04: Add possibility of creating a custom exercise

96
apps/c25k/README.md Normal file
View File

@ -0,0 +1,96 @@
# C25K
Unofficial app for the Couch to 5k training plan.
From being a couch-potato to running 5k in 8 weeks!
Each week has 3 training days, ideally with rest days between them.
Each day's programme consists of running for a certain time with occasional walking/resting phases.
When walking is part of the programme, the (run+walk) stages are repeated a number of times.
![](c25k-scrn1.png)
![](c25k-scrn2.png)
![](c25k-scrn3.png)
## Features
- Show remaining time in seconds for each phase
- Vibrates on phase changes
- Keeps screen on to allow quickly glancing at the time while running
- Shows time on button press
## Usage
If you know the week and day of the programme you'd like to start, set `Week` and `Day` to the appropriate values in the main menu and press `Start`.
**Example**:
To start the programme of the **second day** of **week 4**:
![](c25k-scrn4.png)
---
Alternatively, you can go to the `View plan` menu to look at all the programmes and select the one you'd like to start.
**Example**:
Go to the `View plan` menu:
![](c25k-scrn5.png)
Select the programme to start it:
![](c25k-scrn6.png)
---
The format of the `View menu` is `w{week}d{day}(r:{run mins}|w:{walk mins}|x{number of reps})`.
For example `w6d1(r:6|w:3|x2)` means:
`it's the programme of day 1 on week 6`,
`it consists of running for 6 minutes`,
`followed by walking for 3`,
`done 2 times back to back`.
---
### Create a custom excercise
Under the `Custom run` menu, it's possible to create a custom excercise.
![](c25k-scrn9.png)
Some important details/limitations:
- To disable walking: set `walk` to `0`
- When walking is set to `0`, the repetition count is set to `1`.
- When repetition is set to `2` or higher, `walk` is set to `1`.
**Unfortunately, the value in the menu do not update to reflect the changes, so I recommend setting the values with the rules above in mind.**
---
### Show extra info:
If you ever need to peek at the time, just press the middle (or only) physical button on the watch:
![](c25k-scrn7.png)
This view also shows `current rep / total reps` at the top.
---
### Pause/resume workout:
**This is currently only available on Bangle.js 1.**
Press the top button to pause or to resume the active programme:
![](c25k-scrn8.png)
---
## Disclaimer
This app was hacked together in a day with no JS knowledge.
It's probably inefficient and buggy, but it does what I needed it to do: allow me to follow the C25K programme without a phone.
The app was designed with a Bangle.js 1 in mind, as that's the one I have.
It *should* work fine on the Bangle.js 2, but I couldn't test it on real hardware.
---
Made with <3 by [Erovia](https://github.com/Erovia/BangleApps/tree/c25k)

1
apps/c25k/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4X/AoPk9G9gsj14lZhWq0AEBgtVqALmhQJBAQMFBIICCBc4ADBQYLnAQQKEBcibETQIABHggLiAEQqEh/wgACCBcpXDBAIKDBcqJDh//BQYLkHwg7GBcY7FU5ALgAEQA="))

290
apps/c25k/app.js Normal file
View File

@ -0,0 +1,290 @@
var week = 1; // Stock plan: programme week
var day = 1; // Stock plan: programe day
var run = 1; // Custom plan: running time
var walk = 0; // Custom plan: walking time
var reps = 1; // Custom plan: repetition count
var time; // To store the date
var loop; // To store how many times we will have to do a countdown
var rep; // The current rep counter
var counter; // The seconds counter
var currentMode; // Either "run" or "walk"
var mainInterval; // Ticks every second, checking if a new countdown is needed
var activityInterval; // Ticks every second, doing the countdown
var extraInfoWatch; // Watch for button presses to show additional info
var paused = false; // Track pause state
var pauseOrResumeWatch; // Watch for button presses to pause/resume countdown
var defaultFontSize = (process.env.HWVERSION == 2) ? 7 : 9; // Default font size, Banglejs 2 has smaller
var activityBgColour; // Background colour of current activity
var currentActivity; // To store the current activity
function outOfTime() {
buzz();
// Once we're done
if (loop == 0) {
clearWatch(extraInfoWatch); // Don't watch for button presses anymore
if (pauseOrResumeWatch) clearWatch(pauseOrResumeWatch); // Don't watch for button presses anymore
g.setBgColor("#75C0E0"); // Blue background for the "Done" text
drawText("Done", defaultFontSize); // Write "Done" to screen
g.reset();
setTimeout(E.showMenu, 5000, mainmenu); // Show the main menu again after 5secs
clearInterval(mainInterval); // Stop the main interval from starting a new activity
mainInterval = undefined;
currentMode = undefined;
}
}
// Buzz 3 times on state transitions
function buzz() {
Bangle.buzz(500)
.then(() => new Promise(resolve => setTimeout(resolve, 200)))
.then(() => Bangle.buzz(500))
.then(() => new Promise(resolve => setTimeout(resolve, 200)))
.then(() => Bangle.buzz(500));
}
function drawText(text, size){
g.clear();
g.setFontAlign(0, 0); // center font
g.setFont("6x8", size);
g.drawString(text, g.getWidth() / 2, g.getHeight() / 2);
}
function countDown() {
if (!paused) {
var text = "";
var size = defaultFontSize;
if (time) {
var total = ("walk" in currentActivity) ? currentActivity.repetition : 1;
text += rep + "/" + total + "\n"; // Show the current/total rep count when time is shown
size -= 2; // Use smaller font size to fit everything nicely on the screen
}
text += (currentMode === "run") ? "Run\n" + counter : "Walk\n" + counter; // Switches output text
if (time) text += "\n" + time;
drawText(text, size); // draw the current mode and seconds
Bangle.setLCDPower(1); // keep the watch LCD lit up
counter--; // Reduce the seconds
// If the current activity is done
if (counter < 0) {
clearInterval(activityInterval);
activityInterval = undefined;
outOfTime();
return;
}
}
}
function startTimer() {
// If something is already running, do nothing
if (activityInterval) return;
// Switches between the two modes
if (!currentMode || currentMode === "walk") {
currentMode = "run";
rep++; // Increase the rep counter every time a "run" activity starts
counter = currentActivity.run * 60;
activityBgColour = "#ff5733"; // Red background for running
}
else {
currentMode = "walk";
counter = currentActivity.walk * 60;
activityBgColour = "#4da80a"; // Green background for walking
}
g.setBgColor(activityBgColour);
countDown();
if (!activityInterval) {
loop--; // Reduce the number of iterations
activityInterval = setInterval(countDown, 1000); // Start a new activity
}
}
function showTime() {
if (time) return; // If clock is already shown, don't do anything even if the button was pressed again
// Get the time and format it with a leading 0 if necessary
var d = new Date();
var h = d.getHours();
var m = d.getMinutes();
time = h + ":" + m.toString().padStart(2, 0);
setTimeout(function() { time = undefined; }, 5000); // Hide clock after 5secs
}
// Populate the PLAN menu
function populatePlan() {
for (var i = 0; i < PLAN.length; i++) {
for (var j = 0; j < PLAN[i].length; j++) {
// Ever line will have the following format:
// w{week}d{day}(r:{run mins}|w:{walk mins}|x{number of reps})
var name = "w" + (i + 1) + "d" + (j + 1);
if (process.env.HWVERSION == 2) name += "\n"; // Print in 2 lines to accomodate the Bangle.js 2 screen
name += "(r:" + PLAN[i][j].run;
if ("walk" in PLAN[i][j]) name += "|w:" + PLAN[i][j].walk;
if ("repetition" in PLAN[i][j]) name += "|x" + PLAN[i][j].repetition;
name += ")";
// Each menu item will have a function that start the program at the selected day
planmenu[name] = getFunc(i, j);
}
}
}
// Helper function to generate functions for the activePlan menu
function getFunc(i, j) {
return function() {
currentActivity = PLAN[i][j];
startActivity();
};
}
function startActivity() {
loop = ("walk" in currentActivity) ? currentActivity.repetition * 2 : 1;
rep = 0;
E.showMenu(); // Hide the main menu
extraInfoWatch = setWatch(showTime, (process.env.HWVERSION == 2) ? BTN1 : BTN2, {repeat: true}); // Show the clock on button press
if (process.env.HWVERSION == 1) pauseOrResumeWatch = setWatch(pauseOrResumeActivity, BTN1, {repeat: true}); // Pause or resume on button press (Bangle.js 1 only)
buzz();
mainInterval = setInterval(function() {startTimer();}, 1000); // Check every second if we need to do something
}
// Pause or resume current activity
function pauseOrResumeActivity() {
paused = !paused;
buzz();
if (paused) {
g.setBgColor("#fdd835"); // Yellow background for pause screen
drawText("Paused", (process.env.HWVERSION == 2) ? defaultFontSize - 3 : defaultFontSize - 2); // Although the font size is configured here, this feature does not work on Bangle.js 2 as the only physical button is tied to the extra info screen already
}
else {
g.setBgColor(activityBgColour);
}
}
const PLAN = [
[
{"run": 1, "walk": 1.5, "repetition": 8},
{"run": 1, "walk": 1.5, "repetition": 8},
{"run": 1, "walk": 1.5, "repetition": 8},
],
[
{"run": 1.5, "walk": 2, "repetition": 6},
{"run": 1.5, "walk": 2, "repetition": 6},
{"run": 1.5, "walk": 2, "repetition": 6},
],
[
{"run": 2, "walk": 2, "repetition": 5},
{"run": 2.5, "walk": 2.5, "repetition": 4},
{"run": 2.5, "walk": 2.5, "repetition": 4},
],
[
{"run": 3, "walk": 2, "repetition": 5},
{"run": 3, "walk": 2, "repetition": 5},
{"run": 4, "walk": 2.5, "repetition": 3},
],
[
{"run": 5, "walk": 2, "repetition": 3},
{"run": 8, "walk": 5, "repetition": 2},
{"run": 20},
],
[
{"run": 6, "walk": 3, "repetition": 2},
{"run": 10, "walk": 3, "repetition": 2},
{"run": 25},
],
[
{"run": 25},
{"run": 25},
{"run": 25},
],
[
{"run": 30},
{"run": 30},
{"run": 30},
],
];
var customRun = {"run": 1};
// Main menu
var mainmenu = {
"": { "title": "-- C25K --" },
"Week": {
value: week,
min: 1, max: PLAN.length, step: 1,
onchange : v => { week = v; }
},
"Day": {
value: day,
min: 1, max: 3, step: 1,
onchange: v => { day = v; }
},
"View plan": function() { E.showMenu(planmenu); },
"Custom run": function() { E.showMenu(custommenu); },
"Start": function() {
currentActivity = PLAN[week - 1][day -1];
startActivity();
},
"Exit": function() { load(); },
};
// Plan view
var planmenu = {
"": { title: "-- Plan --" },
"< Back": function() { E.showMenu(mainmenu);},
};
// Custom view
var custommenu = {
"": { title : "-- Cust. run --" },
"< Back": function() { E.showMenu(mainmenu);},
"Run (mins)": {
value: run,
min: 1, max: 150, step: 1,
wrap: true,
onchange: v => { customRun.run = v; }
},
"Walk (mins)": {
value: walk,
min: 0, max: 10, step: 1,
onchange: v => {
if (v > 0) {
if (reps == 1) { reps = 2; } // Walking only makes sense with multiple reps
customRun.repetition = reps;
customRun.walk = v;
}
else {
// If no walking, delete both the reps and walk data
delete customRun.repetition;
delete customRun.walk;
}
walk = v;
}
},
"Reps": {
value: reps,
min: 1, max: 10, step: 1,
onchange: v => {
if (v > 1) {
if (walk == 0) { walk = 1; } // Multiple reps only make sense with walking phases
customRun.walk = walk;
customRun.repetition = v;
}
else {
// If no multiple reps, delete both the reps and walk data
delete customRun.repetition;
delete customRun.walk;
}
reps = v;
}
},
"Start": function() { currentActivity = customRun; startActivity(); }
};
// Populate the activePlan menu view
populatePlan();
// Actually display the menu
E.showMenu(mainmenu);

BIN
apps/c25k/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

BIN
apps/c25k/c25k-scrn1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/c25k/c25k-scrn2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/c25k/c25k-scrn3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/c25k/c25k-scrn4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/c25k/c25k-scrn5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/c25k/c25k-scrn6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
apps/c25k/c25k-scrn7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
apps/c25k/c25k-scrn8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/c25k/c25k-scrn9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

30
apps/c25k/metadata.json Normal file
View File

@ -0,0 +1,30 @@
{
"id": "c25k",
"name": "C25K",
"icon": "app.png",
"version":"0.04",
"description": "Unofficial app for the Couch to 5k training plan",
"readme": "README.md",
"type": "app",
"tags": "running,c25k,tool,outdoors,exercise",
"allow_emulator": true,
"supports": [
"BANGLEJS",
"BANGLEJS2"
],
"storage": [
{"name": "c25k.app.js", "url": "app.js"},
{"name": "c25k.img", "url": "app-icon.js", "evaluate": true}
],
"screenshots": [
{"url": "c25k-scrn1.png"},
{"url": "c25k-scrn2.png"},
{"url": "c25k-scrn3.png"},
{"url": "c25k-scrn4.png"},
{"url": "c25k-scrn5.png"},
{"url": "c25k-scrn6.png"},
{"url": "c25k-scrn7.png"},
{"url": "c25k-scrn8.png"},
{"url": "c25k-scrn9.png"}
]
}