Merge pull request #339 from paulcockrell/master

BuffGym - Gym training program
pull/344/head
Gordon Williams 2020-04-22 08:51:07 +01:00 committed by GitHub
commit c8bb6dae5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 691 additions and 0 deletions

View File

@ -1321,6 +1321,28 @@
}
]
},
{
"id": "buffgym",
"name": "BuffGym",
"icon": "buffgym.png",
"version":"0.01",
"description": "BuffGym is the famous 5x5 workout program for the BangleJS",
"tags": "tool,outdoors,gym,exercise",
"type": "app",
"allow_emulator": false,
"readme": "README.md",
"storage": [
{"name":"buffgym"},
{"name":"buffgym.app.js", "url": "buffgym.app.js"},
{"name":"buffgym-set.js","url":"buffgym-set.js"},
{"name":"buffgym-exercise.js","url":"buffgym-exercise.js"},
{"name":"buffgym-program.js","url":"buffgym-program.js"},
{"name":"buffgym-program-a.json","url":"buffgym-program-a.json"},
{"name":"buffgym-program-b.json","url":"buffgym-program-b.json"},
{"name":"buffgym-program-index.json","url":"buffgym-program-index.json"},
{"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true}
]
},
{
"id": "banglerun",
"name": "BangleRun",

View File

@ -0,0 +1,33 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
}

43
apps/buffgym/README.md Normal file
View File

@ -0,0 +1,43 @@
# BuffGym
This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program.
## Usage
When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start
![](buffgym-scrn1.png)
You are then presented with the programs menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the program.
![](buffgym-scrn2.png)
You will now begin moving through the exercises in the program. You will see the exercise information on the display.
1. At the top is the exercise name, e.g 'Squats'
2. Next is the weight you must train
3. In the center is where you record the number of *reps* you completed (more on that shortly)
4. Below the *reps* value, is the target reps you must try to reach.
5. Below the target reps is the current set you are training, out of the total sets for the exercise.
6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps
![](buffgym-scrn3.png)
You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise.
![](buffgym-scrn4.png)
![](buffgym-scrn5.png)
Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are.
![](buffgym-scrn6.png)
## Features
* If you successfully complete all reps and sets for an exercise, it will automatically update your weights for next time
* Has a neat rest timer to make sure you are training optimally
* Doesn't require a mobile phone, most 'smart watches' are just a visual presentation of the mobile phone app, this runs purley on the watch. So why not leave your phone and its distractions out of the gym!
* Clear and simple user interface
## Created by
[Paul Cockrell](https://github.com/paulcockrell) April 2020.

View File

@ -0,0 +1,144 @@
exports = class Exercise {
constructor(params) {
this.title = params.title;
this.weight = params.weight;
this.unit = params.unit;
this.restPeriod = params.restPeriod;
this.completed = false;
this.sets = [];
this._restTimeout = null;
this._restInterval = null;
this._state = null;
this._originalRestPeriod = params.restPeriod;
this._weightIncrement = params.weightIncrement || 2.5;
}
get humanTitle() {
return `${this.title} ${this.weight}${this.unit}`;
}
get subTitle() {
const totalSets = this.sets.length;
const uncompletedSets = this.sets.filter((set) => !set.isCompleted()).length;
const currentSet = (totalSets - uncompletedSets) + 1;
return `Set ${currentSet} of ${totalSets}`;
}
decRestPeriod() {
this.restPeriod--;
}
addSet(set) {
this.sets.push(set);
}
currentSet() {
return this.sets.filter(set => !set.isCompleted())[0];
}
isLastSet() {
return this.sets.filter(set => !set.isCompleted()).length === 1;
}
isCompleted() {
return !!this.completed;
}
canSetCompleted() {
return this.sets.filter(set => set.isCompleted()).length === this.sets.length;
}
setCompleted() {
if (!this.canSetCompleted()) throw "All sets must be completed";
if (this.canProgress()) this.weight += this._weightIncrement;
this.completed = true;
}
canProgress() {
let completedRepsTotalSum = 0;
let targetRepsTotalSum = 0;
this.sets.forEach(set => completedRepsTotalSum += set.reps);
this.sets.forEach(set => targetRepsTotalSum += set.maxReps);
return (targetRepsTotalSum - completedRepsTotalSum) === 0;
}
startRestTimer(program) {
this._restTimeout = setTimeout(() => {
this.next(program);
}, 1000 * this.restPeriod);
this._restInterval = setInterval(() => {
program.emit("redraw");
}, 1000 );
}
resetRestTimer() {
clearTimeout(this._restTimeout);
clearInterval(this._restInterval);
this._restTimeout = null;
this._restInterval = null;
this.restPeriod = this._originalRestPeriod;
}
isRestTimerRunning() {
return this._restTimeout != null;
}
setupStartedButtons(program) {
clearWatch();
setWatch(() => {
this.currentSet().incReps();
program.emit("redraw");
}, BTN1, {repeat: true});
setWatch(program.next.bind(program), BTN2, {repeat: false});
setWatch(() => {
this.currentSet().decReps();
program.emit("redraw");
}, BTN3, {repeat: true});
}
setupRestingButtons(program) {
clearWatch();
setWatch(program.next.bind(program), BTN2, {repeat: false});
}
next(program) {
const STARTED = 1;
const RESTING = 2;
const COMPLETED = 3;
switch(this._state) {
case null:
this._state = STARTED;
this.setupStartedButtons(program);
break;
case STARTED:
this._state = RESTING;
this.startRestTimer(program);
this.setupRestingButtons(program);
break;
case RESTING:
this.resetRestTimer();
this.currentSet().setCompleted();
if (this.canSetCompleted()) {
this._state = COMPLETED;
this.setCompleted();
} else {
this._state = null;
}
// As we are changing state and require it to be reprocessed
// invoke the next step of program
program.next();
break;
default:
throw "Exercise: Attempting to move to an unknown state";
}
program.emit("redraw");
}
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA="));

View File

@ -0,0 +1,33 @@
{
"title": "Program A",
"exercises": [
{
"title": "Squats",
"weight": 40,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Overhead press",
"weight": 20,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Deadlift",
"weight": 20,
"unit": "Kg",
"sets": [5],
"restPeriod": 90
},
{
"title": "Pullups",
"weight": 0,
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
}

View File

@ -0,0 +1,33 @@
{
"title": "Program B",
"exercises": [
{
"title": "Squats",
"weight": 40,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Bench press",
"weight": 20,
"unit": "Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Row",
"weight": 20,
"unit":"Kg",
"sets": [5, 5, 5, 5, 5],
"restPeriod": 90
},
{
"title": "Tricep extension",
"weight": 20,
"unit": "Kg",
"sets": [10, 10, 10],
"restPeriod": 90
}
]
}

View File

@ -0,0 +1,10 @@
[
{
"title": "Program A",
"file": "buffgym-program-a.json"
},
{
"title": "Program B",
"file": "buffgym-program-b.json"
}
]

View File

@ -0,0 +1,56 @@
exports = class Program {
constructor(params) {
this.title = params.title;
this.exercises = [];
this.completed = false;
this.on("redraw", redraw.bind(null, this));
}
addExercises(exercises) {
exercises.forEach(exercise => this.exercises.push(exercise));
}
currentExercise() {
return this.exercises.filter(exercise => !exercise.isCompleted())[0];
}
canComplete() {
return this.exercises.filter(exercise => exercise.isCompleted()).length === this.exercises.length;
}
setCompleted() {
if (!this.canComplete()) throw "All exercises must be completed";
this.completed = true;
}
isCompleted() {
return !!this.completed;
}
toJSON() {
return {
title: this.title,
exercises: this.exercises.map(exercise => {
return {
title: exercise.title,
weight: exercise.weight,
unit: exercise.unit,
sets: exercise.sets.map(set => set.maxReps),
restPeriod: exercise.restPeriod,
};
}),
};
}
// State machine
next() {
if (this.canComplete()) {
this.setCompleted();
this.emit("redraw");
return;
}
// Call current exercise state machine
this.currentExercise().next(this);
}
}

BIN
apps/buffgym/buffgym-scrn1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
apps/buffgym/buffgym-scrn2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
apps/buffgym/buffgym-scrn3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
apps/buffgym/buffgym-scrn4.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
apps/buffgym/buffgym-scrn5.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
apps/buffgym/buffgym-scrn6.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,28 @@
exports = class Set {
constructor(maxReps) {
this.minReps = 0;
this.maxReps = maxReps;
this.reps = 0;
this.completed = false;
}
isCompleted() {
return !!this.completed;
}
setCompleted() {
this.completed = true;
}
incReps() {
if (this.completed) return;
if (this.reps >= this.maxReps) return;
this.reps++;
}
decReps() {
if (this.completed) return;
if (this.reps <= this.minReps) return;
this.reps--;
}
}

288
apps/buffgym/buffgym.app.js Executable file
View File

@ -0,0 +1,288 @@
/**
* BangleJS Stronglifts 5x5 training aid
*
* Original Author: Paul Cockrell https://github.com/paulcockrell
* Created: April 2020
*
* Inspired by:
* - Stronglifts 5x5 training program https://stronglifts.com/5x5/
* - Stronglifts smart watch app
*/
Bangle.setLCDMode("120x120");
const W = g.getWidth();
const H = g.getHeight();
const RED = "#d32e29";
const PINK = "#f05a56";
const WHITE = "#ffffff";
function drawMenu(params) {
const hs = require("heatshrink");
const incImg = hs.decompress(atob("gsFwMAkM+oUA"));
const decImg = hs.decompress(atob("gsFwIEBnwCBA"));
const okImg = hs.decompress(atob("gsFwMAhGFo0A"));
const DEFAULT_PARAMS = {
showBTN1: false,
showBTN2: false,
showBTN3: false,
};
const p = Object.assign({}, DEFAULT_PARAMS, params);
if (p.showBTN1) g.drawImage(incImg, W - 10, 10);
if (p.showBTN2) g.drawImage(okImg, W - 10, 60);
if (p.showBTN3) g.drawImage(decImg, W - 10, 110);
}
function drawSet(exercise) {
const set = exercise.currentSet();
if (set.isCompleted()) return;
g.clear();
// Draw exercise title
g.setColor(PINK);
g.fillRect(15, 0, W - 15, 18);
g.setFontAlign(0, -1);
g.setFont("6x8", 1);
g.setColor(WHITE);
g.drawString(exercise.title, W / 2, 5);
g.setFont("6x8", 1);
g.drawString(exercise.weight + " " + exercise.unit, W / 2, 27);
// Draw completed reps counter
g.setFontAlign(0, 0);
g.setColor(PINK);
g.fillRect(15, 42, W - 15, 80);
g.setColor(WHITE);
g.setFont("6x8", 5);
g.drawString(set.reps, (W / 2) + 2, (H / 2) + 1);
g.setFont("6x8", 1);
const note = `Target reps: ${set.maxReps}`;
g.drawString(note, W / 2, H - 24);
// Draw sets monitor
g.drawString(exercise.subTitle, W / 2, H - 12);
drawMenu({showBTN1: true, showBTN2: true, showBTN3: true});
g.flip();
}
function drawProgDone() {
const title1 = "You did";
const title2 = "GREAT!";
const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest.";
clearWatch();
setWatch(Bangle.showLauncher, BTN2, {repeat: false});
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString(title1, W / 2, 10);
g.drawString(title2, W / 2, 30);
g.setFont("6x8", 1);
g.drawString(msg, (W / 2) + 3, 70);
g.flip();
}
function drawSetComp() {
const title = "Good work";
const msg = "No need to rest\nmove straight on\nto the next\nexercise.Your\nweight has been\nincreased for\nnext time!";
g.clear();
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString(title, W / 2, 10);
g.setFont("6x8", 1);
g.drawString(msg, (W / 2) - 2, 45);
g.flip();
}
function drawRestTimer(program) {
const exercise = program.currentExercise();
const motivation = "Take a breather..";
if (exercise.restPeriod <= 0) {
exercise.resetRestTimer();
program.next();
return;
}
g.clear();
drawMenu({showBTN2: true});
g.setFontAlign(0, -1);
g.setColor(PINK);
g.fillRect(15, 42, W - 15, 80);
g.setColor(WHITE);
g.setFont("6x8", 1);
g.drawString("Have a short\nrest period.", W / 2, 10);
g.setFont("6x8", 5);
g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19);
g.flip();
exercise.decRestPeriod();
}
function redraw(program) {
const exercise = program.currentExercise();
g.clear();
if (program.isCompleted()) {
saveProg(program);
drawProgDone(program);
return;
}
if (exercise.isRestTimerRunning()) {
if (exercise.isLastSet()) {
drawSetComp(program);
} else {
drawRestTimer(program);
}
return;
}
drawSet(exercise);
}
function drawProgMenu(programs, selProgIdx) {
g.clear();
g.setFontAlign(0, -1);
g.setColor(WHITE);
g.setFont("6x8", 2);
g.drawString("BuffGym", W / 2, 10);
g.setFont("6x8", 1);
g.setFontAlign(-1, -1);
let selectedProgram = programs[selProgIdx].title;
let yPos = 50;
programs.forEach(program => {
g.setColor("#f05a56");
g.fillRect(0, yPos, W, yPos + 11);
g.setColor("#ffffff");
if (selectedProgram === program.title) {
g.drawRect(0, yPos, W - 1, yPos + 11);
}
g.drawString(program.title, 10, yPos + 2);
yPos += 15;
});
g.flip();
}
function setupMenu() {
clearWatch();
const progs = getProgIndex();
let selProgIdx = 0;
drawProgMenu(progs, selProgIdx);
setWatch(()=>{
selProgIdx--;
if (selProgIdx< 0) selProgIdx = 0;
drawProgMenu(progs, selProgIdx);
}, BTN1, {repeat: true});
setWatch(()=>{
const prog = buildProg(progs[selProgIdx].file);
prog.next();
}, BTN2, {repeat: false});
setWatch(()=>{
selProgIdx++;
if (selProgIdx > progs.length - 1) selProgIdx = progs.length - 1;
drawProgMenu(progs, selProgIdx);
}, BTN3, {repeat: true});
}
function drawSplash() {
g.reset();
g.setBgColor(RED);
g.clear();
g.setColor(WHITE);
g.setFontAlign(0,-1);
g.setFont("6x8", 2);
g.drawString("BuffGym", W / 2, 10);
g.setFont("6x8", 1);
g.drawString("5x5", W / 2, 42);
g.drawString("training app", W / 2, 55);
g.drawRect(19, 38, 100, 99);
const img = require("heatshrink").decompress(atob("lkdxH+AB/I5ASQACwpB5vNFkwpBAIfNFdZZkFYwskFZAsiFZBZiVYawEFf6ETFUwsIFUYmB54ADAwIskFYoRKBoIroB4grV58kkgCDFRotWFZwqHFiwYMFZIsTC5wLDFjGlCoWlkgJDRQIABCRAsLCwodCFAIABCwIOCFQYABr4RCCQIrMC4gqEAAwpFFZosFC5ArHFQ4rFNYQrGEgosMBxIrFLQwrLAB4sFSw4rFFjYrQFi4rNbASeEFjIoJFQYsGMAgAPEQgAIGwosCRoorbA="));
g.drawImage(img, 40, 70);
g.flip();
let flasher = false;
let bgCol, txtCol;
const i = setInterval(() => {
if (flasher) {
bgCol = WHITE;
txtCol = RED;
} else {
bgCol = RED;
txtCol = WHITE;
}
flasher = !flasher;
g.setColor(bgCol);
g.fillRect(0, 108, W, 120);
g.setColor(txtCol);
g.drawString("Press btn to begin", W / 2, 110);
g.flip();
}, 250);
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN1, {repeat: false});
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN2, {repeat: false});
setWatch(()=>{
clearInterval(i);
setupMenu();
}, BTN3, {repeat: false});
}
function getProgIndex() {
const progIdx = require("Storage").readJSON("buffgym-program-index.json");
return progIdx;
}
function buildProg(fName) {
const Set = require("buffgym-set.js");
const Exercise = require("buffgym-exercise.js");
const Program = require("buffgym-program.js");
const progJSON = require("Storage").readJSON(fName);
const prog = new Program({
title: progJSON.title,
});
const exercises = progJSON.exercises.map(exerciseJSON => {
const exercise = new Exercise({
title: exerciseJSON.title,
weight: exerciseJSON.weight,
unit: exerciseJSON.unit,
restPeriod: exerciseJSON.restPeriod,
});
exerciseJSON.sets.forEach(setJSON => {
exercise.addSet(new Set(setJSON));
});
return exercise;
});
prog.addExercises(exercises);
return prog;
}
function saveProg(program) {
const fName = getProgIndex().find(prog => prog.title === program.title).file;
require("Storage").writeJSON(fName, program.toJSON());
}
drawSplash();

BIN
apps/buffgym/buffgym.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB