diff --git a/examples/netz/index.html b/examples/netz/index.html index 2f9cda3..431a561 100644 --- a/examples/netz/index.html +++ b/examples/netz/index.html @@ -42,7 +42,7 @@ } - + diff --git a/examples/zickzack/index.html b/examples/zickzack/index.html index 2f9cda3..431a561 100644 --- a/examples/zickzack/index.html +++ b/examples/zickzack/index.html @@ -42,7 +42,7 @@ } - + diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index aadd8b2..c36af08 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -18,7 +18,7 @@ class WallPlotter { // Static constants // --------------------------------------------------------------------------- - static VERSION = '2.3.4'; + static VERSION = '2.3.5'; 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 */ @@ -32,6 +32,8 @@ class WallPlotter { /** If a USB write does not complete, the ACM device is usually gone. */ static WRITE_TIMEOUT_MS = 3000; static RESUME_STORAGE_KEY = 'wallplotter.resume.v1'; + /** Send a quiet M400 if the line has been silent this long (keeps USB awake). */ + static KEEPALIVE_MS = 1200; // --------------------------------------------------------------------------- // Construction @@ -118,6 +120,8 @@ class WallPlotter { this._okWaiter = null; this._sendChain = Promise.resolve(); this._lastBusyLogAt = 0; + this._lastSendAt = 0; + this._keepaliveTimer = null; // --- Keyboard / buttons --- this._keysEnabled = false; @@ -429,6 +433,7 @@ class WallPlotter { this.writer = this.port.writable.getWriter(); this._startReadLoop(); + this._startKeepalive(); this._link = 'connected'; this.onStatusChange('connected'); @@ -493,6 +498,7 @@ class WallPlotter { if (this._link === 'disconnected' && !this.port) return; this.readLoopActive = false; + this._stopKeepalive(); this._cancelPendingOk(); this._rxBuf = ''; @@ -538,6 +544,7 @@ class WallPlotter { _releaseSerialNow() { if (!this.port && this._link === 'disconnected') return; this.readLoopActive = false; + this._stopKeepalive(); this._cancelPendingOk(); this._rxBuf = ''; const reader = this.reader; @@ -569,7 +576,7 @@ class WallPlotter { } } - _writeLine(command) { + _writeLine(command, quiet = false) { if (!this.writer) { return Promise.reject(new Error('Not connected to plotter')); } @@ -588,7 +595,7 @@ class WallPlotter { if (done) return; done = true; clearTimeout(timer); - this.onLog('→ ' + command); + if (!quiet) this.onLog('→ ' + command); resolve(); }, (err) => { @@ -673,9 +680,9 @@ class WallPlotter { if (this._unloadBound) return; this._unloadBound = true; const release = () => { - this._stashInterruptedPath(); - this._persistResumeState(true); - this._releaseSerialNow(); + this._stashInterruptedPath(); + this._persistResumeState(true); + this._releaseSerialNow(); }; window.addEventListener('pagehide', release); window.addEventListener('beforeunload', release); @@ -806,15 +813,18 @@ class WallPlotter { return; } + this._lastSendAt = performance.now(); const waitOk = options.waitOk !== false; + const quiet = options.quiet === true || /^M400\b/i.test(command); const okPromise = waitOk ? this._waitForOk(options.timeoutMs ?? WallPlotter.OK_TIMEOUT_MS, { command, waitLogAfterMs: options.waitLogAfterMs, + quiet, }) : null; try { - const written = this._writeLine(command); + const written = this._writeLine(command, quiet); if (okPromise) { await Promise.all([ written.catch((err) => { @@ -1268,8 +1278,11 @@ class WallPlotter { _waitForOk(timeoutMs, options = {}) { const command = options.command || ''; - const waitLogAfterMs = options.waitLogAfterMs ?? WallPlotter.OK_WAIT_LOG_MS; - const waitLabel = command ? ` (${command})` : ''; + const quiet = options.quiet === true; + const waitLogAfterMs = quiet + ? Math.max(options.waitLogAfterMs ?? WallPlotter.OK_WAIT_LOG_MS, 8000) + : options.waitLogAfterMs ?? WallPlotter.OK_WAIT_LOG_MS; + const waitLabel = command && !quiet ? ` (${command})` : ''; return new Promise((resolve, reject) => { if (this._okWaiter) { @@ -1339,6 +1352,32 @@ class WallPlotter { waiter.reject(err); } + _startKeepalive() { + this._stopKeepalive(); + this._keepaliveTimer = setInterval(() => { + this._keepaliveTick(); + }, WallPlotter.KEEPALIVE_MS); + } + + _stopKeepalive() { + if (this._keepaliveTimer != null) { + clearInterval(this._keepaliveTimer); + this._keepaliveTimer = null; + } + } + + _keepaliveTick() { + if (this._link !== 'connected' || !this.writer) return; + if (this._okWaiter || this._job === 'aborting') return; + if (performance.now() - this._lastSendAt < WallPlotter.KEEPALIVE_MS) return; + this.send('M400', { + quiet: true, + allowWithoutHome: true, + timeoutMs: 4000, + waitLogAfterMs: 10000, + }).catch(() => {}); + } + _startReadLoop() { this.readLoopActive = true; const plotter = this; @@ -1403,8 +1442,9 @@ class WallPlotter { } return; } - this.onLog('← ' + line); - if (/^(echo:\s*)?ok\b/i.test(line)) { + const isOk = /^(echo:\s*)?ok\b/i.test(line); + if (!isOk) this.onLog('← ' + line); + if (isOk) { const waiter = this._okWaiter; if (waiter) waiter.resolve(); }