diff --git a/apps/c25k/ChangeLog b/apps/c25k/ChangeLog new file mode 100644 index 000000000..0e7594334 --- /dev/null +++ b/apps/c25k/ChangeLog @@ -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 diff --git a/apps/c25k/README.md b/apps/c25k/README.md new file mode 100644 index 000000000..8237199d5 --- /dev/null +++ b/apps/c25k/README.md @@ -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) diff --git a/apps/c25k/app-icon.js b/apps/c25k/app-icon.js new file mode 100644 index 000000000..6b85dbf29 --- /dev/null +++ b/apps/c25k/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoPk9G9gsj14lZhWq0AEBgtVqALmhQJBAQMFBIICCBc4ADBQYLnAQQKEBcibETQIABHggLiAEQqEh/wgACCBcpXDBAIKDBcqJDh//BQYLkHwg7GBcY7FU5ALgAEQA=")) diff --git a/apps/c25k/app.js b/apps/c25k/app.js new file mode 100644 index 000000000..eed918e46 --- /dev/null +++ b/apps/c25k/app.js @@ -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); diff --git a/apps/c25k/app.png b/apps/c25k/app.png new file mode 100644 index 000000000..6b3a9ba95 Binary files /dev/null and b/apps/c25k/app.png differ diff --git a/apps/c25k/c25k-scrn1.png b/apps/c25k/c25k-scrn1.png new file mode 100644 index 000000000..c4d9ea24b Binary files /dev/null and b/apps/c25k/c25k-scrn1.png differ diff --git a/apps/c25k/c25k-scrn2.png b/apps/c25k/c25k-scrn2.png new file mode 100644 index 000000000..ba064200e Binary files /dev/null and b/apps/c25k/c25k-scrn2.png differ diff --git a/apps/c25k/c25k-scrn3.png b/apps/c25k/c25k-scrn3.png new file mode 100644 index 000000000..6901abf31 Binary files /dev/null and b/apps/c25k/c25k-scrn3.png differ diff --git a/apps/c25k/c25k-scrn4.png b/apps/c25k/c25k-scrn4.png new file mode 100644 index 000000000..ad64da947 Binary files /dev/null and b/apps/c25k/c25k-scrn4.png differ diff --git a/apps/c25k/c25k-scrn5.png b/apps/c25k/c25k-scrn5.png new file mode 100644 index 000000000..ca32abdfa Binary files /dev/null and b/apps/c25k/c25k-scrn5.png differ diff --git a/apps/c25k/c25k-scrn6.png b/apps/c25k/c25k-scrn6.png new file mode 100644 index 000000000..53f5221d7 Binary files /dev/null and b/apps/c25k/c25k-scrn6.png differ diff --git a/apps/c25k/c25k-scrn7.png b/apps/c25k/c25k-scrn7.png new file mode 100644 index 000000000..407afd48b Binary files /dev/null and b/apps/c25k/c25k-scrn7.png differ diff --git a/apps/c25k/c25k-scrn8.png b/apps/c25k/c25k-scrn8.png new file mode 100644 index 000000000..1cd92d876 Binary files /dev/null and b/apps/c25k/c25k-scrn8.png differ diff --git a/apps/c25k/c25k-scrn9.png b/apps/c25k/c25k-scrn9.png new file mode 100644 index 000000000..53dbaad1f Binary files /dev/null and b/apps/c25k/c25k-scrn9.png differ diff --git a/apps/c25k/metadata.json b/apps/c25k/metadata.json new file mode 100644 index 000000000..876926a0c --- /dev/null +++ b/apps/c25k/metadata.json @@ -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"} + ] +}