Skip to content

Commit ee5da44

Browse files
add onDeviceLostCallback to recover lost device (#212)
* add onDeviceLostCallback to recover lost device * fix lint * allow reconnect timeout custom add configurable retries
1 parent ed69ccb commit ee5da44

File tree

5 files changed

+141
-10
lines changed

5 files changed

+141
-10
lines changed

examples/typescript/src/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ <h3>Console </h3>
8080
<option value="115200">115200</option>
8181
<option value="74880">74880</option>
8282
</select>
83+
<label for="reconnectDelay" id="lblReconnectDelay">Reconnect Delay (ms):</label>
84+
<input type="number" id="reconnectDelay" name="reconnectDelay" value="1000" min="100" max="10000" step="100">
85+
<label for="maxRetries" id="lblMaxRetries">Max Retries:</label>
86+
<input type="number" id="maxRetries" name="maxRetries" value="5" min="1" max="20" step="1">
8387

8488
<br><br>
8589

examples/typescript/src/index.ts

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const baudrates = document.getElementById("baudrates") as HTMLSelectElement;
22
const consoleBaudrates = document.getElementById("consoleBaudrates") as HTMLSelectElement;
3+
const reconnectDelay = document.getElementById("reconnectDelay") as HTMLInputElement;
4+
const maxRetriesInput = document.getElementById("maxRetries") as HTMLInputElement;
35
const connectButton = document.getElementById("connectButton") as HTMLButtonElement;
46
const traceButton = document.getElementById("copyTraceButton") as HTMLButtonElement;
57
const disconnectButton = document.getElementById("disconnectButton") as HTMLButtonElement;
@@ -37,6 +39,7 @@ const term = new Terminal({ cols: 120, rows: 40 });
3739
term.open(terminal);
3840

3941
let device = null;
42+
let deviceInfo = null;
4043
let transport: Transport;
4144
let chip: string = null;
4245
let esploader: ESPLoader;
@@ -88,6 +91,7 @@ connectButton.onclick = async () => {
8891
try {
8992
if (device === null) {
9093
device = await serialLib.requestPort({});
94+
deviceInfo = device.getInfo();
9195
transport = new Transport(device, true);
9296
}
9397
const flashOptions = {
@@ -209,6 +213,7 @@ function removeRow(row: HTMLTableRowElement) {
209213
*/
210214
function cleanUp() {
211215
device = null;
216+
deviceInfo = null;
212217
transport = null;
213218
chip = null;
214219
}
@@ -232,11 +237,71 @@ disconnectButton.onclick = async () => {
232237
};
233238

234239
let isConsoleClosed = false;
240+
let isReconnecting = false;
241+
242+
const sleep = async (ms: number) => {
243+
return new Promise((resolve) => setTimeout(resolve, ms));
244+
};
245+
235246
consoleStartButton.onclick = async () => {
236247
if (device === null) {
237248
device = await serialLib.requestPort({});
238249
transport = new Transport(device, true);
250+
deviceInfo = device.getInfo();
251+
252+
// Set up device lost callback
253+
transport.setDeviceLostCallback(async () => {
254+
if (!isConsoleClosed && !isReconnecting) {
255+
term.writeln("\n[DEVICE LOST] Device disconnected. Trying to reconnect...");
256+
await sleep(parseInt(reconnectDelay.value));
257+
isReconnecting = true;
258+
259+
const maxRetries = parseInt(maxRetriesInput.value);
260+
let retryCount = 0;
261+
262+
while (retryCount < maxRetries && !isConsoleClosed) {
263+
retryCount++;
264+
term.writeln(`\n[RECONNECT] Attempt ${retryCount}/${maxRetries}...`);
265+
266+
if (serialLib && serialLib.getPorts) {
267+
const ports = await serialLib.getPorts();
268+
if (ports.length > 0) {
269+
const newDevice = ports.find(
270+
(port) =>
271+
port.getInfo().usbVendorId === deviceInfo.usbVendorId &&
272+
port.getInfo().usbProductId === deviceInfo.usbProductId,
273+
);
274+
275+
if (newDevice) {
276+
device = newDevice;
277+
transport.updateDevice(device);
278+
term.writeln("[RECONNECT] Found previously authorized device, connecting...");
279+
await transport.connect(parseInt(consoleBaudrates.value));
280+
term.writeln("[RECONNECT] Successfully reconnected!");
281+
consoleStopButton.style.display = "initial";
282+
resetButton.style.display = "initial";
283+
isReconnecting = false;
284+
285+
startConsoleReading();
286+
return;
287+
}
288+
}
289+
}
290+
291+
if (retryCount < maxRetries) {
292+
term.writeln(`[RECONNECT] Device not found, retrying in ${parseInt(reconnectDelay.value)}ms...`);
293+
await sleep(parseInt(reconnectDelay.value));
294+
}
295+
}
296+
297+
if (retryCount >= maxRetries) {
298+
term.writeln("\n[RECONNECT] Failed to reconnect after 5 attempts. Please manually reconnect.");
299+
isReconnecting = false;
300+
}
301+
}
302+
});
239303
}
304+
240305
lblConsoleFor.style.display = "block";
241306
lblConsoleBaudrate.style.display = "none";
242307
consoleBaudrates.style.display = "none";
@@ -247,21 +312,45 @@ consoleStartButton.onclick = async () => {
247312

248313
await transport.connect(parseInt(consoleBaudrates.value));
249314
isConsoleClosed = false;
315+
isReconnecting = false;
316+
317+
startConsoleReading();
318+
};
319+
320+
/**
321+
* Start the console reading loop
322+
*/
323+
async function startConsoleReading() {
324+
if (isConsoleClosed || !transport) return;
250325

251-
while (true && !isConsoleClosed) {
326+
try {
252327
const readLoop = transport.rawRead();
253-
const { value, done } = await readLoop.next();
254328

255-
if (done || !value) {
256-
break;
329+
while (true && !isConsoleClosed) {
330+
const { value, done } = await readLoop.next();
331+
332+
if (done || !value) {
333+
break;
334+
}
335+
336+
if (value) {
337+
term.write(value);
338+
}
339+
}
340+
} catch (error) {
341+
if (!isConsoleClosed) {
342+
term.writeln(`\n[CONSOLE ERROR] ${error instanceof Error ? error.message : String(error)}`);
257343
}
258-
term.write(value);
259344
}
260-
console.log("quitting console");
261-
};
345+
346+
if (!isConsoleClosed) {
347+
term.writeln("\n[CONSOLE] Connection lost, waiting for reconnection...");
348+
}
349+
}
262350

263351
consoleStopButton.onclick = async () => {
264352
isConsoleClosed = true;
353+
isReconnecting = false;
265354
if (transport) {
266355
await transport.disconnect();
267356
await transport.waitForUnlock(1500);

src/image/base.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,23 @@ import { checksum, ESP_CHECKSUM_MAGIC, padTo } from "../util";
55

66
export const ESP_IMAGE_MAGIC = 0xe9;
77

8+
/**
9+
* Return position aligned to size
10+
* @param {number} position Position to align
11+
* @param {number} size Alignment size
12+
* @returns {number} Aligned position
13+
*/
814
export function alignFilePosition(position: number, size: number): number {
915
const align = size - 1 - (position % size);
1016
return position + align;
1117
}
1218

19+
/**
20+
* Read a UINT32 from a byte array (little-endian)
21+
* @param {Uint8Array} data Data to read a UINT32
22+
* @param {number} offset data start offset
23+
* @returns {number} The read UINT32 value
24+
*/
1325
function readUInt32LE(data: Uint8Array, offset: number): number {
1426
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24);
1527
}
@@ -233,8 +245,8 @@ export class BaseFirmwareImage {
233245

234246
/**
235247
* Return ESPLoader checksum from end of just-read image
236-
* @param data image to read checksum from
237-
* @param offset Current offset in image
248+
* @param {Uint8Array} data image to read checksum from
249+
* @param {number} offset Current offset in image
238250
* @returns {number} checksum value
239251
*/
240252
readChecksum(data: Uint8Array, offset: number): number {

src/image/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
/**
2020
* Function to load a firmware image from a string (from FileReader)
2121
* @param {ROM} rom - The ROM object representing the target device
22-
* @param imageData Image data as a string
22+
* @param {string} imageData Image data as a string
2323
* @returns {Promise<BaseFirmwareImage>} - A promise that resolves to the loaded firmware image
2424
*/
2525
export async function loadFirmwareImage(rom: ROM, imageData: string): Promise<BaseFirmwareImage> {

src/webserial.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,29 @@ class Transport {
6363
private lastTraceTime = Date.now();
6464
private reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
6565
private buffer: Uint8Array = new Uint8Array(0);
66+
private onDeviceLostCallback: (() => void) | null = null;
6667

6768
constructor(public device: SerialPort, public tracing = false, enableSlipReader = true) {
6869
this.slipReaderEnabled = enableSlipReader;
6970
}
7071

72+
/**
73+
* Set callback for when device is lost
74+
* @param {Function} callback Function to call when device is lost
75+
*/
76+
setDeviceLostCallback(callback: (() => void) | null) {
77+
this.onDeviceLostCallback = callback;
78+
}
79+
80+
/**
81+
* Update the device reference (used when re-selecting device after reset)
82+
* @param {typeof import("w3c-web-serial").SerialPort} newDevice New SerialPort device
83+
*/
84+
updateDevice(newDevice: SerialPort) {
85+
this.device = newDevice;
86+
this.trace("Device reference updated");
87+
}
88+
7189
/**
7290
* Request the serial device vendor ID and Product ID as string.
7391
* @returns {string} Return the device VendorID and ProductID from SerialPortInfo as formatted string.
@@ -388,6 +406,14 @@ class Transport {
388406
}
389407
} catch (error) {
390408
console.error("Error reading from serial port:", error);
409+
410+
// Check if it's a NetworkError indicating device loss
411+
if (error instanceof Error && error.name === "NetworkError" && error.message.includes("device has been lost")) {
412+
this.trace("Device lost detected (NetworkError)");
413+
if (this.onDeviceLostCallback) {
414+
this.onDeviceLostCallback();
415+
}
416+
}
391417
} finally {
392418
this.buffer = new Uint8Array(0);
393419
}

0 commit comments

Comments
 (0)