From c05cc43ccb8b2e0e724d0c58780fb9e7505028d0 Mon Sep 17 00:00:00 2001 From: Martin Fuchs Date: Sun, 16 Aug 2026 16:41:10 +0200 Subject: [PATCH] serial osx fix? --- lib/README.md | 2 +- lib/WallPlotter.js | 132 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/lib/README.md b/lib/README.md index 3ba74f6..4aa1779 100644 --- a/lib/README.md +++ b/lib/README.md @@ -166,7 +166,7 @@ All **async**. Coordinate moves on Plot require home (`H`) first. ### `connect()` -Web Serial port picker, enable steppers (`M17`), pen up, query polargraph area (`M665` → machine overlay bounds), `M114`. +Web Serial port picker, enable steppers (`M17`), pen up, query polargraph area (`M665` → machine overlay bounds), `M114`. Reload/close releases the port (needed on macOS). If only one previously allowed port exists, it is reused. ### `disconnect()` diff --git a/lib/WallPlotter.js b/lib/WallPlotter.js index ce8c6c3..42b9aee 100644 --- a/lib/WallPlotter.js +++ b/lib/WallPlotter.js @@ -18,7 +18,7 @@ class WallPlotter { // Static constants // --------------------------------------------------------------------------- - static VERSION = '2.2.2'; + static VERSION = '2.2.3'; 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 */ @@ -156,6 +156,9 @@ class WallPlotter { this._updatePlotButtons(); userOnStatusChange(status); }; + + this._unloadBound = false; + this._installUnloadHandler(); } // =========================================================================== @@ -394,8 +397,16 @@ class WallPlotter { } if (this.connected) return; - this.port = await navigator.serial.requestPort(); - await this.port.open({ baudRate: this.baudRate }); + let port; + try { + port = await this._acquirePort(); + await this._openPort(port); + } catch (err) { + this.onLog('! ' + err.message); + port = await navigator.serial.requestPort(); + await this._openPort(port); + } + this.port = port; this.writer = this.port.writable.getWriter(); this._startReadLoop(); @@ -415,37 +426,81 @@ class WallPlotter { } } + async _acquirePort() { + const existing = await navigator.serial.getPorts(); + if (existing.length === 1) return existing[0]; + return navigator.serial.requestPort(); + } + + async _openPort(port) { + const opened = port.open({ baudRate: this.baudRate }); + const timedOut = this._delay(3000).then(() => { + const err = new Error( + 'serial open timeout — port still held after reload. Unplug USB or close other serial tabs.' + ); + err.name = 'TimeoutError'; + throw err; + }); + try { + await Promise.race([opened, timedOut]); + } catch (err) { + try { + await port.close(); + } catch (_) { + /* ignore */ + } + if (typeof port.forget === 'function') { + try { + await port.forget(); + } catch (_) { + /* ignore */ + } + } + throw err; + } + } + /** Close the serial port. */ async disconnect() { - if (!this.connected) return; + if (!this.connected && !this.port) return; this.readLoopActive = false; this._cancelPendingOk(); this._rxBuf = ''; + this._machineBusy = false; - if (this.reader) { + const reader = this.reader; + const writer = this.writer; + const port = this.port; + this.reader = null; + this.writer = null; + this.port = null; + + if (reader) { try { - await this.reader.cancel(); + await reader.cancel(); } catch (_) { /* already closed */ } - this.reader = null; - } - if (this.writer) { try { - this.writer.releaseLock(); + reader.releaseLock(); } catch (_) { /* ignore */ } - this.writer = null; } - if (this.port) { + if (writer) { try { - await this.port.close(); + writer.releaseLock(); + } catch (_) { + /* ignore */ + } + } + if (port) { + try { + await port.close(); } catch (_) { /* ignore */ } - this.port = null; } this.connected = false; @@ -454,6 +509,55 @@ class WallPlotter { this.onStatusChange('disconnected'); } + /** Best-effort close for page reload (must not await — Mac keeps the USB lock otherwise). */ + _releaseSerialNow() { + if (!this.port && !this.connected) return; + this.readLoopActive = false; + this._cancelPendingOk(); + this._rxBuf = ''; + this._machineBusy = false; + const reader = this.reader; + const writer = this.writer; + const port = this.port; + this.reader = null; + this.writer = null; + this.port = null; + this.connected = false; + this.homeSet = false; + try { + reader?.cancel(); + } catch (_) { + /* ignore */ + } + try { + reader?.releaseLock(); + } catch (_) { + /* ignore */ + } + try { + writer?.releaseLock(); + } catch (_) { + /* ignore */ + } + try { + port?.close(); + } catch (_) { + /* ignore */ + } + } + + _installUnloadHandler() { + if (this._unloadBound) return; + this._unloadBound = true; + const release = () => this._releaseSerialNow(); + window.addEventListener('pagehide', release); + window.addEventListener('beforeunload', release); + window.addEventListener('unload', release); + window.addEventListener('pageshow', (event) => { + if (event.persisted) this._releaseSerialNow(); + }); + } + /** * Write one G-code line; by default wait for Marlin ok. * @param {string} line