states fixing idle issue?

This commit is contained in:
Martin Fuchs
2026-08-16 20:04:44 +02:00
parent a87b373b9e
commit 3958d9f3d1
4 changed files with 158 additions and 185 deletions
+1 -1
View File
@@ -42,7 +42,7 @@
} }
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="../../lib/WallPlotter.js?v=2.2.5"></script> <script src="../../lib/WallPlotter.js?v=2.3.0"></script>
<script src="sketch.js"></script> <script src="sketch.js"></script>
</head> </head>
<body> <body>
+1 -1
View File
@@ -42,7 +42,7 @@
} }
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.11.3/lib/p5.min.js"></script>
<script src="../../lib/WallPlotter.js?v=2.2.5"></script> <script src="../../lib/WallPlotter.js?v=2.3.0"></script>
<script src="sketch.js"></script> <script src="sketch.js"></script>
</head> </head>
<body> <body>
+1 -1
View File
@@ -151,7 +151,7 @@ Returns estimated duration in **milliseconds** for the current path buffer, or f
### `stopPath()` / `stopPlot()` ### `stopPath()` / `stopPlot()`
`stopPath()` aborts the current `runPath` and unblocks any wait for Marlin `ok`. `stopPath()` aborts the current `runPath` after the in-flight command gets its `ok` (it does not cancel that wait — that was dropping the next `ok`).
`stopPlot()` first press is the same as **Pause**: finish the current stroke, then **Resume** continues. Second press stops the browser from sending more moves. The **in-flight** `G1` still runs — this firmware does not have `EMERGENCY_PARSER`, so `M410` cannot interrupt motion (and on a polargraph it desyncs belts). Unplug power for a hard stop. `stopPlot()` first press is the same as **Pause**: finish the current stroke, then **Resume** continues. Second press stops the browser from sending more moves. The **in-flight** `G1` still runs — this firmware does not have `EMERGENCY_PARSER`, so `M410` cannot interrupt motion (and on a polargraph it desyncs belts). Unplug power for a hard stop.
+153 -180
View File
@@ -18,7 +18,7 @@ class WallPlotter {
// Static constants // Static constants
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static VERSION = '2.2.5'; static VERSION = '2.3.0';
static MIN_FEED_MM_MIN = 1; static MIN_FEED_MM_MIN = 1;
static MAX_FEED_MM_MIN = 10000; static MAX_FEED_MM_MIN = 10000;
/** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */ /** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */
@@ -82,8 +82,8 @@ class WallPlotter {
this.previewMinDelayMs = options.previewMinDelayMs ?? 30; this.previewMinDelayMs = options.previewMinDelayMs ?? 30;
this.previewFrameMs = options.previewFrameMs ?? 16; this.previewFrameMs = options.previewFrameMs ?? 16;
// --- Live machine state --- // --- Link: disconnected | connecting | connected ---
this.connected = false; this._link = 'disconnected';
this.homeSet = false; this.homeSet = false;
this.isPenDown = false; this.isPenDown = false;
this.x = 0; this.x = 0;
@@ -91,35 +91,28 @@ class WallPlotter {
this.statusText = 'Disconnected'; this.statusText = 'Disconnected';
this.logLines = []; this.logLines = [];
// --- Path / plot progress --- // --- Job: idle | running | paused | aborting ---
this._job = 'idle';
this._path = []; this._path = [];
this.previewPath = []; this.previewPath = [];
this.isPlotting = false;
this.isPaused = false;
this.plotProgress = 0; this.plotProgress = 0;
this._plotTestOnly = false; this._plotTestOnly = false;
this._pathAbort = false;
this._pathRunId = 0; this._pathRunId = 0;
this._pendingAfterAbort = null; this._pathLoopActive = false;
this._plotStartedAt = 0; this._plotStartedAt = 0;
this._plotEstimateMs = 0; this._plotEstimateMs = 0;
this._pausedAccumMs = 0; this._pausedAccumMs = 0;
this._pauseStartedAt = 0; this._pauseStartedAt = 0;
this._statusBeforePause = null;
this._statusBeforeOkWait = null;
this._freezeMotion = false;
this._lastSegmentMs = 0; this._lastSegmentMs = 0;
// --- Web Serial --- // --- Serial: one command in flight, wait for its ok ---
this.port = null; this.port = null;
this.writer = null; this.writer = null;
this.reader = null; this.reader = null;
this.readLoopActive = false; this.readLoopActive = false;
this._rxBuf = ''; this._rxBuf = '';
this._okResolvers = []; this._okWaiter = null;
this._sendChain = Promise.resolve(); this._sendChain = Promise.resolve();
this._okIgnoreUntil = 0;
this._machineBusy = false;
this._lastBusyLogAt = 0; this._lastBusyLogAt = 0;
// --- Keyboard / buttons --- // --- Keyboard / buttons ---
@@ -139,7 +132,6 @@ class WallPlotter {
this._testLabel = 'Test'; this._testLabel = 'Test';
this._stopLabel = 'Stop'; this._stopLabel = 'Stop';
this._lastTestBtnClick = 0; this._lastTestBtnClick = 0;
this._connecting = false;
// --- Callbacks --- // --- Callbacks ---
const userOnLog = options.onLog ?? (() => {}); const userOnLog = options.onLog ?? (() => {});
@@ -151,8 +143,7 @@ class WallPlotter {
const userOnStatusChange = options.onStatusChange ?? (() => {}); const userOnStatusChange = options.onStatusChange ?? (() => {});
this.onStatusChange = (status) => { this.onStatusChange = (status) => {
if (status === 'connected') this.statusText = 'Connected'; this._syncStatus();
else if (status === 'disconnected') this.statusText = 'Disconnected';
this._updatePlotButtons(); this._updatePlotButtons();
userOnStatusChange(status); userOnStatusChange(status);
}; };
@@ -161,6 +152,45 @@ class WallPlotter {
this._installUnloadHandler(); this._installUnloadHandler();
} }
get connected() {
return this._link === 'connected';
}
get isPlotting() {
return this._job !== 'idle';
}
get isPaused() {
return this._job === 'paused';
}
_setJob(next) {
if (this._job === next) return;
if (this._job === 'paused' && this._pauseStartedAt) {
this._pausedAccumMs += performance.now() - this._pauseStartedAt;
}
this._job = next;
this._pauseStartedAt = next === 'paused' ? performance.now() : 0;
this._syncStatus();
this._updatePlotButtons();
this._redrawSketch();
}
_abortJob() {
if (this._job === 'idle') return;
this._setJob('aborting');
}
_syncStatus() {
if (this._link === 'disconnected') this.statusText = 'Disconnected';
else if (this._link === 'connecting') this.statusText = 'Connecting…';
else if (this._job === 'paused') this.statusText = 'Paused';
else if (this._job === 'aborting') this.statusText = 'Stopped';
else if (this._job === 'running') {
this.statusText = this._plotTestOnly ? 'Testing' : 'Plotting';
} else this.statusText = 'Connected';
}
// =========================================================================== // ===========================================================================
// Public API — setup / UI // Public API — setup / UI
// =========================================================================== // ===========================================================================
@@ -319,40 +349,23 @@ class WallPlotter {
return this._estimatePathDurationMs(commands ?? this._path, this.x, this.y); return this._estimatePathDurationMs(commands ?? this._path, this.x, this.y);
} }
/** Abort the current runPath (browser side only) and unblock any `ok` wait. */ /** Abort the current runPath after the in-flight command's `ok`. */
stopPath() { stopPath() {
this._pathAbort = true; this._abortJob();
this.isPaused = false;
this._freezeMotion = true;
this._cancelPendingOk();
} }
/** Pause the current runPath after the in-flight segment. */ /** Pause the current runPath after the in-flight segment. */
pausePlot() { pausePlot() {
if (!this.isPlotting || this.isPaused) return; if (this._job !== 'running') return;
this.isPaused = true; this._setJob('paused');
this._pauseStartedAt = performance.now();
this._statusBeforePause = this.statusText;
this.statusText = 'Paused';
this.onLog('plot paused'); this.onLog('plot paused');
this._updatePlotButtons();
this._redrawSketch();
} }
/** Continue a paused runPath (including after Stop or a timeout). */ /** Continue a paused runPath (including after Stop or a timeout). */
resumePlot() { resumePlot() {
if (!this.isPlotting || !this.isPaused) return; if (this._job !== 'paused') return;
if (this._pauseStartedAt) { this._setJob('running');
this._pausedAccumMs += performance.now() - this._pauseStartedAt;
}
this._pauseStartedAt = 0;
this.isPaused = false;
this._freezeMotion = false;
if (this._statusBeforePause) this.statusText = this._statusBeforePause;
this._statusBeforePause = null;
this.onLog('plot resumed'); this.onLog('plot resumed');
this._updatePlotButtons();
this._redrawSketch();
} }
/** /**
@@ -362,27 +375,24 @@ class WallPlotter {
* a running move, and on a polargraph it desyncs belt lengths. * a running move, and on a polargraph it desyncs belt lengths.
*/ */
async stopPlot() { async stopPlot() {
const hadWait = this._okResolvers.length > 0; if (this._job === 'paused') {
this._abortJob();
if (this.isPlotting && this.isPaused) {
this.stopPath();
this.statusText = 'Stopped — current stroke may still finish';
this.onLog( this.onLog(
'plot aborted — motors finish the in-flight move (unplug power for a hard stop)' 'plot aborted — motors finish the in-flight move (unplug power for a hard stop)'
); );
return; return;
} }
if (this.isPlotting) { if (this._job === 'running') {
this.pausePlot(); this.pausePlot();
this.onLog('plot stopped after current move — resume to continue'); this.onLog('plot stopped after current move — resume to continue');
return; return;
} }
if (hadWait) { if (this._okWaiter) {
this._cancelPendingOk(); this._cancelPendingOk();
this.onLog('serial wait cancelled'); this.onLog('serial wait cancelled');
this.statusText = this.connected ? 'Connected' : 'Disconnected'; this._syncStatus();
} }
} }
@@ -395,7 +405,7 @@ class WallPlotter {
if (!('serial' in navigator)) { if (!('serial' in navigator)) {
throw new Error('Web Serial is not supported in this browser. Use Chrome or Edge.'); throw new Error('Web Serial is not supported in this browser. Use Chrome or Edge.');
} }
if (this.connected) return; if (this._link === 'connected') return;
let port; let port;
try { try {
@@ -410,7 +420,7 @@ class WallPlotter {
this.writer = this.port.writable.getWriter(); this.writer = this.port.writable.getWriter();
this._startReadLoop(); this._startReadLoop();
this.connected = true; this._link = 'connected';
this.homeSet = false; this.homeSet = false;
this._homeWarning = 'Home not set — jog to origin and press H'; this._homeWarning = 'Home not set — jog to origin and press H';
this.onStatusChange('connected'); this.onStatusChange('connected');
@@ -462,12 +472,11 @@ class WallPlotter {
/** Close the serial port. */ /** Close the serial port. */
async disconnect() { async disconnect() {
if (!this.connected && !this.port) return; if (this._link === 'disconnected' && !this.port) return;
this.readLoopActive = false; this.readLoopActive = false;
this._cancelPendingOk(); this._cancelPendingOk();
this._rxBuf = ''; this._rxBuf = '';
this._machineBusy = false;
const reader = this.reader; const reader = this.reader;
const writer = this.writer; const writer = this.writer;
@@ -503,7 +512,7 @@ class WallPlotter {
} }
} }
this.connected = false; this._link = 'disconnected';
this.homeSet = false; this.homeSet = false;
this._homeWarning = null; this._homeWarning = null;
this.onStatusChange('disconnected'); this.onStatusChange('disconnected');
@@ -511,18 +520,17 @@ class WallPlotter {
/** Best-effort close for page reload (must not await — Mac keeps the USB lock otherwise). */ /** Best-effort close for page reload (must not await — Mac keeps the USB lock otherwise). */
_releaseSerialNow() { _releaseSerialNow() {
if (!this.port && !this.connected) return; if (!this.port && this._link === 'disconnected') return;
this.readLoopActive = false; this.readLoopActive = false;
this._cancelPendingOk(); this._cancelPendingOk();
this._rxBuf = ''; this._rxBuf = '';
this._machineBusy = false;
const reader = this.reader; const reader = this.reader;
const writer = this.writer; const writer = this.writer;
const port = this.port; const port = this.port;
this.reader = null; this.reader = null;
this.writer = null; this.writer = null;
this.port = null; this.port = null;
this.connected = false; this._link = 'disconnected';
this.homeSet = false; this.homeSet = false;
try { try {
reader?.cancel(); reader?.cancel();
@@ -594,25 +602,26 @@ class WallPlotter {
}) })
: null; : null;
try { try {
const written = this.writer.write( const written = this.writer
new TextEncoder().encode(command + '\n') .write(new TextEncoder().encode(command + '\n'))
); .then(() => this.onLog('→ ' + command));
if (okPromise) { if (okPromise) {
await Promise.all([ await Promise.all([
written.then(() => this.onLog('→ ' + command)), written.catch((err) => {
this._cancelPendingOk();
throw err;
}),
okPromise, okPromise,
]); ]);
} else { } else {
await written; await written;
this.onLog('→ ' + command);
} }
} catch (err) { } catch (err) {
if (err.name === 'AbortError') throw err; if (err.name === 'AbortError') throw err;
this._okIgnoreUntil = performance.now() + 400;
const timeoutErr = new Error(err.message); const timeoutErr = new Error(err.message);
timeoutErr.name = 'TimeoutError'; timeoutErr.name = 'TimeoutError';
this.onLog('! ' + err.message); this.onLog('! ' + err.message);
if (!this.isPlotting) this.statusText = 'Error: ' + err.message; if (this._job === 'idle') this.statusText = 'Error: ' + err.message;
throw timeoutErr; throw timeoutErr;
} }
} }
@@ -680,12 +689,12 @@ class WallPlotter {
/** G92 X0 Y0 — set current pose as home. Refused while plotting or waiting for Marlin. */ /** G92 X0 Y0 — set current pose as home. Refused while plotting or waiting for Marlin. */
async setHomeHere() { async setHomeHere() {
if (this.isPlotting) { if (this._job !== 'idle') {
this.onLog('! cannot set home while plotting — resume or abort first'); this.onLog('! cannot set home while plotting — resume or abort first');
this.statusText = 'Finish or abort plot before H'; this.statusText = 'Finish or abort plot before H';
return; return;
} }
if (!this._isMachineIdle()) { if (this._okWaiter) {
this.onLog('! plotter busy — wait for ok before setting home'); this.onLog('! plotter busy — wait for ok before setting home');
this.statusText = 'Waiting for plotter…'; this.statusText = 'Waiting for plotter…';
return; return;
@@ -700,7 +709,7 @@ class WallPlotter {
this.y = 0; this.y = 0;
this.homeSet = true; this.homeSet = true;
this._homeWarning = null; this._homeWarning = null;
if (this.connected) this.statusText = 'Connected'; this._syncStatus();
} }
// =========================================================================== // ===========================================================================
@@ -721,39 +730,38 @@ class WallPlotter {
// =========================================================================== // ===========================================================================
async _executePath(commands, testOnly = false) { async _executePath(commands, testOnly = false) {
if (!testOnly && !this._requireHome()) return;
const runId = ++this._pathRunId; const runId = ++this._pathRunId;
this._pathAbort = false; if (this._pathLoopActive) this._abortJob();
while (this._pathLoopActive) {
if (runId !== this._pathRunId) return;
await this._delay(16);
}
if (runId !== this._pathRunId) return;
this._pathLoopActive = true;
this._plotTestOnly = testOnly; this._plotTestOnly = testOnly;
this.previewPath = commands; this.previewPath = commands;
if (!testOnly && !this._requireHome()) {
this._plotTestOnly = false;
return;
}
this.isPlotting = true;
this.isPaused = false;
this.plotProgress = 0; this.plotProgress = 0;
this._plotStartedAt = performance.now(); this._plotStartedAt = performance.now();
this._plotEstimateMs = this._estimatePathDurationMs(commands, this.x, this.y); this._plotEstimateMs = this._estimatePathDurationMs(commands, this.x, this.y);
this._pausedAccumMs = 0; this._pausedAccumMs = 0;
this._pauseStartedAt = 0;
this._statusBeforePause = null;
this._freezeMotion = false;
this._lastSegmentMs = 0; this._lastSegmentMs = 0;
this._updatePlotButtons(); this._setJob('running');
const savedFeedTravel = this.feedRateTravel; const savedFeedTravel = this.feedRateTravel;
const savedFeedDraw = this.feedRateDraw; const savedFeedDraw = this.feedRateDraw;
let px = this.x; let px = this.x;
let py = this.y; let py = this.y;
const stopped = () => this._job === 'aborting' || runId !== this._pathRunId;
try { try {
for (let i = 0; i < commands.length; i++) { for (let i = 0; i < commands.length; i++) {
if (this._pathAbort || runId !== this._pathRunId) break; if (stopped()) break;
await this._waitIfPaused(); await this._waitIfPaused();
this._freezeMotion = false; if (stopped()) break;
if (this._pathAbort || runId !== this._pathRunId) break;
const cmd = commands[i]; const cmd = commands[i];
if (cmd.type === 'speed') { if (cmd.type === 'speed') {
@@ -770,6 +778,7 @@ class WallPlotter {
if (!testOnly && ms > 0) { if (!testOnly && ms > 0) {
await this.send(`G4 P${Math.round(ms)}`, { await this.send(`G4 P${Math.round(ms)}`, {
timeoutMs: Math.max(WallPlotter.OK_TIMEOUT_MS, ms + 5000), timeoutMs: Math.max(WallPlotter.OK_TIMEOUT_MS, ms + 5000),
waitLogAfterMs: Math.max(WallPlotter.OK_WAIT_LOG_MS, ms + 1500),
}); });
} }
await this._holdPosition(ms); await this._holdPosition(ms);
@@ -787,34 +796,21 @@ class WallPlotter {
} }
} }
} catch (err) { } catch (err) {
if (err.name === 'AbortError') break; if (err.name === 'AbortError' || stopped()) break;
if (err.name === 'TimeoutError') { if (err.name === 'TimeoutError') {
if (!this.isPaused) this.pausePlot(); if (this._job === 'running') this.pausePlot();
this.onLog('segment skipped after timeout — resume continues from here'); this.onLog('segment skipped after timeout — resume continues from here');
px = this.x; px = this.x;
py = this.y; py = this.y;
await this._waitIfPaused(); await this._waitIfPaused();
if (this._pathAbort || runId !== this._pathRunId) break; if (stopped()) break;
if (cmd.type === 'move' || cmd.type === 'draw') {
px = this.x;
py = this.y;
}
this.plotProgress = i + 1; this.plotProgress = i + 1;
continue; continue;
} }
throw err; throw err;
} }
if (this._pathAbort || runId !== this._pathRunId) break; if (stopped()) break;
if (this._freezeMotion) {
px = this.x;
py = this.y;
await this._waitIfPaused();
this._freezeMotion = false;
if (this._pathAbort || runId !== this._pathRunId) break;
continue;
}
if (cmd.type === 'move' || cmd.type === 'draw') { if (cmd.type === 'move' || cmd.type === 'draw') {
px = cmd.x; px = cmd.x;
@@ -838,18 +834,9 @@ class WallPlotter {
} else { } else {
this.isPenDown = false; this.isPenDown = false;
} }
this.isPlotting = false;
this.isPaused = false;
this._pauseStartedAt = 0;
this._statusBeforePause = null;
this._plotTestOnly = false; this._plotTestOnly = false;
this._updatePlotButtons(); this._setJob('idle');
this._redrawSketch(); this._pathLoopActive = false;
const next = this._pendingAfterAbort;
this._pendingAfterAbort = null;
if (next === 'test') this._triggerSketchFn('test');
else if (next === 'plot') this._triggerSketchFn('plot');
} }
} }
@@ -903,11 +890,11 @@ class WallPlotter {
let elapsed = 0; let elapsed = 0;
let last = performance.now(); let last = performance.now();
while (elapsed < ms) { while (elapsed < ms) {
if (this._pathAbort || this._freezeMotion) return; if (this._job === 'aborting') return;
if (this._plotTestOnly && this.isPaused) { if (this._job === 'paused') {
await this._waitIfPaused(); await this._waitIfPaused();
last = performance.now(); last = performance.now();
if (this._pathAbort) return; if (this._job === 'aborting') return;
} }
const now = performance.now(); const now = performance.now();
elapsed += now - last; elapsed += now - last;
@@ -924,11 +911,11 @@ class WallPlotter {
let last = performance.now(); let last = performance.now();
while (true) { while (true) {
if (this._pathAbort || this._freezeMotion) break; if (this._job === 'aborting') break;
if (this._plotTestOnly && this.isPaused) { if (this._plotTestOnly && this._job === 'paused') {
await this._waitIfPaused(); await this._waitIfPaused();
last = performance.now(); last = performance.now();
if (this._pathAbort || this._freezeMotion) break; if (this._job === 'aborting') break;
} }
const now = performance.now(); const now = performance.now();
elapsed += now - last; elapsed += now - last;
@@ -940,7 +927,7 @@ class WallPlotter {
if (t >= 1) break; if (t >= 1) break;
await this._delay(this.previewFrameMs); await this._delay(this.previewFrameMs);
} }
if (!this._pathAbort && !this._freezeMotion) { if (this._job !== 'aborting') {
this.x = cmd.x; this.x = cmd.x;
this.y = cmd.y; this.y = cmd.y;
} }
@@ -948,7 +935,7 @@ class WallPlotter {
/** Wait while paused. Live plots lift the pen until the next segment. */ /** Wait while paused. Live plots lift the pen until the next segment. */
async _waitIfPaused() { async _waitIfPaused() {
if (!this.isPaused) return; if (this._job !== 'paused') return;
if (!this._plotTestOnly && this.connected && this.isPenDown) { if (!this._plotTestOnly && this.connected && this.isPenDown) {
try { try {
@@ -958,7 +945,7 @@ class WallPlotter {
} }
} }
while (this.isPaused && !this._pathAbort) { while (this._job === 'paused') {
this._redrawSketch(); this._redrawSketch();
await this._delay(this.previewFrameMs); await this._delay(this.previewFrameMs);
} }
@@ -1014,6 +1001,11 @@ class WallPlotter {
const waitLabel = command ? ` (${command})` : ''; const waitLabel = command ? ` (${command})` : '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this._okWaiter) {
reject(new Error('serial already waiting for ok'));
return;
}
let settled = false; let settled = false;
let timer = null; let timer = null;
let waitLogTimer = null; let waitLogTimer = null;
@@ -1022,11 +1014,10 @@ class WallPlotter {
const finish = (fn, arg) => { const finish = (fn, arg) => {
if (settled) return; if (settled) return;
settled = true; settled = true;
if (this._okWaiter === entry) this._okWaiter = null;
clearTimeout(timer); clearTimeout(timer);
clearTimeout(waitLogTimer); clearTimeout(waitLogTimer);
if (waitLogInterval != null) clearInterval(waitLogInterval); if (waitLogInterval != null) clearInterval(waitLogInterval);
const idx = this._okResolvers.indexOf(entry);
if (idx >= 0) this._okResolvers.splice(idx, 1);
fn(arg); fn(arg);
}; };
@@ -1043,13 +1034,8 @@ class WallPlotter {
const entry = { const entry = {
resolve: () => { resolve: () => {
if (this.statusText === 'Waiting for plotter…') {
this.statusText =
this._statusBeforeOkWait ??
(this.connected ? 'Connected' : 'Disconnected');
this._statusBeforeOkWait = null;
}
finish(resolve); finish(resolve);
this._syncStatus();
}, },
reject: (err) => finish(reject, err), reject: (err) => finish(reject, err),
bump: () => { bump: () => {
@@ -1057,15 +1043,13 @@ class WallPlotter {
}, },
}; };
this._okWaiter = entry;
armTimer(); armTimer();
waitLogTimer = setTimeout(() => { waitLogTimer = setTimeout(() => {
if (settled) return; if (settled) return;
const started = performance.now(); const started = performance.now();
this.onLog(`… waiting for Marlin ok${waitLabel}`); this.onLog(`… waiting for Marlin ok${waitLabel}`);
if (this.statusText !== 'Waiting for plotter…') {
this._statusBeforeOkWait = this.statusText;
}
this.statusText = 'Waiting for plotter…'; this.statusText = 'Waiting for plotter…';
this._redrawSketch(); this._redrawSketch();
waitLogInterval = setInterval(() => { waitLogInterval = setInterval(() => {
@@ -1073,19 +1057,15 @@ class WallPlotter {
this.onLog(`… still waiting for Marlin ok${waitLabel} (${s}s)`); this.onLog(`… still waiting for Marlin ok${waitLabel} (${s}s)`);
}, WallPlotter.OK_WAIT_PING_MS); }, WallPlotter.OK_WAIT_PING_MS);
}, waitLogAfterMs); }, waitLogAfterMs);
this._okResolvers.push(entry);
}); });
} }
_cancelPendingOk() { _cancelPendingOk() {
this._okIgnoreUntil = performance.now() + 400; const waiter = this._okWaiter;
const pending = this._okResolvers.slice(); if (!waiter) return;
for (const entry of pending) {
const err = new Error('aborted'); const err = new Error('aborted');
err.name = 'AbortError'; err.name = 'AbortError';
entry.reject(err); waiter.reject(err);
}
} }
_startReadLoop() { _startReadLoop() {
@@ -1137,9 +1117,8 @@ class WallPlotter {
_handleResponse(line) { _handleResponse(line) {
if (/busy:\s*processing/i.test(line)) { if (/busy:\s*processing/i.test(line)) {
this._machineBusy = true; if (this._okWaiter && typeof this._okWaiter.bump === 'function') {
for (const entry of this._okResolvers) { this._okWaiter.bump();
if (typeof entry.bump === 'function') entry.bump();
} }
const now = performance.now(); const now = performance.now();
if (now - this._lastBusyLogAt > 4000) { if (now - this._lastBusyLogAt > 4000) {
@@ -1150,13 +1129,8 @@ class WallPlotter {
} }
this.onLog('← ' + line); this.onLog('← ' + line);
if (/^(echo:\s*)?ok\b/i.test(line)) { if (/^(echo:\s*)?ok\b/i.test(line)) {
this._machineBusy = false; const waiter = this._okWaiter;
const next = this._okResolvers.shift(); if (waiter) waiter.resolve();
if (next) {
next.resolve();
} else if (performance.now() < this._okIgnoreUntil) {
this.onLog('← ok ignored (stale, no waiter)');
}
} }
this._parseMachineBounds(line); this._parseMachineBounds(line);
if (line.startsWith('X:')) { if (line.startsWith('X:')) {
@@ -1190,7 +1164,7 @@ class WallPlotter {
} }
_isMachineIdle() { _isMachineIdle() {
return this._okResolvers.length === 0 && !this._machineBusy; return this._job === 'idle' && !this._okWaiter;
} }
_showHomeRequiredWarning() { _showHomeRequiredWarning() {
@@ -1236,7 +1210,7 @@ class WallPlotter {
return false; return false;
} }
if (k === '0') { if (k === '0') {
if (this.isPlotting || !this._isMachineIdle()) return false; if (!this._isMachineIdle()) return false;
await this.goToZero(); await this.goToZero();
return false; return false;
} }
@@ -1256,8 +1230,8 @@ class WallPlotter {
} }
_handleContinuousJog() { _handleContinuousJog() {
if (!this.connected || this._jogInFlight || this.isPlotting) return; if (!this.connected || this._jogInFlight || !this._isMachineIdle()) return;
if (!this.homeSet || !this._isMachineIdle()) return; if (!this.homeSet) return;
const draw = keyIsDown(SHIFT); const draw = keyIsDown(SHIFT);
const s = this.stepSize; const s = this.stepSize;
@@ -1307,81 +1281,80 @@ class WallPlotter {
} }
async _toggleConnection() { async _toggleConnection() {
if (this._connecting) return; if (this._link === 'connecting') return;
try { try {
if (this.connected) await this.disconnect(); if (this.connected) await this.disconnect();
else { else {
this._connecting = true; this._link = 'connecting';
this.statusText = 'Connecting…'; this._syncStatus();
this._updatePlotButtons(); this._updatePlotButtons();
await this.connect(); await this.connect();
} }
} catch (err) { } catch (err) {
if (err.name === 'AbortError') return; if (err.name === 'AbortError') return;
this._link = 'disconnected';
this.statusText = 'Error: ' + err.message; this.statusText = 'Error: ' + err.message;
this.onLog('! ' + err.message); this.onLog('! ' + err.message);
} finally { } finally {
this._connecting = false; if (this._link === 'connecting') this._link = 'disconnected';
this._updatePlotButtons(); this._updatePlotButtons();
} }
} }
_onConnectButton() { _onConnectButton() {
if (this.connected || this._connecting) return; if (this._link !== 'disconnected') return;
this._toggleConnection(); this._toggleConnection();
} }
_onPlotButton() { _onPlotButton() {
if (this.isPlotting) { if (this._job === 'paused') {
if (this.isPaused) this.resumePlot(); this.resumePlot();
else this.pausePlot();
return; return;
} }
if (!this.connected) return; if (this._job === 'running') {
this.pausePlot();
return;
}
if (this._job !== 'idle' || !this.connected) return;
this._triggerSketchFn('plot'); this._triggerSketchFn('plot');
} }
_onTestButton() { _onTestButton() {
const now = millis(); const now = millis();
if (this.isPlotting && this._plotTestOnly) { if (this._job === 'running' && this._plotTestOnly) {
if (now - this._lastTestBtnClick < 450) this._restartPathAfterAbort('test'); if (now - this._lastTestBtnClick < 450) this._restartPath('test');
else this.stopPlot(); else this.stopPlot();
this._lastTestBtnClick = now; this._lastTestBtnClick = now;
return; return;
} }
this._lastTestBtnClick = now; this._lastTestBtnClick = now;
if (this.isPlotting) { this._restartPath('test');
this._restartPathAfterAbort('test');
return;
}
this._triggerSketchFn('test');
} }
_restartPathAfterAbort(mode) { _restartPath(mode) {
this._pendingAfterAbort = mode; if (this._pathLoopActive) this._abortJob();
this._pathAbort = true; this._triggerSketchFn(mode);
this.isPaused = false;
this._freezeMotion = true;
this._cancelPendingOk();
} }
_updatePlotButtons() { _updatePlotButtons() {
if (this._connectBtn) { if (this._connectBtn) {
if (this.connected) this._connectBtn.hide(); if (this.connected) this._connectBtn.hide();
else this._connectBtn.show(); else this._connectBtn.show();
this._connectBtn.elt.disabled = this._connecting; this._connectBtn.elt.disabled = this._link === 'connecting';
} }
if (this._plotBtn) { if (this._plotBtn) {
let label = this._plotLabel; let label = this._plotLabel;
if (this.isPlotting) { if (this._job === 'paused') label = this._resumeLabel;
label = this.isPaused ? this._resumeLabel : this._pauseLabel; else if (this._job === 'running' || this._job === 'aborting') {
label = this._pauseLabel;
} }
this._plotBtn.html(label); this._plotBtn.html(label);
this._plotBtn.elt.disabled = !this.connected && !this.isPlotting; this._plotBtn.elt.disabled =
this._job === 'aborting' || (!this.connected && this._job === 'idle');
} }
if (this._testBtn) { if (this._testBtn) {
this._testBtn.html( this._testBtn.html(
this.isPlotting && this._plotTestOnly ? this._stopLabel : this._testLabel this._job !== 'idle' && this._plotTestOnly ? this._stopLabel : this._testLabel
); );
} }
} }
@@ -1631,7 +1604,7 @@ class WallPlotter {
if (this.isPlotting) { if (this.isPlotting) {
fill(180, 220, 255); fill(180, 220, 255);
text( text(
`${this.isPaused ? 'paused' : 'plotting'} ${this.plotProgress}/${this.previewPath.length}`, `${this._job === 'paused' ? 'paused' : this._job === 'aborting' ? 'stopped' : 'plotting'} ${this.plotProgress}/${this.previewPath.length}`,
16, 16,
152 152
); );