diff --git a/apps/smartrep/logic/bench_press.js b/apps/smartrep/logic/bench_press.js new file mode 100644 index 0000000000..a201d62d27 --- /dev/null +++ b/apps/smartrep/logic/bench_press.js @@ -0,0 +1,62 @@ + +let alpha = 0.2; +let ema = null; +let win = []; +let windowSize = 20; + +let repCount = 0; +let state = "down"; +let lastPeakT = 0, lastValleyT = 0; +let lastPeakV = 0, lastValleyV = 0; + +let minRepTime = 300; +let minAmplitude = 15; +let tracking = false; + +exports.start = function(onRep) { + repCount = 0; + ema = null; + state = "down"; + lastPeakT = lastValleyT = (getTime() * 1000) | 0; + lastPeakV = lastValleyV = 0; + win = []; + tracking = true; + + Bangle.setPollInterval(40); + Bangle.on("accel", function handler(a) { + let angle = Math.atan2(a.x, a.z) * 180 / Math.PI; + if (ema === null) ema = angle; + ema = alpha * angle + (1 - alpha) * ema; + + win.push(ema); + if (win.length > windowSize) win.shift(); + let mx = Math.max(...win); + let mn = Math.min(...win); + let mean = win.reduce((a, b) => a + b) / win.length; + + let upTh = mean + 0.3 * (mx - mn); + let downTh = mean - 0.3 * (mx - mn); + let now = (getTime() * 1000) | 0; + + if (state === "down" && ema > upTh && now - lastValleyT > minRepTime) { + state = "up"; + lastPeakT = now; + lastPeakV = ema; + } else if (state === "up" && ema < downTh && now - lastPeakT > minRepTime) { + state = "down"; + lastValleyT = now; + lastValleyV = ema; + let amp = Math.abs(lastPeakV - lastValleyV); + if (amp >= minAmplitude) { + repCount++; + onRep(); + Bangle.buzz(); + } + } + }); +}; + +exports.stop = function() { + tracking = false; + Bangle.removeAllListeners("accel"); +}; diff --git a/apps/smartrep/logic/bicep_curl.js b/apps/smartrep/logic/bicep_curl.js new file mode 100644 index 0000000000..30857ac07d --- /dev/null +++ b/apps/smartrep/logic/bicep_curl.js @@ -0,0 +1,86 @@ +// === Bicep Curl Rep Counter for Bangle.js 1 === + +// ==== Configuration ==== +const POLLING_INTERVAL = 40; // ~25Hz sampling +const THRESHOLD_UP = 0.85; // Detected upward motion (from dataset) +const THRESHOLD_DOWN = 0.35; // Detected downward motion (from dataset) +const MIN_REP_INTERVAL = 500; // Minimum 0.5 sec between reps + +// ==== State Variables ==== +let repCount = 0; +let inCurl = false; +let lastRepTime = 0; +let tracking = false; + +// ==== UI Functions ==== +function drawStatus() { + g.clearRect(0, 40, 240, 120); + g.setFont("6x8", 3); + g.drawString("Reps: " + repCount, 20, 60); + g.setFont("6x8", 2); + g.drawString(inCurl ? "Up" : "Down", 100, 100); +} + +function showStartScreen() { + g.clear(); + g.setFont("6x8", 2); + g.drawString("Bicep Curl Tracker", 10, 10); + drawStatus(); +} + +function showStopScreen() { + g.clear(); + g.setFont("6x8", 2); + g.drawString("Tracking Stopped", 20, 60); +} + +// ==== Accelerometer Handler ==== +function onAccel(data) { + let z = data.z; + let now = getTime() * 1000; + + if (!inCurl && z > THRESHOLD_UP) { + inCurl = true; + } + + if (inCurl && z < THRESHOLD_DOWN && (now - lastRepTime > MIN_REP_INTERVAL)) { + repCount++; + lastRepTime = now; + inCurl = false; + drawStatus(); + } +} + +// ==== Control Functions ==== +function startTracking() { + if (tracking) return; + tracking = true; + repCount = 0; + inCurl = false; + showStartScreen(); + Bangle.setPollInterval(POLLING_INTERVAL); + Bangle.on('accel', onAccel); + Bangle.setLCDPower(1); +} + +function stopTracking() { + if (!tracking) return; + tracking = false; + Bangle.removeListener('accel', onAccel); + showStopScreen(); +} + +function resetReps() { + repCount = 0; + drawStatus(); +} + +// ==== Button Bindings ==== +setWatch(startTracking, BTN1, { repeat: true, edge: "rising" }); +setWatch(stopTracking, BTN2, { repeat: true, edge: "rising" }); +setWatch(resetReps, BTN3, { repeat: true, edge: "rising" }); + +// ==== Init Display ==== +g.clear(); +g.setFont("6x8", 2); +g.drawString("Press BTN1 to Start", 10, 50); diff --git a/apps/smartrep/logic/cable_rows.js b/apps/smartrep/logic/cable_rows.js new file mode 100644 index 0000000000..940273caef --- /dev/null +++ b/apps/smartrep/logic/cable_rows.js @@ -0,0 +1,40 @@ +let pollingInterval = 40; +let thresholdPull = 0.85; // pulling motion +let thresholdRelease = 0.35; // return motion +let minInterval = 500; // 0.5s between rep + +let repCount = 0; +let inPull = false; +let lastRepTime = 0; +let tracking = false; + +exports.start = function(onRep) { + if (tracking) return; + tracking = true; + repCount = 0; + inPull = false; + lastRepTime = 0; + + Bangle.setPollInterval(pollingInterval); + Bangle.on('accel', function handler(a) { + let y = a.y; + let now = getTime() * 1000; + + if (!inPull && y > thresholdPull) inPull = true; + + if (inPull && y < thresholdRelease && now - lastRepTime > minInterval) { + lastRepTime = now; + inPull = false; + repCount++; + onRep(); + Bangle.buzz(); + } + }); +}; + +exports.stop = function() { + if (!tracking) return; + tracking = false; + Bangle.removeAllListeners('accel'); +}; + diff --git a/apps/smartrep/logic/lateral_raise.js b/apps/smartrep/logic/lateral_raise.js new file mode 100644 index 0000000000..02eee66600 --- /dev/null +++ b/apps/smartrep/logic/lateral_raise.js @@ -0,0 +1,87 @@ +// === Lateral Arm Raise Rep Counter for Bangle.js 1 === +// Tracks up-down motion using x-axis and gives feedback on each rep. + +// === Display Title === +g.clear(); +g.setFont("6x8", 2); +g.drawString("Lateral Raise Tracker", 10, 10); + +// === Configuration === +let pollingInterval = 40; // ~25Hz polling rate +let thresholdUp = 0.6; // X-axis threshold for upward motion +let thresholdDown = 0.2; // X-axis threshold for downward motion +let minInterval = 600; // Minimum time between reps (ms) + +// === State Variables === +let repCount = 0; // Number of reps counted +let inRaise = false; // Whether arm is currently rising +let lastRepTime = 0; // Last rep timestamp (ms) +let tracking = false; // Whether tracking is active + +// === Draw Rep Status === +function drawStatus() { + g.clearRect(0, 40, 240, 120); + g.setFont("6x8", 3); + g.drawString("Reps: " + repCount, 20, 60); + g.setFont("6x8", 2); + g.drawString(inRaise ? "Up" : "Down", 100, 100); +} + +// === Feedback (Vibration + Optional Beep) === +function giveFeedback() { + Bangle.buzz(200); // 200ms vibration + Bangle.beep(); // Optional sound +} + +// === Accelerometer Handler === +function onAccel(a) { + let x = a.x; + let now = getTime() * 1000; // Convert to ms + + // Detect upward motion + if (!inRaise && x > thresholdUp) { + inRaise = true; + } + + // Detect downward motion + validate full rep + if (inRaise && x < thresholdDown && (now - lastRepTime > minInterval)) { + repCount++; + lastRepTime = now; + inRaise = false; + drawStatus(); + giveFeedback(); + } +} + +// === Start Tracking === +function startTracking() { + if (tracking) return; + tracking = true; + repCount = 0; + inRaise = false; + g.clear(); + g.setFont("6x8", 2); + g.drawString("Lateral Raise Tracker", 10, 10); + drawStatus(); + Bangle.setPollInterval(pollingInterval); + Bangle.on('accel', onAccel); + Bangle.setLCDPower(1); +} + +// === Stop Tracking === +function stopTracking() { + if (!tracking) return; + tracking = false; + Bangle.removeListener('accel', onAccel); + g.clear(); + g.setFont("6x8", 2); + g.drawString("Tracking Stopped", 20, 60); +} + +// === Button Bindings === +setWatch(startTracking, BTN1, { repeat: true, edge: "rising" }); +setWatch(stopTracking, BTN2, { repeat: true, edge: "rising" }); +setWatch(() => { + repCount = 0; + drawStatus(); +}, BTN3, { repeat: true, edge: "rising" }); diff --git a/apps/smartrep/logic/pushup.js b/apps/smartrep/logic/pushup.js new file mode 100644 index 0000000000..975eeea09f --- /dev/null +++ b/apps/smartrep/logic/pushup.js @@ -0,0 +1,63 @@ +// === Push-Up Detector for Bangle.js 1 === + +// === Configuration === +const POLLING_INTERVAL = 40; // Check accelerometer every 40ms (~25Hz) +const WINDOW_DURATION = 2000; // Analyze 2 seconds of motion data + +// === Variables === +let samples = []; // Stores recent accelerometer data + +// === Show title === +function showHeader() { + g.clear(); + g.setFont("6x8", 2); + g.drawString("Push-Up Detector", 10, 10); +} + +// === Show current state (push-up or rest) === +function showPrediction(text) { + g.clearRect(0, 40, 240, 100); + g.setFont("6x8", 3); + g.drawString(text, 20, 50); +} + +// === Extract average Z-axis acceleration === +function computeFeatures(samples) { + let sumZ = samples.reduce((sum, s) => sum + s.z, 0); + let az_mean = sumZ / samples.length; + return { az_mean }; +} + +// === Classify motion as push-up or rest based on Z-axis average === +function classify(features) { + return features.az_mean <= -0.34885 ? 1 : 0; // 1 = push-up, 0 = rest +} + +// === Handle new accelerometer data === +function onAccel(data) { + let t = getTime(); + samples.push({ t: t, z: data.z }); + + // Keep only the last 2 seconds of samples + samples = samples.filter(s => t - s.t <= WINDOW_DURATION / 1000); + + // Classify if enough data is collected + if (samples.length >= 10) { + let features = computeFeatures(samples); + let result = classify(features); + showPrediction(result === 1 ? "Push-Up!" : "Resting"); + } +} + +// === Start Tracking === +showHeader(); +Bangle.setPollInterval(POLLING_INTERVAL); +Bangle.on('accel', onAccel); + +// === Stop Tracking on BTN1 === +setWatch(() => { + Bangle.removeListener('accel', onAccel); + g.clear(); + g.setFont("6x8", 2); + g.drawString("Stopped", 30, 60); +}, BTN1, { repeat: false, edge: "rising" }); diff --git a/apps/smartrep/logic/shoulder_press.js b/apps/smartrep/logic/shoulder_press.js new file mode 100644 index 0000000000..fc2ed9e9d0 --- /dev/null +++ b/apps/smartrep/logic/shoulder_press.js @@ -0,0 +1,96 @@ +// === Shoulder Press Rep Counter for Bangle.js 1 === + +// === Configurable Parameters === +var alpha = 0.2; // Smoothing factor for EMA (Exponential Moving Average) +var windowSize = 20; // Number of recent EMA values to track (sliding window) +var minRepTime = 300; // Minimum time (ms) between rep phases +var minAmplitude = 15; // Minimum angle swing (degrees) to count a valid rep + +// === State Variables === +var ema = null; // Current smoothed angle +var win = []; // Sliding window of recent EMA values +var repCount = 0; // Number of reps counted +var state = "down"; // Current movement state +var lastPeakT = 0, lastValleyT = 0; // Timestamps of last peak/valley +var lastPeakV = 0, lastValleyV = 0; // EMA values at last peak/valley +var accelListener = null; // Handle to accelerometer listener + +// === UI Display === +function showMessage(msg, big) { + big = !!big; + g.clear(); + g.setFont(big ? "Vector" : "6x8", big ? 40 : 1); + g.setFontAlign(0, 0); + var lines = msg.split("\n"); + var y = (g.getHeight() / 2) - (lines.length * 10 / 2); + lines.forEach(function(line, i) { + g.drawString(line, g.getWidth() / 2, y + i * 10); + }); +} + +function showCount() { + showMessage("Reps: " + repCount, false); +} + +// === Start Tracking === +function startSession() { + repCount = 0; + ema = null; + state = "down"; + lastPeakT = lastValleyT = (getTime() * 1000) | 0; + lastPeakV = lastValleyV = 0; + win = []; + + showMessage("Get Ready", true); + setTimeout(() => showMessage("Go!", true), 500); + + accelListener = Bangle.on("accel", function(a) { + var angle = Math.atan2(a.x, a.z) * 180 / Math.PI; + if (ema === null) ema = angle; + ema = alpha * angle + (1 - alpha) * ema; + + win.push(ema); + if (win.length > windowSize) win.shift(); + + var mx = win[0], mn = win[0], sum = 0; + win.forEach(function(v) { + if (v > mx) mx = v; + if (v < mn) mn = v; + sum += v; + }); + + var mean = sum / win.length; + var upTh = mean + 0.3 * (mx - mn); + var downTh = mean - 0.3 * (mx - mn); + + var now = (getTime() * 1000) | 0; + if (state === "down" && ema > upTh && now - lastValleyT > minRepTime) { + state = "up"; + lastPeakT = now; lastPeakV = ema; + } else if (state === "up" && ema < downTh && now - lastPeakT > minRepTime) { + state = "down"; + lastValleyT = now; lastValleyV = ema; + var amp = Math.abs(lastPeakV - lastValleyV); + if (amp >= minAmplitude) { + repCount++; + showCount(); + } + } + }); +} + +// === Stop Tracking === +function stopSession() { + if (accelListener) { + Bangle.removeListener("accel", accelListener); + accelListener = null; + } + showMessage("Stopped\nReps: " + repCount, true); +} + +// === Setup Buttons and Initial UI === +Bangle.loadWidgets(); +Bangle.drawWidgets(); +setWatch(startSession, BTN1, { repeat: false, edge: "rising" }); +setWatch(stopSession, BTN2, { repeat: false, edge: "rising" }); +showMessage("BTN1=Start\nBTN2=Stop", false); diff --git a/apps/smartrep/logic/squats.js b/apps/smartrep/logic/squats.js new file mode 100644 index 0000000000..33b8031d35 --- /dev/null +++ b/apps/smartrep/logic/squats.js @@ -0,0 +1,73 @@ +// === SR Squat Tracker for Bangle.js 1 === +// Uses real-time Z-axis acceleration to detect squats based on downward and upward motion. + +// === State Variables === +let reps = 0; // Total reps counted +let running = false; // Whether tracking is active +let inDown = false; // Whether user is currently in downward squat motion + +// === Show Initial Splash Screen === +function showSplash() { + g.clear(); + g.setFont("6x8", 2); + g.drawString("SR Fitness", 60, 30); + g.setFont("Vector", 20); + g.drawString("Squats", 75, 60); + g.setFont("6x8", 1); + g.drawString("BTN2: Start", 65, 110); + drawButtons(); +} + +// === Assign Buttons === +function drawButtons() { + setWatch(startSquatTracking, BTN2, { repeat: false, edge: "rising" }); + setWatch(stopTracking, BTN1, { repeat: false, edge: "rising" }); +} + +// === Start Tracking Squats === +function startSquatTracking() { + reps = 0; + inDown = false; + running = true; + Bangle.setPollInterval(40); // ~25Hz + Bangle.on('accel', onSquat); + showStatus(); +} + +// === Stop Tracking Squats === +function stopTracking() { + running = false; + Bangle.removeAllListeners('accel'); + g.clear(); + g.setFont("6x8", 2); + g.drawString("Tracking Stopped", 20, 60); + g.drawString("BTN2: Restart", 30, 90); + drawButtons(); +} + +// === Accelerometer Handler === +function onSquat(a) { + if (!running) return; + let z = a.z; + + // Detect transition: down → up = one rep + if (z < -1.2) { + inDown = true; + } else if (z > -0.8 && inDown) { + reps++; + inDown = false; + showStatus(); + } +} + +// === Display Rep Count === +function showStatus() { + g.clear(); + g.setFont("6x8", 2); + g.drawString("Squats Tracker", 20, 10); + g.drawString("Reps: " + reps, 20, 40); + g.drawString("BTN1: Stop", 20, 100); +} + +// === Initialize App === +showSplash(); diff --git a/apps/smartrep/logic/triceps_pushdown.js b/apps/smartrep/logic/triceps_pushdown.js new file mode 100644 index 0000000000..0654c8e490 --- /dev/null +++ b/apps/smartrep/logic/triceps_pushdown.js @@ -0,0 +1,39 @@ +let pollingInterval = 40; +let thresholdStart = 0.6; // starting from top +let thresholdEnd = 0.2; // ending low +let minInterval = 600; + +let repCount = 0; +let inPush = false; +let lastRepTime = 0; +let tracking = false; + +exports.start = function(onRep) { + if (tracking) return; + tracking = true; + repCount = 0; + inPush = false; + lastRepTime = 0; + + Bangle.setPollInterval(pollingInterval); + Bangle.on('accel', function handler(a) { + let x = a.x; + let now = getTime() * 1000; + + if (!inPush && x < thresholdEnd) inPush = true; + + if (inPush && x > thresholdStart && now - lastRepTime > minInterval) { + lastRepTime = now; + inPush = false; + repCount++; + onRep(); + Bangle.buzz(); + } + }); +}; + +exports.stop = function() { + if (!tracking) return; + tracking = false; + Bangle.removeAllListeners('accel'); +}; diff --git a/apps/smartrep/metadata.json b/apps/smartrep/metadata.json new file mode 100644 index 0000000000..ba1eb98aa6 --- /dev/null +++ b/apps/smartrep/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "smartrep", + "name": "SmartRep", + "type": "app", + "icon": "smartrep-icon.png", + "tags": "fitness,exercise,health", + "supports": ["BANGLEJS"], + "description": "SmartRep tracks and counts reps for 8 exercises using accelerometer data. Animated UI and feedback included.", + "storage": [ + { "name": "smartrep.app.js", "url": "smartrep.app.js" }, + { "name": "smartrep-icon.png", "url": "smartrep-icon.png" }, + { "name": "logic/benchpress.js" }, + { "name": "logic/bicepcurl.js" }, + { "name": "logic/cable_rows.js" }, + { "name": "logic/lateralraise.js" }, + { "name": "logic/pushup.js" }, + { "name": "logic/shoulderpress.js" }, + { "name": "logic/tricepspushdown.js" }, + { "name": "logic/squats.js" } + ] +} diff --git a/apps/smartrep/smartrep-icon.png b/apps/smartrep/smartrep-icon.png new file mode 100644 index 0000000000..f316bb7e31 Binary files /dev/null and b/apps/smartrep/smartrep-icon.png differ diff --git a/apps/smartrep/smartrep.app.js b/apps/smartrep/smartrep.app.js new file mode 100644 index 0000000000..7914a775bb --- /dev/null +++ b/apps/smartrep/smartrep.app.js @@ -0,0 +1,202 @@ +const exercises = [ + "Bench Press", "Bicep Curls", "Cable Rows", + "Lateral Raise", "Pushup", "Shoulder Press", + "Triceps Pushdown", "Squats" +]; + + +let selected = 0; +let scrollOffset = 0; +const visibleCount = 3; + +// ==== Safe wrapped text function ==== +function drawWrappedText(text, y, scale, center) { + scale = scale !== undefined ? scale : 2; + center = center !== undefined ? center : true; + g.setFont("6x8", scale); + const maxWidth = g.getWidth(); + const words = text.split(" "); + let line = ""; + let lineY = y; + + for (let i = 0; i < words.length; i++) { + const testLine = line + (line ? " " : "") + words[i]; + if (g.stringWidth(testLine) > maxWidth && line) { + const x = center ? (maxWidth - g.stringWidth(line)) / 2 : 15; + g.drawString(line, x, lineY); + line = words[i]; + lineY += 20 * scale; + } else { + line = testLine; + } + } + const x = center ? (maxWidth - g.stringWidth(line)) / 2 : 15; + g.drawString(line, x, lineY); +} + +// ==== Home Page ==== +function showHomePage() { + g.clear(); + g.setBgColor(0, 0, 0); + g.setColor(0, 255, 0); + drawWrappedText("SmartRep", 10, 3); + + g.setColor(0, 200, 255); + drawWrappedText("Use BTN1&3 to move", 50, 2); + drawWrappedText("BTN2 to select", 75, 2); + + if (selected < scrollOffset) scrollOffset = selected; + if (selected >= scrollOffset + visibleCount) + scrollOffset = selected - visibleCount + 1; + + let y = 115; + for (let i = scrollOffset; i < scrollOffset + visibleCount && i < exercises.length; i++) { + g.setFont("6x8", i === selected ? 3 : 2); + g.setColor(i === selected ? 255 : 255, i === selected ? 255 : 255, i === selected ? 0 : 255); + g.drawString(exercises[i], (g.getWidth() - g.stringWidth(exercises[i])) / 2, y); + y += (i === selected) ? 38 : 30; + } +} + +// ==== Button Bindings for Home Page ==== +function setupHomeButtons() { + clearWatch(); + setWatch(() => { + selected = (selected - 1 + exercises.length) % exercises.length; + showHomePage(); + }, BTN1, { repeat: true, edge: "rising" }); + + setWatch(() => { + selected = (selected + 1) % exercises.length; + showHomePage(); + }, BTN3, { repeat: true, edge: "rising" }); + + setWatch(() => { + selectTargetReps(exercises[selected]); + }, BTN2, { repeat: false, edge: "rising" }); +} + +// ==== Target Repetition Selection Page ==== +function selectTargetReps(exerciseName) { + let targetReps = 5; + function draw() { + g.clear(); + g.setColor(0, 255, 0); + drawWrappedText("Select Target", 20, 3); + g.setColor(0, 200, 255); + drawWrappedText("BTN1- / BTN3+", 60, 2); + drawWrappedText("BTN2 to Start", 85, 2); + g.setColor(255, 255, 255); + g.setFont("6x8", 3); + g.drawString(targetReps, (g.getWidth() - g.stringWidth(targetReps)) / 2, 130); + } + + draw(); + clearWatch(); + setWatch(() => { + if (targetReps > 1) targetReps--; + draw(); + }, BTN1, { repeat: true, edge: "rising" }); + + setWatch(() => { + targetReps++; + draw(); + }, BTN3, { repeat: true, edge: "rising" }); + + setWatch(() => { + showExerciseSession(exerciseName, targetReps); + }, BTN2, { repeat: false, edge: "rising" }); +} + +// ==== Exercise Repetition Session Page ==== +function showExerciseSession(exerciseName, targetReps) { + let reps = 0; + const cx = g.getWidth() / 2; + const cy = g.getHeight() / 2; + const r = 40; + let completed = false; + + function drawDots() { + for (let i = 0; i < targetReps; i++) { + const angle = (2 * Math.PI * i) / targetReps - Math.PI / 2; + const x = cx + r * Math.cos(angle); + const y = cy + r * Math.sin(angle); + if (i < reps) { + g.setColor(i === reps - 1 ? 255 : 255, i === reps - 1 ? 255 : 255, i === reps - 1 ? 0 : 0); + } else { + g.setColor(255, 255, 255); + } + g.fillCircle(x, y, 2); + } + } + + function draw() { + g.clear(); + g.setColor(0, 255, 0); + drawWrappedText(exerciseName, 10, 3); + + drawDots(); + + // Center rep counter + g.setFont("6x8", 3); + g.setColor(255, 255, 255); + const repStr = "" + reps; + g.drawString(repStr, (g.getWidth() - g.stringWidth(repStr)) / 2, cy - 10); + + // Instructions (stacked) + g.setFont("6x8", 2); + g.setColor(0, 200, 255); + const xInstr = (g.getWidth() - g.stringWidth("BTN1: Reset")) / 2; + g.drawString("BTN1: Reset", xInstr, 180); + g.drawString("BTN2: Exit", xInstr, 200); + } + + function animateDotFill() { + draw(); + if (reps === targetReps) { + Bangle.buzz(); + g.setColor(255, 255, 255); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); // flash + setTimeout(() => { + g.setColor(50, 50, 50); + g.fillRect(0, 0, g.getWidth(), g.getHeight()); // dim screen + draw(); + }, 200); + } + } + + draw(); + clearWatch(); + + setWatch(() => { + if (!completed) { + if (reps < targetReps) { + reps++; + animateDotFill(); + } + if (reps === targetReps) completed = true; + } + }, BTN3, { repeat: true, edge: "rising" }); + + setWatch(() => { + reps = 0; + completed = false; + draw(); + }, BTN1, { repeat: true, edge: "rising" }); + + setWatch(() => { + showHomePage(); + setupHomeButtons(); + }, BTN2, { repeat: false, edge: "rising" }); +} + +// ==== INIT ==== +Bangle.setLCDTimeout(0); +Bangle.on('lcdPower', on => { + if (on) { + showHomePage(); + setupHomeButtons(); + } +}); +showHomePage(); +setupHomeButtons();