fix: harden USBHardwareDeployer — drain after timeout, async cleanup, flash error recovery, baud切换
This commit is contained in:
parent
f8e92875c9
commit
369897529d
|
|
@ -114,10 +114,14 @@ const SERIAL_MONITOR_BAUD = 9600;
|
|||
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||
|
||||
function decodeBase64(base64: string): Uint8Array {
|
||||
try {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes;
|
||||
} catch {
|
||||
throw new Error('Invalid base64 input: expected a valid base64-encoded string');
|
||||
}
|
||||
}
|
||||
|
||||
/* ── USBHardwareDeployer ─────────────────────────────────────────────── */
|
||||
|
|
@ -126,7 +130,6 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
||||
private keepReading = false;
|
||||
private serialMonitorActive = false;
|
||||
private _onDisconnected: (() => void) | null = null;
|
||||
private _deviceName: string | null = null;
|
||||
|
||||
|
|
@ -157,7 +160,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
}
|
||||
|
||||
/* Clean up any previous session first */
|
||||
this.cleanup();
|
||||
await this.cleanup();
|
||||
|
||||
let port: SerialPort;
|
||||
try {
|
||||
|
|
@ -186,10 +189,10 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
this.reader = this.port.readable!.getReader();
|
||||
|
||||
/* Listen for disconnect on navigator.serial */
|
||||
this._boundDisconnectHandler = (event: SerialConnectionEvent) => {
|
||||
this._boundDisconnectHandler = async (event: SerialConnectionEvent) => {
|
||||
if (event.port === this.port) {
|
||||
console.log('[USB] port disconnected event');
|
||||
this.cleanup();
|
||||
await this.cleanup();
|
||||
this._onDisconnected?.();
|
||||
}
|
||||
};
|
||||
|
|
@ -262,7 +265,6 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
|
||||
this.writer = this.port.writable!.getWriter();
|
||||
this.reader = this.port.readable!.getReader();
|
||||
this.serialMonitorActive = true;
|
||||
this.keepReading = true;
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
|
@ -294,7 +296,6 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
|
||||
stopSerialMonitor(): void {
|
||||
console.log('[USB-SERIAL] stopSerialMonitor');
|
||||
this.serialMonitorActive = false;
|
||||
this.keepReading = false;
|
||||
}
|
||||
|
||||
|
|
@ -323,9 +324,9 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
|
||||
/* ── Disconnect / lifecycle ─────────────────────────────────── */
|
||||
|
||||
disconnect(): void {
|
||||
async disconnect(): Promise<void> {
|
||||
this.stopSerialMonitor();
|
||||
this.cleanup();
|
||||
await this.cleanup();
|
||||
}
|
||||
|
||||
onDisconnected(callback: () => void): void {
|
||||
|
|
@ -337,7 +338,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
private async stopReaderWriter(): Promise<void> {
|
||||
this.keepReading = false;
|
||||
try {
|
||||
this.reader?.cancel();
|
||||
await this.reader?.cancel();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
|
@ -350,7 +351,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
this.writer = null;
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
private async cleanup(): Promise<void> {
|
||||
if (this._boundDisconnectHandler) {
|
||||
navigator.serial.removeEventListener(
|
||||
'disconnect',
|
||||
|
|
@ -359,9 +360,12 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
this._boundDisconnectHandler = null;
|
||||
}
|
||||
this.keepReading = false;
|
||||
this.serialMonitorActive = false;
|
||||
this.stopReaderWriter();
|
||||
this.port?.close().catch(() => {});
|
||||
await this.stopReaderWriter();
|
||||
try {
|
||||
await this.port?.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
this.port = null;
|
||||
this._deviceName = null;
|
||||
}
|
||||
|
|
@ -442,6 +446,9 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
|
||||
/**
|
||||
* Read exactly `len` bytes from the serial stream, with a deadline.
|
||||
*
|
||||
* On timeout, any stray bytes that arrive from the orphaned `reader.read()`
|
||||
* promise are drained so the protocol stream stays synchronised (C1).
|
||||
*/
|
||||
private async readExact(len: number, timeoutMs: number): Promise<Uint8Array> {
|
||||
const result = new Uint8Array(len);
|
||||
|
|
@ -451,6 +458,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
const remaining = deadline - Date.now();
|
||||
if (remaining <= 0)
|
||||
throw new Error(`read timeout: got ${got}/${len}`);
|
||||
try {
|
||||
const { value, done } = await Promise.race([
|
||||
this.reader!.read(),
|
||||
new Promise<never>((_, reject) =>
|
||||
|
|
@ -464,6 +472,11 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
const copyLen = Math.min(value.length, len - got);
|
||||
result.set(value.subarray(0, copyLen), got);
|
||||
got += copyLen;
|
||||
} catch (e) {
|
||||
/* Drain stale bytes from orphaned reader.read() promise (C1) */
|
||||
await this.drainStray();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -512,6 +525,11 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
private async getSignature(): Promise<Uint8Array> {
|
||||
const cmd = new Uint8Array([STK_READ_SIGN, CRC_EOP]);
|
||||
const resp = await this.sendAndExpect(cmd, 3, STK_CMD_TIMEOUT_MS, true);
|
||||
if (resp.length < 3) {
|
||||
throw new Error(
|
||||
`signature response too short: got ${resp.length} bytes, expected 3`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`[USB] signature: ${Array.from(resp)
|
||||
.map((b) => '0x' + b.toString(16).padStart(2, '0'))
|
||||
|
|
@ -580,6 +598,28 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
completedChunks: 0
|
||||
});
|
||||
|
||||
/* ── Ensure port is at flash baud rate (I4) ── */
|
||||
this.stopSerialMonitor();
|
||||
await this.stopReaderWriter();
|
||||
if (!this.port) {
|
||||
throw new Error('Port tidak tersedia — hubungkan perangkat terlebih dahulu');
|
||||
}
|
||||
try {
|
||||
await this.port.close();
|
||||
} catch {
|
||||
/* may already be closed */
|
||||
}
|
||||
await this.port.open({
|
||||
baudRate: FLASH_BAUD,
|
||||
dataBits: 8,
|
||||
stopBits: 1,
|
||||
parity: 'none',
|
||||
flowControl: 'none'
|
||||
});
|
||||
console.log('[USB] flashBuffer: port reopened at', FLASH_BAUD);
|
||||
this.writer = this.port.writable!.getWriter();
|
||||
this.reader = this.port.readable!.getReader();
|
||||
|
||||
/* 1. Auto-reset → optiboot */
|
||||
await this.resetArduino();
|
||||
|
||||
|
|
@ -605,8 +645,13 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
);
|
||||
}
|
||||
|
||||
/* Track progmode for safe error recovery (I2) */
|
||||
let inProgmode = false;
|
||||
|
||||
try {
|
||||
/* 4. Enter programming mode */
|
||||
await this.enterProgmode();
|
||||
inProgmode = true;
|
||||
|
||||
/* 5. Program pages (word addresses = byte/2) */
|
||||
for (let page = 0; page < totalPages; page++) {
|
||||
|
|
@ -616,7 +661,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
const wordAddr = byteAddr / 2;
|
||||
|
||||
await this.loadAddress(wordAddr);
|
||||
await this.progPage(buffer.slice(byteAddr, byteAddr + thisLen));
|
||||
await this.progPage(buffer.subarray(byteAddr, byteAddr + thisLen));
|
||||
|
||||
onProgress({
|
||||
state: 'flashing',
|
||||
|
|
@ -628,6 +673,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
|
||||
/* 6. Leave programming mode */
|
||||
await this.leaveProgmode();
|
||||
inProgmode = false;
|
||||
|
||||
onProgress({
|
||||
state: 'success',
|
||||
|
|
@ -635,5 +681,22 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
|||
totalChunks: totalPages,
|
||||
completedChunks: totalPages
|
||||
});
|
||||
} catch (e) {
|
||||
/* Best-effort leave progmode to unstick the device (I2) */
|
||||
if (inProgmode) {
|
||||
try {
|
||||
await this.leaveProgmode();
|
||||
} catch {
|
||||
/* give up */
|
||||
}
|
||||
}
|
||||
onProgress({
|
||||
state: 'error',
|
||||
message: `Flash gagal: ${(e as Error).message}`,
|
||||
totalChunks: totalPages,
|
||||
completedChunks: 0
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue