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));
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
function decodeBase64(base64: string): Uint8Array {
|
function decodeBase64(base64: string): Uint8Array {
|
||||||
const binary = atob(base64);
|
try {
|
||||||
const bytes = new Uint8Array(binary.length);
|
const binary = atob(base64);
|
||||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
const bytes = new Uint8Array(binary.length);
|
||||||
return bytes;
|
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 ─────────────────────────────────────────────── */
|
/* ── USBHardwareDeployer ─────────────────────────────────────────────── */
|
||||||
|
|
@ -126,7 +130,6 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||||
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
||||||
private keepReading = false;
|
private keepReading = false;
|
||||||
private serialMonitorActive = false;
|
|
||||||
private _onDisconnected: (() => void) | null = null;
|
private _onDisconnected: (() => void) | null = null;
|
||||||
private _deviceName: string | null = null;
|
private _deviceName: string | null = null;
|
||||||
|
|
||||||
|
|
@ -157,7 +160,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clean up any previous session first */
|
/* Clean up any previous session first */
|
||||||
this.cleanup();
|
await this.cleanup();
|
||||||
|
|
||||||
let port: SerialPort;
|
let port: SerialPort;
|
||||||
try {
|
try {
|
||||||
|
|
@ -186,10 +189,10 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
this.reader = this.port.readable!.getReader();
|
this.reader = this.port.readable!.getReader();
|
||||||
|
|
||||||
/* Listen for disconnect on navigator.serial */
|
/* Listen for disconnect on navigator.serial */
|
||||||
this._boundDisconnectHandler = (event: SerialConnectionEvent) => {
|
this._boundDisconnectHandler = async (event: SerialConnectionEvent) => {
|
||||||
if (event.port === this.port) {
|
if (event.port === this.port) {
|
||||||
console.log('[USB] port disconnected event');
|
console.log('[USB] port disconnected event');
|
||||||
this.cleanup();
|
await this.cleanup();
|
||||||
this._onDisconnected?.();
|
this._onDisconnected?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -262,7 +265,6 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
|
|
||||||
this.writer = this.port.writable!.getWriter();
|
this.writer = this.port.writable!.getWriter();
|
||||||
this.reader = this.port.readable!.getReader();
|
this.reader = this.port.readable!.getReader();
|
||||||
this.serialMonitorActive = true;
|
|
||||||
this.keepReading = true;
|
this.keepReading = true;
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
|
@ -294,7 +296,6 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
|
|
||||||
stopSerialMonitor(): void {
|
stopSerialMonitor(): void {
|
||||||
console.log('[USB-SERIAL] stopSerialMonitor');
|
console.log('[USB-SERIAL] stopSerialMonitor');
|
||||||
this.serialMonitorActive = false;
|
|
||||||
this.keepReading = false;
|
this.keepReading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,9 +324,9 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
|
|
||||||
/* ── Disconnect / lifecycle ─────────────────────────────────── */
|
/* ── Disconnect / lifecycle ─────────────────────────────────── */
|
||||||
|
|
||||||
disconnect(): void {
|
async disconnect(): Promise<void> {
|
||||||
this.stopSerialMonitor();
|
this.stopSerialMonitor();
|
||||||
this.cleanup();
|
await this.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDisconnected(callback: () => void): void {
|
onDisconnected(callback: () => void): void {
|
||||||
|
|
@ -337,7 +338,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
private async stopReaderWriter(): Promise<void> {
|
private async stopReaderWriter(): Promise<void> {
|
||||||
this.keepReading = false;
|
this.keepReading = false;
|
||||||
try {
|
try {
|
||||||
this.reader?.cancel();
|
await this.reader?.cancel();
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
|
@ -350,7 +351,7 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
this.writer = null;
|
this.writer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanup(): void {
|
private async cleanup(): Promise<void> {
|
||||||
if (this._boundDisconnectHandler) {
|
if (this._boundDisconnectHandler) {
|
||||||
navigator.serial.removeEventListener(
|
navigator.serial.removeEventListener(
|
||||||
'disconnect',
|
'disconnect',
|
||||||
|
|
@ -359,9 +360,12 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
this._boundDisconnectHandler = null;
|
this._boundDisconnectHandler = null;
|
||||||
}
|
}
|
||||||
this.keepReading = false;
|
this.keepReading = false;
|
||||||
this.serialMonitorActive = false;
|
await this.stopReaderWriter();
|
||||||
this.stopReaderWriter();
|
try {
|
||||||
this.port?.close().catch(() => {});
|
await this.port?.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
this.port = null;
|
this.port = null;
|
||||||
this._deviceName = null;
|
this._deviceName = null;
|
||||||
}
|
}
|
||||||
|
|
@ -442,6 +446,9 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read exactly `len` bytes from the serial stream, with a deadline.
|
* 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> {
|
private async readExact(len: number, timeoutMs: number): Promise<Uint8Array> {
|
||||||
const result = new Uint8Array(len);
|
const result = new Uint8Array(len);
|
||||||
|
|
@ -451,19 +458,25 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
const remaining = deadline - Date.now();
|
const remaining = deadline - Date.now();
|
||||||
if (remaining <= 0)
|
if (remaining <= 0)
|
||||||
throw new Error(`read timeout: got ${got}/${len}`);
|
throw new Error(`read timeout: got ${got}/${len}`);
|
||||||
const { value, done } = await Promise.race([
|
try {
|
||||||
this.reader!.read(),
|
const { value, done } = await Promise.race([
|
||||||
new Promise<never>((_, reject) =>
|
this.reader!.read(),
|
||||||
setTimeout(
|
new Promise<never>((_, reject) =>
|
||||||
() => reject(new Error('read timeout')),
|
setTimeout(
|
||||||
remaining
|
() => reject(new Error('read timeout')),
|
||||||
|
remaining
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
]);
|
||||||
]);
|
if (done) throw new Error('stream closed');
|
||||||
if (done) throw new Error('stream closed');
|
const copyLen = Math.min(value.length, len - got);
|
||||||
const copyLen = Math.min(value.length, len - got);
|
result.set(value.subarray(0, copyLen), got);
|
||||||
result.set(value.subarray(0, copyLen), got);
|
got += copyLen;
|
||||||
got += copyLen;
|
} catch (e) {
|
||||||
|
/* Drain stale bytes from orphaned reader.read() promise (C1) */
|
||||||
|
await this.drainStray();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -512,6 +525,11 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
private async getSignature(): Promise<Uint8Array> {
|
private async getSignature(): Promise<Uint8Array> {
|
||||||
const cmd = new Uint8Array([STK_READ_SIGN, CRC_EOP]);
|
const cmd = new Uint8Array([STK_READ_SIGN, CRC_EOP]);
|
||||||
const resp = await this.sendAndExpect(cmd, 3, STK_CMD_TIMEOUT_MS, true);
|
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(
|
console.log(
|
||||||
`[USB] signature: ${Array.from(resp)
|
`[USB] signature: ${Array.from(resp)
|
||||||
.map((b) => '0x' + b.toString(16).padStart(2, '0'))
|
.map((b) => '0x' + b.toString(16).padStart(2, '0'))
|
||||||
|
|
@ -580,6 +598,28 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
completedChunks: 0
|
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 */
|
/* 1. Auto-reset → optiboot */
|
||||||
await this.resetArduino();
|
await this.resetArduino();
|
||||||
|
|
||||||
|
|
@ -605,35 +645,58 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 4. Enter programming mode */
|
/* Track progmode for safe error recovery (I2) */
|
||||||
await this.enterProgmode();
|
let inProgmode = false;
|
||||||
|
|
||||||
/* 5. Program pages (word addresses = byte/2) */
|
try {
|
||||||
for (let page = 0; page < totalPages; page++) {
|
/* 4. Enter programming mode */
|
||||||
const byteAddr = page * pageSize;
|
await this.enterProgmode();
|
||||||
const remaining = buffer.length - byteAddr;
|
inProgmode = true;
|
||||||
const thisLen = remaining > pageSize ? pageSize : remaining;
|
|
||||||
const wordAddr = byteAddr / 2;
|
|
||||||
|
|
||||||
await this.loadAddress(wordAddr);
|
/* 5. Program pages (word addresses = byte/2) */
|
||||||
await this.progPage(buffer.slice(byteAddr, byteAddr + thisLen));
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
const byteAddr = page * pageSize;
|
||||||
|
const remaining = buffer.length - byteAddr;
|
||||||
|
const thisLen = remaining > pageSize ? pageSize : remaining;
|
||||||
|
const wordAddr = byteAddr / 2;
|
||||||
|
|
||||||
|
await this.loadAddress(wordAddr);
|
||||||
|
await this.progPage(buffer.subarray(byteAddr, byteAddr + thisLen));
|
||||||
|
|
||||||
|
onProgress({
|
||||||
|
state: 'flashing',
|
||||||
|
message: `Flashing page ${page + 1}/${totalPages}`,
|
||||||
|
totalChunks: totalPages,
|
||||||
|
completedChunks: page + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. Leave programming mode */
|
||||||
|
await this.leaveProgmode();
|
||||||
|
inProgmode = false;
|
||||||
|
|
||||||
onProgress({
|
onProgress({
|
||||||
state: 'flashing',
|
state: 'success',
|
||||||
message: `Flashing page ${page + 1}/${totalPages}`,
|
message: 'Flash berhasil!',
|
||||||
totalChunks: totalPages,
|
totalChunks: totalPages,
|
||||||
completedChunks: page + 1
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 6. Leave programming mode */
|
|
||||||
await this.leaveProgmode();
|
|
||||||
|
|
||||||
onProgress({
|
|
||||||
state: 'success',
|
|
||||||
message: 'Flash berhasil!',
|
|
||||||
totalChunks: totalPages,
|
|
||||||
completedChunks: totalPages
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue