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);
}