diff --git a/apps.json b/apps.json index 1cacc920e..70fb6cac7 100644 --- a/apps.json +++ b/apps.json @@ -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", diff --git a/apps/buffgym/.eslintrc.json b/apps/buffgym/.eslintrc.json new file mode 100644 index 000000000..c91a72544 --- /dev/null +++ b/apps/buffgym/.eslintrc.json @@ -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" + ] + } +} \ No newline at end of file diff --git a/apps/buffgym/README.md b/apps/buffgym/README.md new file mode 100644 index 000000000..e9e217828 --- /dev/null +++ b/apps/buffgym/README.md @@ -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. \ No newline at end of file diff --git a/apps/buffgym/buffgym-exercise.js b/apps/buffgym/buffgym-exercise.js new file mode 100644 index 000000000..68e49be84 --- /dev/null +++ b/apps/buffgym/buffgym-exercise.js @@ -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"); + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-icon.js b/apps/buffgym/buffgym-icon.js new file mode 100644 index 000000000..31764acbb --- /dev/null +++ b/apps/buffgym/buffgym-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA=")); \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-a.json b/apps/buffgym/buffgym-program-a.json new file mode 100644 index 000000000..7ebaf3741 --- /dev/null +++ b/apps/buffgym/buffgym-program-a.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-b.json b/apps/buffgym/buffgym-program-b.json new file mode 100644 index 000000000..b93348621 --- /dev/null +++ b/apps/buffgym/buffgym-program-b.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-program-index.json b/apps/buffgym/buffgym-program-index.json new file mode 100644 index 000000000..3bb51f1b5 --- /dev/null +++ b/apps/buffgym/buffgym-program-index.json @@ -0,0 +1,10 @@ +[ + { + "title": "Program A", + "file": "buffgym-program-a.json" + }, + { + "title": "Program B", + "file": "buffgym-program-b.json" + } +] \ No newline at end of file diff --git a/apps/buffgym/buffgym-program.js b/apps/buffgym/buffgym-program.js new file mode 100644 index 000000000..68a069da5 --- /dev/null +++ b/apps/buffgym/buffgym-program.js @@ -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); + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym-scrn1.png b/apps/buffgym/buffgym-scrn1.png new file mode 100755 index 000000000..07b79386f Binary files /dev/null and b/apps/buffgym/buffgym-scrn1.png differ diff --git a/apps/buffgym/buffgym-scrn2.png b/apps/buffgym/buffgym-scrn2.png new file mode 100755 index 000000000..ec70fb791 Binary files /dev/null and b/apps/buffgym/buffgym-scrn2.png differ diff --git a/apps/buffgym/buffgym-scrn3.png b/apps/buffgym/buffgym-scrn3.png new file mode 100755 index 000000000..0888fc507 Binary files /dev/null and b/apps/buffgym/buffgym-scrn3.png differ diff --git a/apps/buffgym/buffgym-scrn4.png b/apps/buffgym/buffgym-scrn4.png new file mode 100755 index 000000000..3078d25dc Binary files /dev/null and b/apps/buffgym/buffgym-scrn4.png differ diff --git a/apps/buffgym/buffgym-scrn5.png b/apps/buffgym/buffgym-scrn5.png new file mode 100755 index 000000000..b34a5b124 Binary files /dev/null and b/apps/buffgym/buffgym-scrn5.png differ diff --git a/apps/buffgym/buffgym-scrn6.png b/apps/buffgym/buffgym-scrn6.png new file mode 100755 index 000000000..563426ad3 Binary files /dev/null and b/apps/buffgym/buffgym-scrn6.png differ diff --git a/apps/buffgym/buffgym-set.js b/apps/buffgym/buffgym-set.js new file mode 100644 index 000000000..4bd12d7ec --- /dev/null +++ b/apps/buffgym/buffgym-set.js @@ -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--; + } +} \ No newline at end of file diff --git a/apps/buffgym/buffgym.app.js b/apps/buffgym/buffgym.app.js new file mode 100755 index 000000000..4dc6ffd5a --- /dev/null +++ b/apps/buffgym/buffgym.app.js @@ -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(); \ No newline at end of file diff --git a/apps/buffgym/buffgym.png b/apps/buffgym/buffgym.png new file mode 100755 index 000000000..9bde64cc4 Binary files /dev/null and b/apps/buffgym/buffgym.png differ