diff --git a/examples/typescript/src/index.html b/examples/typescript/src/index.html index 91299155..81adf69d 100644 --- a/examples/typescript/src/index.html +++ b/examples/typescript/src/index.html @@ -80,6 +80,10 @@

Console

+ + + +

diff --git a/examples/typescript/src/index.ts b/examples/typescript/src/index.ts index e46b5f8e..4d06d81c 100644 --- a/examples/typescript/src/index.ts +++ b/examples/typescript/src/index.ts @@ -1,5 +1,7 @@ const baudrates = document.getElementById("baudrates") as HTMLSelectElement; const consoleBaudrates = document.getElementById("consoleBaudrates") as HTMLSelectElement; +const reconnectDelay = document.getElementById("reconnectDelay") as HTMLInputElement; +const maxRetriesInput = document.getElementById("maxRetries") as HTMLInputElement; const connectButton = document.getElementById("connectButton") as HTMLButtonElement; const traceButton = document.getElementById("copyTraceButton") as HTMLButtonElement; const disconnectButton = document.getElementById("disconnectButton") as HTMLButtonElement; @@ -37,6 +39,7 @@ const term = new Terminal({ cols: 120, rows: 40 }); term.open(terminal); let device = null; +let deviceInfo = null; let transport: Transport; let chip: string = null; let esploader: ESPLoader; @@ -88,6 +91,7 @@ connectButton.onclick = async () => { try { if (device === null) { device = await serialLib.requestPort({}); + deviceInfo = device.getInfo(); transport = new Transport(device, true); } const flashOptions = { @@ -209,6 +213,7 @@ function removeRow(row: HTMLTableRowElement) { */ function cleanUp() { device = null; + deviceInfo = null; transport = null; chip = null; } @@ -232,11 +237,71 @@ disconnectButton.onclick = async () => { }; let isConsoleClosed = false; +let isReconnecting = false; + +const sleep = async (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + consoleStartButton.onclick = async () => { if (device === null) { device = await serialLib.requestPort({}); transport = new Transport(device, true); + deviceInfo = device.getInfo(); + + // Set up device lost callback + transport.setDeviceLostCallback(async () => { + if (!isConsoleClosed && !isReconnecting) { + term.writeln("\n[DEVICE LOST] Device disconnected. Trying to reconnect..."); + await sleep(parseInt(reconnectDelay.value)); + isReconnecting = true; + + const maxRetries = parseInt(maxRetriesInput.value); + let retryCount = 0; + + while (retryCount < maxRetries && !isConsoleClosed) { + retryCount++; + term.writeln(`\n[RECONNECT] Attempt ${retryCount}/${maxRetries}...`); + + if (serialLib && serialLib.getPorts) { + const ports = await serialLib.getPorts(); + if (ports.length > 0) { + const newDevice = ports.find( + (port) => + port.getInfo().usbVendorId === deviceInfo.usbVendorId && + port.getInfo().usbProductId === deviceInfo.usbProductId, + ); + + if (newDevice) { + device = newDevice; + transport.updateDevice(device); + term.writeln("[RECONNECT] Found previously authorized device, connecting..."); + await transport.connect(parseInt(consoleBaudrates.value)); + term.writeln("[RECONNECT] Successfully reconnected!"); + consoleStopButton.style.display = "initial"; + resetButton.style.display = "initial"; + isReconnecting = false; + + startConsoleReading(); + return; + } + } + } + + if (retryCount < maxRetries) { + term.writeln(`[RECONNECT] Device not found, retrying in ${parseInt(reconnectDelay.value)}ms...`); + await sleep(parseInt(reconnectDelay.value)); + } + } + + if (retryCount >= maxRetries) { + term.writeln("\n[RECONNECT] Failed to reconnect after 5 attempts. Please manually reconnect."); + isReconnecting = false; + } + } + }); } + lblConsoleFor.style.display = "block"; lblConsoleBaudrate.style.display = "none"; consoleBaudrates.style.display = "none"; @@ -247,21 +312,45 @@ consoleStartButton.onclick = async () => { await transport.connect(parseInt(consoleBaudrates.value)); isConsoleClosed = false; + isReconnecting = false; + + startConsoleReading(); +}; + +/** + * Start the console reading loop + */ +async function startConsoleReading() { + if (isConsoleClosed || !transport) return; - while (true && !isConsoleClosed) { + try { const readLoop = transport.rawRead(); - const { value, done } = await readLoop.next(); - if (done || !value) { - break; + while (true && !isConsoleClosed) { + const { value, done } = await readLoop.next(); + + if (done || !value) { + break; + } + + if (value) { + term.write(value); + } + } + } catch (error) { + if (!isConsoleClosed) { + term.writeln(`\n[CONSOLE ERROR] ${error instanceof Error ? error.message : String(error)}`); } - term.write(value); } - console.log("quitting console"); -}; + + if (!isConsoleClosed) { + term.writeln("\n[CONSOLE] Connection lost, waiting for reconnection..."); + } +} consoleStopButton.onclick = async () => { isConsoleClosed = true; + isReconnecting = false; if (transport) { await transport.disconnect(); await transport.waitForUnlock(1500); diff --git a/src/image/base.ts b/src/image/base.ts index 6af9acd3..8622732f 100644 --- a/src/image/base.ts +++ b/src/image/base.ts @@ -5,11 +5,23 @@ import { checksum, ESP_CHECKSUM_MAGIC, padTo } from "../util"; export const ESP_IMAGE_MAGIC = 0xe9; +/** + * Return position aligned to size + * @param {number} position Position to align + * @param {number} size Alignment size + * @returns {number} Aligned position + */ export function alignFilePosition(position: number, size: number): number { const align = size - 1 - (position % size); return position + align; } +/** + * Read a UINT32 from a byte array (little-endian) + * @param {Uint8Array} data Data to read a UINT32 + * @param {number} offset data start offset + * @returns {number} The read UINT32 value + */ function readUInt32LE(data: Uint8Array, offset: number): number { return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24); } @@ -233,8 +245,8 @@ export class BaseFirmwareImage { /** * Return ESPLoader checksum from end of just-read image - * @param data image to read checksum from - * @param offset Current offset in image + * @param {Uint8Array} data image to read checksum from + * @param {number} offset Current offset in image * @returns {number} checksum value */ readChecksum(data: Uint8Array, offset: number): number { diff --git a/src/image/index.ts b/src/image/index.ts index c9791185..1ba3df44 100644 --- a/src/image/index.ts +++ b/src/image/index.ts @@ -19,7 +19,7 @@ import { /** * Function to load a firmware image from a string (from FileReader) * @param {ROM} rom - The ROM object representing the target device - * @param imageData Image data as a string + * @param {string} imageData Image data as a string * @returns {Promise} - A promise that resolves to the loaded firmware image */ export async function loadFirmwareImage(rom: ROM, imageData: string): Promise { diff --git a/src/webserial.ts b/src/webserial.ts index 14bd6fc3..19bda32e 100644 --- a/src/webserial.ts +++ b/src/webserial.ts @@ -63,11 +63,29 @@ class Transport { private lastTraceTime = Date.now(); private reader: ReadableStreamDefaultReader | undefined; private buffer: Uint8Array = new Uint8Array(0); + private onDeviceLostCallback: (() => void) | null = null; constructor(public device: SerialPort, public tracing = false, enableSlipReader = true) { this.slipReaderEnabled = enableSlipReader; } + /** + * Set callback for when device is lost + * @param {Function} callback Function to call when device is lost + */ + setDeviceLostCallback(callback: (() => void) | null) { + this.onDeviceLostCallback = callback; + } + + /** + * Update the device reference (used when re-selecting device after reset) + * @param {typeof import("w3c-web-serial").SerialPort} newDevice New SerialPort device + */ + updateDevice(newDevice: SerialPort) { + this.device = newDevice; + this.trace("Device reference updated"); + } + /** * Request the serial device vendor ID and Product ID as string. * @returns {string} Return the device VendorID and ProductID from SerialPortInfo as formatted string. @@ -388,6 +406,14 @@ class Transport { } } catch (error) { console.error("Error reading from serial port:", error); + + // Check if it's a NetworkError indicating device loss + if (error instanceof Error && error.name === "NetworkError" && error.message.includes("device has been lost")) { + this.trace("Device lost detected (NetworkError)"); + if (this.onDeviceLostCallback) { + this.onDeviceLostCallback(); + } + } } finally { this.buffer = new Uint8Array(0); }