From 88f6ddbb515588f2d41934168a61d497dc36f468 Mon Sep 17 00:00:00 2001 From: Surfbee3 Date: Tue, 23 Jun 2026 10:10:40 +1000 Subject: [PATCH] Support boards that auto-reset on serial open (DTR->RST), e.g. STM32WL/Wio-E5 Some boards (e.g. Seeed Wio-E5 / STM32WL on a USB-UART bridge) wire DTR -> RST through a cap, so opening the serial port asserts DTR and resets the MCU. The connection then fires its handshake (deviceQuery) immediately and races the boot (~1.2-1.9 s measured), so the query is lost and the connection times out. These boards currently cannot connect to the web app / CLI at all. This makes the client tolerate the open-time reset: - De-assert DTR/RTS right after open() so the line sits steady (Chrome's Web Serial and Node serialport assert DTR on open). Wrapped in try/catch; a no-op on boards that don't wire DTR to reset, and setting both low is the run-normally state for ESP-style auto-reset circuits (won't trigger the bootloader). - Add an optional bootDelayMs to wait for boot before handshaking. Default 0, so behaviour is unchanged for native-USB boards. Reset-on-open boards pass e.g. 2500. Web: WebSerialConnection.open({ bootDelayMs: 2500 }) Node: new NodeJSSerialConnection(path, 2500) Verified on a Seeed Wio-E5 (STM32WL) companion: getSelfInfo() round-trips with bootDelayMs=2500 where it previously timed out. Note for app integrators: if you pass bootDelayMs, size your overall connection timeout >= bootDelayMs + handshake (the library handshake has no internal timeout). Co-Authored-By: Claude Opus 4.8 --- src/connection/nodejs_serial_connection.js | 27 ++++++++++++++-- src/connection/web_serial_connection.js | 37 ++++++++++++++++++---- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/connection/nodejs_serial_connection.js b/src/connection/nodejs_serial_connection.js index 4bc519d..cc80c31 100644 --- a/src/connection/nodejs_serial_connection.js +++ b/src/connection/nodejs_serial_connection.js @@ -4,10 +4,14 @@ class NodeJSSerialConnection extends SerialConnection { /** * @param path serial port to connect to, e.g: "/dev/ttyACM0" or "/dev/cu.usbmodem14401" + * @param bootDelayMs ms to wait after the port opens before handshaking. Set to + * ~2500 for boards that reset on connect (DTR -> RST, e.g. Wio-E5/STM32WL + * devboards). Default 0 preserves the original behaviour. */ - constructor(path) { + constructor(path, bootDelayMs = 0) { super(); this.serialPortPath = path; + this.bootDelayMs = bootDelayMs; } async connect() { @@ -23,7 +27,26 @@ class NodeJSSerialConnection extends SerialConnection { }); this.serialPort.on("open", async () => { - await this.onConnected(); + + // De-assert DTR/RTS. On boards that wire DTR -> RST (via a cap), opening + // the port asserts DTR and resets the MCU; hold a steady de-asserted state + // to avoid further spurious resets. Harmless on native-USB boards. + try { + this.serialPort.set({ dtr: false, rts: false }); + } catch(e) { + // set() unsupported / failed; ignore + } + + // Wait for the device to finish booting before handshaking. The MCU was + // reset by the DTR assertion on open; the original immediate onConnected() + // raced that boot (~1.2-1.9 s measured on a Wio-E5) and the handshake was + // lost -> connection timeout. Default bootDelayMs 0 = original behaviour. + if(this.bootDelayMs > 0){ + await new Promise((resolve) => setTimeout(resolve, this.bootDelayMs)); + } + + await this.onConnected(); + }); this.serialPort.on("close", () => { diff --git a/src/connection/web_serial_connection.js b/src/connection/web_serial_connection.js index a9eae02..f43cd74 100644 --- a/src/connection/web_serial_connection.js +++ b/src/connection/web_serial_connection.js @@ -2,7 +2,7 @@ import SerialConnection from "./serial_connection.js"; class WebSerialConnection extends SerialConnection { - constructor(serialPort) { + constructor(serialPort, bootDelayMs = 0) { super(); @@ -16,14 +16,25 @@ class WebSerialConnection extends SerialConnection { this.onDisconnected(); }); - // fire connected callback after constructor has returned + // fire connected callback after constructor has returned. + // + // bootDelayMs lets boards that reset on connect finish booting before we + // send the handshake. Some boards (e.g. STM32WL / Seeed Wio-E5 devboards) + // wire DTR -> RST through a cap, so opening the port asserts DTR and resets + // the MCU. The original setTimeout(0) raced that boot -> handshake (deviceQuery) + // was sent into a rebooting MCU -> never answered -> connection timeout. + // Measured boot-to-ready on a Wio-E5 repeater: ~1.2-1.9 s, so ~2.5 s is safe. + // Default 0 preserves the original behaviour for native-USB boards. setTimeout(async () => { await this.onConnected(); - }, 0); + }, bootDelayMs); } - static async open() { + static async open(options = {}) { + + const baudRate = options.baudRate ?? 115200; + const bootDelayMs = options.bootDelayMs ?? 0; // set to ~2500 for DTR-reset boards (Wio-E5) // ensure browser supports web serial if(!navigator.serial){ @@ -38,10 +49,24 @@ class WebSerialConnection extends SerialConnection { // open port await serialPort.open({ - baudRate: 115200, + baudRate: baudRate, }); - return new WebSerialConnection(serialPort); + // De-assert DTR/RTS after opening. + // + // Chrome's Web Serial asserts DTR on open(); on boards that wire DTR -> RST + // (via a cap) that pulses a reset. We can't suppress the open-time assertion + // (the API gives no pre-open signal control), but holding a steady de-asserted + // state afterwards avoids further spurious resets mid-session. This is harmless + // on native-USB boards (no reset wire). setSignals may be unsupported on some + // platforms, so failures are ignored. + try { + await serialPort.setSignals({ dataTerminalReady: false, requestToSend: false }); + } catch(e) { + // setSignals unsupported / failed; ignore + } + + return new WebSerialConnection(serialPort, bootDelayMs); }