Skip to content

Commit f4d1f2e

Browse files
authored
Merge pull request #1 from Allmight97/feature/stop-and-freeze-pr12
STOP reliability + Freeze Live Graph after Set (PR1+PR2) - complete
2 parents a27a9f3 + 61bc9c6 commit f4d1f2e

File tree

5 files changed

+245
-13
lines changed

5 files changed

+245
-13
lines changed

app.js

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ class VitruvianApp {
44
constructor() {
55
this.device = new VitruvianDevice();
66
this.loadHistory = [];
7+
this.frozenHistory = null;
8+
this.isGraphFrozen = false;
79
this.maxHistoryPoints = 300; // 30 seconds at 100ms polling
810
this.maxPosA = 1000; // Dynamic max for Right Cable (A)
911
this.maxPosB = 1000; // Dynamic max for Left Cable (B)
@@ -13,16 +15,27 @@ class VitruvianApp {
1315
this.targetReps = 0; // Target working reps
1416
this.workoutHistory = []; // Track completed workouts
1517
this.currentWorkout = null; // Current workout info
18+
this._isStopping = false;
19+
this._isCompleting = false;
1620
this.setupLogging();
1721
this.setupGraph();
1822
this.resetRepCountersToEmpty();
23+
this.updateResumeGraphButton(false);
1924
}
2025

2126
setupLogging() {
2227
// Connect device logging to UI
2328
this.device.onLog = (message, type) => {
2429
this.addLogEntry(message, type);
2530
};
31+
32+
this.device.onDisconnect = () => {
33+
this.updateConnectionStatus(false);
34+
this.addLogEntry(
35+
"Device disconnected. Please reconnect before starting another workout.",
36+
"error",
37+
);
38+
};
2639
}
2740

2841
setupGraph() {
@@ -65,12 +78,64 @@ class VitruvianApp {
6578
this.drawGraph();
6679
}
6780

81+
updateResumeGraphButton(visible) {
82+
const button = document.getElementById("resumeGraphBtn");
83+
if (!button) return;
84+
85+
if (visible) {
86+
button.classList.remove("hidden");
87+
} else {
88+
button.classList.add("hidden");
89+
}
90+
}
91+
92+
freezeGraphSnapshot(message) {
93+
if (this.isGraphFrozen) {
94+
return;
95+
}
96+
97+
if (this.loadHistory.length === 0) {
98+
return;
99+
}
100+
101+
this.isGraphFrozen = true;
102+
this.frozenHistory = this.loadHistory.map((point) => ({ ...point }));
103+
this.updateResumeGraphButton(true);
104+
105+
const logMessage =
106+
message || "Graph frozen – click Resume Live Graph to return to live data.";
107+
this.addLogEntry(logMessage, "info");
108+
109+
this.drawGraph();
110+
}
111+
112+
resumeLiveGraph(silent = false) {
113+
if (!this.isGraphFrozen) {
114+
return;
115+
}
116+
117+
this.isGraphFrozen = false;
118+
this.frozenHistory = null;
119+
this.loadHistory = [];
120+
this.updateResumeGraphButton(false);
121+
122+
if (!silent) {
123+
this.addLogEntry("Live graph resumed", "info");
124+
}
125+
126+
this.drawGraph();
127+
}
128+
68129
drawGraph() {
69130
if (!this.ctx || !this.canvas) return;
70131

71132
const width = this.canvasDisplayWidth || this.canvas.width;
72133
const height = this.canvasDisplayHeight || this.canvas.height;
73134
const ctx = this.ctx;
135+
const history =
136+
this.isGraphFrozen && this.frozenHistory && this.frozenHistory.length > 0
137+
? this.frozenHistory
138+
: this.loadHistory;
74139

75140
if (width === 0 || height === 0) return; // Canvas not sized yet
76141

@@ -81,7 +146,7 @@ class VitruvianApp {
81146
// Set text rendering for crisp fonts
82147
ctx.textRendering = "optimizeLegibility";
83148

84-
if (this.loadHistory.length < 2) {
149+
if (history.length < 2) {
85150
// Not enough data to draw
86151
ctx.fillStyle = "#6c757d";
87152
ctx.font = "14px -apple-system, sans-serif";
@@ -96,7 +161,7 @@ class VitruvianApp {
96161

97162
// Find max load for scaling
98163
let maxLoad = 0;
99-
for (const point of this.loadHistory) {
164+
for (const point of history) {
100165
const totalLoad = point.loadA + point.loadB;
101166
if (totalLoad > maxLoad) maxLoad = totalLoad;
102167
}
@@ -149,7 +214,7 @@ class VitruvianApp {
149214

150215
// Draw lines for each cable and total
151216
// Calculate spacing based on actual number of points to fill the graph width
152-
const numPoints = this.loadHistory.length;
217+
const numPoints = history.length;
153218
const pointSpacing = numPoints > 1 ? graphWidth / (numPoints - 1) : 0;
154219

155220
// Helper to draw a line
@@ -161,8 +226,8 @@ class VitruvianApp {
161226
ctx.beginPath();
162227

163228
let started = false;
164-
for (let i = 0; i < this.loadHistory.length; i++) {
165-
const point = this.loadHistory[i];
229+
for (let i = 0; i < history.length; i++) {
230+
const point = history[i];
166231
const value = getData(point);
167232
const x = padding + i * pointSpacing;
168233
const y = padding + graphHeight - (value / maxLoad) * graphHeight;
@@ -284,6 +349,10 @@ class VitruvianApp {
284349
document.getElementById("barA").style.height = heightA + "%";
285350
document.getElementById("barB").style.height = heightB + "%";
286351

352+
if (this.isGraphFrozen) {
353+
return;
354+
}
355+
287356
// Add to load history
288357
this.loadHistory.push({
289358
timestamp: sample.timestamp,
@@ -383,7 +452,13 @@ class VitruvianApp {
383452
}
384453

385454
completeWorkout() {
386-
if (this.currentWorkout) {
455+
if (!this.currentWorkout || this._isCompleting) {
456+
return;
457+
}
458+
459+
this._isCompleting = true;
460+
461+
try {
387462
// Add to history
388463
this.addToWorkoutHistory({
389464
mode: this.currentWorkout.mode,
@@ -395,6 +470,12 @@ class VitruvianApp {
395470
// Reset to empty state
396471
this.resetRepCountersToEmpty();
397472
this.addLogEntry("Workout completed and saved to history", "success");
473+
474+
this.freezeGraphSnapshot(
475+
"Workout completed – graph frozen. Click Resume Live Graph to review the last set.",
476+
);
477+
} finally {
478+
this._isCompleting = false;
398479
}
399480
}
400481

@@ -508,16 +589,50 @@ class VitruvianApp {
508589
}
509590

510591
async stopWorkout() {
592+
if (this._isStopping) {
593+
return;
594+
}
595+
596+
this._isStopping = true;
597+
const stopBtn = document.getElementById("stopBtn");
598+
if (stopBtn) {
599+
stopBtn.disabled = true;
600+
}
601+
602+
const startedAt = Date.now();
603+
511604
try {
512605
await this.device.sendStopCommand();
513606
this.addLogEntry("Workout stopped by user", "info");
514607

515608
// Complete the workout and save to history
516609
this.completeWorkout();
610+
611+
this.freezeGraphSnapshot(
612+
"STOP acknowledged – graph frozen. Click Resume Live Graph to continue live telemetry.",
613+
);
517614
} catch (error) {
518615
console.error("Stop workout error:", error);
519616
this.addLogEntry(`Failed to stop workout: ${error.message}`, "error");
617+
618+
if (!this.device.isConnected) {
619+
this.updateConnectionStatus(false);
620+
this.addLogEntry(
621+
"Device disconnected during STOP safety fallback. Please reconnect before continuing.",
622+
"error",
623+
);
624+
}
625+
520626
alert(`Failed to stop workout: ${error.message}`);
627+
} finally {
628+
const duration = Date.now() - startedAt;
629+
this.addLogEntry(`STOP flow completed in ${duration}ms`, "info");
630+
631+
if (stopBtn && this.device.isConnected) {
632+
stopBtn.disabled = false;
633+
}
634+
635+
this._isStopping = false;
521636
}
522637
}
523638

@@ -572,6 +687,8 @@ class VitruvianApp {
572687
};
573688
this.updateRepCounters();
574689

690+
this.resumeLiveGraph(true);
691+
575692
await this.device.startProgram(params);
576693

577694
// Set up monitor listener
@@ -638,6 +755,8 @@ class VitruvianApp {
638755
};
639756
this.updateRepCounters();
640757

758+
this.resumeLiveGraph(true);
759+
641760
await this.device.startEcho(params);
642761

643762
// Set up monitor listener

device.js

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ class VitruvianDevice {
3232
this.propertyInterval = null;
3333
this.monitorInterval = null;
3434
this.onLog = null; // Callback for logging
35+
this.onDisconnect = null; // Callback for external disconnect handling
3536
this.propertyListeners = [];
3637
this.repListeners = [];
3738
this.monitorListeners = [];
3839
this.lastGoodPosA = 0;
3940
this.lastGoodPosB = 0;
41+
this._isStopping = false;
4042
}
4143

4244
log(message, type = "info") {
@@ -76,6 +78,13 @@ class VitruvianDevice {
7678
this.device.addEventListener("gattserverdisconnected", () => {
7779
this.log("Device disconnected", "error");
7880
this.handleDisconnect();
81+
if (typeof this.onDisconnect === "function") {
82+
try {
83+
this.onDisconnect();
84+
} catch (callbackError) {
85+
console.error("onDisconnect callback error:", callbackError);
86+
}
87+
}
7988
});
8089

8190
// Connect to GATT server
@@ -225,11 +234,59 @@ class VitruvianDevice {
225234
if (!this.isConnected) {
226235
throw new Error("Device not connected");
227236
}
237+
if (this._isStopping) {
238+
this.log("STOP already in progress", "info");
239+
return;
240+
}
241+
242+
this._isStopping = true;
243+
244+
try {
245+
this.stopPropertyPolling();
246+
this.stopMonitorPolling();
228247

229-
this.log("\nSending STOP command...", "info");
230-
const cmd = buildInitCommand(); // Stop command is same as init command
231-
await this.writeWithResponse("Stop command", cmd);
232-
this.log("Workout stopped!", "success");
248+
const cmd = buildInitCommand();
249+
const maxAttempts = 3;
250+
251+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
252+
this.log(`\nSTOP attempt ${attempt}/${maxAttempts}...`, "info");
253+
254+
try {
255+
await this.writeWithResponse("Stop command", cmd);
256+
this.log("Workout stopped!", "success");
257+
return;
258+
} catch (error) {
259+
const message = (error && error.message) || String(error);
260+
261+
if (
262+
attempt < maxAttempts &&
263+
/Network|GATT|InvalidState/i.test(message)
264+
) {
265+
this.log(
266+
`STOP retry ${attempt + 1}/${maxAttempts} after transient error: ${message}`,
267+
"info",
268+
);
269+
await this.sleep(100);
270+
continue;
271+
}
272+
273+
throw error;
274+
}
275+
}
276+
} catch (error) {
277+
this.log(`STOP failed after retries: ${error.message}`, "error");
278+
try {
279+
await this.disconnect();
280+
} catch (disconnectError) {
281+
this.log(
282+
`Disconnect during STOP cleanup failed: ${disconnectError.message}`,
283+
"error",
284+
);
285+
}
286+
throw error;
287+
} finally {
288+
this._isStopping = false;
289+
}
233290
}
234291

235292
// Start a workout program

docs/feedback_tracker.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Bug Reports
2+
3+
# Feature Requests
4+
[ ]: Brainstorm with Ai appropriate tests for this repo and consider adjusting to a Test/Behavior Driven Development approach.
5+
# Ractoring Suggestions
6+
7+
# Feedback From Reddit Users (r/vitruvian_form)
8+
- User: sudden_Hunter-9342
9+
Feedback: `trialled it. Connects easily. functions are pretty cool, with echo easy to use.
10+
1. [x] BUG: Mid-exercise STOP button does not work possible fixes:
11+
a. disconnect and reconnect
12+
b. Choose and start an exercise mode a few times until the error goes away(warm up reps and working reps reset)
13+
Notes: Fixed via PR #1 (STOP retries + UI debouncing). STOP now retries 3× with BLE cleanup; UI reflects fallback disconnect so users no longer need to reconnect manually.
14+
2. [ ] BUG: Seems the final rep is not executed. (i.e. 8th rep out of 8 deloads). Not a big deal, i just add an extra rep.
15+
3. [x] FEATURE: Stop the chart graphing at the end of reps. otherwise main data in the chart disappears to the side.
16+
Notes: Addressed in PR #1 by freezing the graph when STOP/auto-complete fires and exposing a Resume button so the last 30s stays visible.
17+
4. [ ] FEATURE: Adding rest periods/timer
18+
Dev Notes: Addressing in vitruvian-change-plan.md

docs/releases/CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Changelog
2+
3+
## 2025-10-26
4+
- PR: https://github.com/Allmight97/workoutmachineappfree.github.io/pull/1
5+
- Title: STOP reliability + Freeze Live Graph after Set (PR1+PR2)
6+
- Summary:
7+
- Hardened STOP behavior with guarded retries and polling pause to ensure immediate, reliable halts.
8+
- Added graph freeze on STOP or set completion with a visible "Resume Live Graph" control; keeps numeric stats live.
9+
- User impact:
10+
- Safer workouts: STOP succeeds promptly or disconnects cleanly; clear log feedback.
11+
- Better review UX: final set data remains visible; simple resume returns to live view.
12+
- Developer impact:
13+
- Structured logs around STOP attempts and acknowledgements aid diagnostics.
14+
- Clear, minimal state flags for graph (`isGraphFrozen`, `frozenHistory`) and explicit resume entry point.
15+
- Files changed:
16+
- app.js – STOP UI debounce/timing; graph freeze/resume helpers; guards in live stats and draw routines.
17+
- device.js – STOP retries (3x/100ms), pause polling during STOP, disconnect fallback, idempotent guard.
18+
- index.html – Added hidden "Resume Live Graph" button next to Load History header.
19+

0 commit comments

Comments
 (0)