Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/typescript/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ <h3>Console </h3>
<option value="115200">115200</option>
<option value="74880">74880</option>
</select>
<label for="reconnectDelay" id="lblReconnectDelay">Reconnect Delay (ms):</label>
<input type="number" id="reconnectDelay" name="reconnectDelay" value="1000" min="100" max="10000" step="100">
<label for="maxRetries" id="lblMaxRetries">Max Retries:</label>
<input type="number" id="maxRetries" name="maxRetries" value="5" min="1" max="20" step="1">

<br><br>

Expand Down
103 changes: 96 additions & 7 deletions examples/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -37,6 +39,7 @@
term.open(terminal);

let device = null;
let deviceInfo = null;
let transport: Transport;
let chip: string = null;
let esploader: ESPLoader;
Expand Down Expand Up @@ -88,6 +91,7 @@
try {
if (device === null) {
device = await serialLib.requestPort({});
deviceInfo = device.getInfo();
transport = new Transport(device, true);
}
const flashOptions = {
Expand All @@ -103,7 +107,7 @@

// Temporarily broken
// await esploader.flashId();
console.log("Settings done for :" + chip);

Check warning on line 110 in examples/typescript/src/index.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
lblBaudrate.style.display = "none";
lblConnTo.innerHTML = "Connected to device: " + chip;
lblConnTo.style.display = "block";
Expand All @@ -114,7 +118,7 @@
filesDiv.style.display = "initial";
consoleDiv.style.display = "none";
} catch (e) {
console.error(e);

Check warning on line 121 in examples/typescript/src/index.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
term.writeln(`Error: ${e.message}`);
}
};
Expand All @@ -138,7 +142,7 @@
try {
await esploader.eraseFlash();
} catch (e) {
console.error(e);

Check warning on line 145 in examples/typescript/src/index.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
term.writeln(`Error: ${e.message}`);
} finally {
eraseButton.disabled = false;
Expand Down Expand Up @@ -209,6 +213,7 @@
*/
function cleanUp() {
device = null;
deviceInfo = null;
transport = null;
chip = null;
}
Expand All @@ -232,11 +237,71 @@
};

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";
Expand All @@ -247,21 +312,45 @@

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);
Expand Down Expand Up @@ -356,7 +445,7 @@
await esploader.writeFlash(flashOptions);
await esploader.after();
} catch (e) {
console.error(e);

Check warning on line 448 in examples/typescript/src/index.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
term.writeln(`Error: ${e.message}`);
} finally {
// Hide progress bars and show erase buttons
Expand Down
16 changes: 14 additions & 2 deletions src/image/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@

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);
}
Expand Down Expand Up @@ -145,7 +157,7 @@
warnIfUnusualSegment(offset: number, size: number, isIromSegment: boolean): void {
if (!isIromSegment) {
if (offset > 0x40200000 || offset < 0x3ffe0000 || size > 65536) {
console.warn(`WARNING: Suspicious segment 0x${offset.toString(16)}, length ${size}`);

Check warning on line 160 in src/image/base.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
}
}
}
Expand Down Expand Up @@ -233,8 +245,8 @@

/**
* 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 {
Expand Down Expand Up @@ -347,7 +359,7 @@
setMmuPageSize(size: number): void {
if (!this.MMU_PAGE_SIZE_CONF && size !== this.IROM_ALIGN) {
// For chips where MMU page size cannot be set or is fixed, log a warning and use the default.
console.warn(

Check warning on line 362 in src/image/base.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
`WARNING: Changing MMU page size is not supported on ${this.ROM_LOADER.CHIP_NAME}! ` +
(this.IROM_ALIGN !== 0 ? `Defaulting to ${this.IROM_ALIGN / 1024}KB.` : ""),
);
Expand Down
2 changes: 1 addition & 1 deletion src/image/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseFirmwareImage>} - A promise that resolves to the loaded firmware image
*/
export async function loadFirmwareImage(rom: ROM, imageData: string): Promise<BaseFirmwareImage> {
Expand Down
26 changes: 26 additions & 0 deletions src/webserial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,29 @@ class Transport {
private lastTraceTime = Date.now();
private reader: ReadableStreamDefaultReader<Uint8Array> | 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.
Expand Down Expand Up @@ -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);
}
Expand Down
Loading