diff --git a/lib/README.md b/lib/README.md index 8e9e58d..ebfa4a0 100644 --- a/lib/README.md +++ b/lib/README.md @@ -152,11 +152,11 @@ Returns estimated duration in **milliseconds** for the current path buffer, or f `stopPath()` aborts the current `runPath` and unblocks any wait for Marlin `ok`. -`stopPlot()` **halts and pauses** so **Resume** can continue from the current position (live plot: `M410` quick-stop, then `M114`). Key **S** and a Marlin `ok` timeout do the same. **S** / Stop again while paused aborts the run. +`stopPlot()` first press is the same as **Pause**: finish the current stroke, then **Resume** continues. Second press is an emergency `M410` quick-stop. That leaves belt length unknown on a polargraph, so **home is cleared** — jog to origin and press **H** before plotting again. An `ok` timeout pauses the same way (no `M410`); that segment is skipped. ### `pausePlot()` / `resumePlot()` -**Pause** waits for the current segment to finish, then lifts the pen. **Stop** / timeout interrupt the current move immediately. Both leave the Plot button as **Resume**. Elapsed / remaining HUD time freezes while paused. +**Pause** / first **Stop** wait for the current segment to finish (`M400`), then lift the pen. Do not resume after emergency stop until you re-home. Elapsed / remaining HUD time freezes while paused. --- diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index 0016c61..d14e435 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -18,7 +18,7 @@ class WallPlotter { // Static constants // --------------------------------------------------------------------------- - static VERSION = '2.2.0'; + static VERSION = '2.2.2'; static MIN_FEED_MM_MIN = 1; static MAX_FEED_MM_MIN = 10000; /** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */ @@ -117,6 +117,10 @@ class WallPlotter { this.readLoopActive = false; this._rxBuf = ''; this._okResolvers = []; + this._sendChain = Promise.resolve(); + this._okIgnoreUntil = 0; + this._machineBusy = false; + this._lastBusyLogAt = 0; // --- Keyboard / buttons --- this._keysEnabled = false; @@ -349,21 +353,34 @@ class WallPlotter { } /** - * Halt the current move and pause so Resume can continue. - * If already paused, abort the run. Also cancels a stuck `ok` wait. + * First press: pause after the current stroke (safe to Resume). + * Second press: emergency M410 — home is cleared, do not resume. */ async stopPlot() { const hadWait = this._okResolvers.length > 0; if (this.isPlotting && this.isPaused) { this.stopPath(); - this.statusText = 'Stopped'; - this.onLog('plot aborted'); + if (this.connected && !this._plotTestOnly) { + try { + await this.send('M410', { waitOk: false, allowWithoutHome: true }); + } catch (_) { + /* port may be busy */ + } + this.homeSet = false; + this._okIgnoreUntil = performance.now() + 1500; + this._homeWarning = 'Re-home (H) after emergency stop — position is unknown'; + this.statusText = this._homeWarning; + } else { + this.statusText = 'Stopped'; + } + this.onLog('plot aborted — set home before plotting again'); return; } if (this.isPlotting) { - await this._haltToPause('stopped'); + this.pausePlot(); + this.onLog('plot stopped after current move — resume to continue'); return; } @@ -451,6 +468,12 @@ class WallPlotter { * @param {{allowWithoutHome?: boolean, waitOk?: boolean, timeoutMs?: number}} [options] */ async send(line, options = {}) { + const queued = this._sendChain.then(() => this._sendNow(line, options)); + this._sendChain = queued.catch(() => {}); + return queued; + } + + async _sendNow(line, options = {}) { if (!this.connected || !this.writer) { throw new Error('Not connected to plotter'); } @@ -478,6 +501,7 @@ class WallPlotter { await okPromise; } catch (err) { if (err.name === 'AbortError') throw err; + this._okIgnoreUntil = performance.now() + 400; const timeoutErr = new Error(err.message); timeoutErr.name = 'TimeoutError'; this.onLog('! ' + err.message); @@ -547,9 +571,24 @@ class WallPlotter { else await this.moveTo(this.x + dx, this.y + dy); } - /** G92 X0 Y0 — set current pose as home. */ + /** G92 X0 Y0 — set current pose as home. Refused while plotting or waiting for Marlin. */ async setHomeHere() { + if (this.isPlotting) { + this.onLog('! cannot set home while plotting — resume or abort first'); + this.statusText = 'Finish or abort plot before H'; + return; + } + if (!this._isMachineIdle()) { + this.onLog('! plotter busy — wait for ok before setting home'); + this.statusText = 'Waiting for plotter…'; + return; + } await this.send('G92 X0 Y0'); + try { + await this.send('M114', { timeoutMs: 3000, allowWithoutHome: true }); + } catch (_) { + /* position report optional */ + } this.x = 0; this.y = 0; this.homeSet = true; @@ -618,7 +657,6 @@ class WallPlotter { continue; } - let retry = false; try { if (cmd.type === 'wait') { const ms = Math.max(0, cmd.ms ?? 0); @@ -642,27 +680,32 @@ class WallPlotter { } } } catch (err) { - if (err.name === 'AbortError') { - retry = this.isPaused && !this._pathAbort; - if (!retry) break; - } else if (err.name === 'TimeoutError') { - if (!this.isPaused) await this._haltToPause('timeout'); - retry = !this._pathAbort; - if (!retry) break; - } else { - throw err; + if (err.name === 'AbortError') break; + if (err.name === 'TimeoutError') { + if (!this.isPaused) this.pausePlot(); + this.onLog('segment skipped after timeout — resume continues from here'); + px = this.x; + py = this.y; + await this._waitIfPaused(); + if (this._pathAbort || runId !== this._pathRunId) break; + if (cmd.type === 'move' || cmd.type === 'draw') { + px = this.x; + py = this.y; + } + this.plotProgress = i + 1; + continue; } + throw err; } if (this._pathAbort || runId !== this._pathRunId) break; - if (retry || this._freezeMotion) { + if (this._freezeMotion) { px = this.x; py = this.y; await this._waitIfPaused(); this._freezeMotion = false; if (this._pathAbort || runId !== this._pathRunId) break; - i -= 1; continue; } @@ -787,45 +830,6 @@ class WallPlotter { } } - /** Interrupt the current move (M410) and pause so Resume can continue. */ - async _haltToPause(reason) { - if (!this.isPlotting || this.isPaused) return; - this._freezeMotion = true; - this._cancelPendingOk(); - this.isPaused = true; - this._pauseStartedAt = performance.now(); - this._statusBeforePause = this.connected ? 'Connected' : 'Disconnected'; - this.statusText = 'Paused'; - this.onLog( - reason === 'timeout' - ? 'plot paused after timeout — resume to continue' - : 'plot stopped — resume to continue' - ); - this._updatePlotButtons(); - this._redrawSketch(); - - if (this.connected && !this._plotTestOnly) { - try { - await this.send('M410', { waitOk: false, allowWithoutHome: true }); - } catch (_) { - /* port may be busy */ - } - try { - await this.send('M114', { - timeoutMs: 3000, - allowWithoutHome: true, - }); - } catch (_) { - /* USB may be dead */ - } - try { - await this.penUp(); - } catch (_) { - /* ignore */ - } - } - } - /** Wait while paused. Live plots lift the pen until the next segment. */ async _waitIfPaused() { if (!this.isPaused) return; @@ -927,6 +931,7 @@ class WallPlotter { const waitLogTimer = setTimeout(() => { if (settled) return; + const started = performance.now(); this.onLog('… waiting for Marlin ok'); if (this.statusText !== 'Waiting for plotter…') { this._statusBeforeOkWait = this.statusText; @@ -934,7 +939,8 @@ class WallPlotter { this.statusText = 'Waiting for plotter…'; this._redrawSketch(); waitLogInterval = setInterval(() => { - this.onLog('… still waiting for Marlin ok'); + const s = Math.round((performance.now() - started) / 1000); + this.onLog(`… still waiting for Marlin ok (${s}s)`); }, WallPlotter.OK_WAIT_PING_MS); }, WallPlotter.OK_WAIT_LOG_MS); @@ -943,6 +949,7 @@ class WallPlotter { } _cancelPendingOk() { + this._okIgnoreUntil = performance.now() + 400; const pending = this._okResolvers.slice(); for (const entry of pending) { const err = new Error('aborted'); @@ -988,8 +995,22 @@ class WallPlotter { } _handleResponse(line) { + if (/busy:\s*processing/i.test(line)) { + this._machineBusy = true; + const now = performance.now(); + if (now - this._lastBusyLogAt > 4000) { + this._lastBusyLogAt = now; + this.onLog('← busy (move still running)'); + } + return; + } this.onLog('← ' + line); if (/^ok\b/i.test(line)) { + this._machineBusy = false; + if (performance.now() < this._okIgnoreUntil) { + this.onLog('← ok ignored (stale)'); + return; + } const next = this._okResolvers.shift(); if (next) next.resolve(); } @@ -1024,6 +1045,10 @@ class WallPlotter { return /^G[01]\b/i.test(command) && /\b[XY]/i.test(command); } + _isMachineIdle() { + return this._okResolvers.length === 0 && !this._machineBusy; + } + _showHomeRequiredWarning() { const msg = 'Home not set — press H before sending moves'; this._homeWarning = msg; @@ -1063,6 +1088,7 @@ class WallPlotter { return false; } if (k === '0') { + if (this.isPlotting || !this._isMachineIdle()) return false; await this.goToZero(); return false; } @@ -1082,7 +1108,8 @@ class WallPlotter { } _handleContinuousJog() { - if (!this.connected || this._jogInFlight) return; + if (!this.connected || this._jogInFlight || this.isPlotting) return; + if (!this.homeSet || !this._isMachineIdle()) return; const draw = keyIsDown(SHIFT); const s = this.stepSize; @@ -1112,8 +1139,8 @@ class WallPlotter { async _jogBy(dx, dy, draw = false) { try { + if (!this._requireHome()) return; if (draw) { - if (!this._requireHome()) return; await this.drawTo(this.x + dx, this.y + dy); return; } @@ -1121,8 +1148,7 @@ class WallPlotter { const nx = this.x + dx; const ny = this.y + dy; await this.send( - `G1 X${this._fmt(nx)} Y${this._fmt(ny)} F${this.feedRateTravel}`, - { allowWithoutHome: true } + `G1 X${this._fmt(nx)} Y${this._fmt(ny)} F${this.feedRateTravel}` ); this.x = nx; this.y = ny; @@ -1491,7 +1517,7 @@ class WallPlotter { 'H set current pos as home (G92) — required first', '0 go to origin (0, 0)', 'P toggle pen (test up/down angles)', - 'S halt plot (Resume to continue, S again to abort)', + 'S stop after current stroke (S again = e-stop, re-home)', 'C connect / disconnect', ]; for (let i = 0; i < help.length; i++) {