fix(usb-deployer): read from rxBuffer, kill orphaned-read race in get_sync
readExact and drainStray now consume the FIFO buffer fed by the pump instead of calling reader.read() directly. No orphaned reader.read() promise can swallow optiboot INSYNC/OK replies anymore. This is the core fix for "get_sync failed after N attempts".
This commit is contained in:
parent
121d592ce0
commit
f7c088013a
|
|
@ -514,78 +514,36 @@ export class USBHardwareDeployer implements HardwareDeployer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read exactly `len` bytes from the serial stream, with a deadline.
|
* Read exactly `len` bytes from rxBuffer with a deadline.
|
||||||
*
|
* The pump is the sole reader.read() caller, so there is no orphaned-read
|
||||||
* On timeout, any stray bytes that arrive from the orphaned `reader.read()`
|
* race: bytes that arrive after a timeout stay buffered for the next call.
|
||||||
* 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);
|
try {
|
||||||
let got = 0;
|
return await this.rxBuffer.readExact(len, timeoutMs);
|
||||||
const deadline = Date.now() + timeoutMs;
|
} catch (e) {
|
||||||
while (got < len) {
|
const got = this.rxBuffer.length;
|
||||||
const remaining = deadline - Date.now();
|
console.log(`[USB] readExact failed (${got} buffered): ${(e as Error).message}`);
|
||||||
if (remaining <= 0)
|
throw e;
|
||||||
throw new Error(`read timeout: got ${got}/${len}`);
|
|
||||||
try {
|
|
||||||
const { value, done } = await Promise.race([
|
|
||||||
this.reader!.read(),
|
|
||||||
new Promise<never>((_, reject) =>
|
|
||||||
setTimeout(
|
|
||||||
() => reject(new Error('read timeout')),
|
|
||||||
remaining
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
if (done) throw new Error('stream closed');
|
|
||||||
const copyLen = Math.min(value.length, len - got);
|
|
||||||
result.set(value.subarray(0, copyLen), got);
|
|
||||||
got += copyLen;
|
|
||||||
} catch (e) {
|
|
||||||
const reason = (e as Error).message || 'unknown';
|
|
||||||
console.log(`[USB] readExact failed at ${got}/${len}: ${reason}`);
|
|
||||||
/* Drain stale bytes from orphaned reader.read() promise (C1) */
|
|
||||||
await this.drainStray();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drain any stray bytes from the serial input.
|
* Drain any buffered/stray bytes. With the pump running, stray bytes are
|
||||||
* Keeps reading until no bytes arrive for DRAIN_BYTE_TIMEOUT_MS,
|
* already in rxBuffer, so draining is a synchronous clear plus a short
|
||||||
* or DRAIN_TOTAL_MS has elapsed.
|
* settle window to absorb bytes still in flight.
|
||||||
*/
|
*/
|
||||||
private async drainStray(): Promise<void> {
|
private async drainStray(): Promise<void> {
|
||||||
|
/* Absorb bytes still arriving: wait up to DRAIN_TOTAL_MS, clearing. */
|
||||||
const deadline = Date.now() + DRAIN_TOTAL_MS;
|
const deadline = Date.now() + DRAIN_TOTAL_MS;
|
||||||
let totalDrained = 0;
|
let drained = this.rxBuffer.readAvailable().length;
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
try {
|
await sleep(DRAIN_BYTE_TIMEOUT_MS);
|
||||||
const { value, done } = await Promise.race([
|
const more = this.rxBuffer.readAvailable().length;
|
||||||
this.reader!.read(),
|
if (more === 0) break;
|
||||||
new Promise<never>((_, reject) =>
|
drained += more;
|
||||||
setTimeout(() => reject(new Error('timeout')), DRAIN_BYTE_TIMEOUT_MS)
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
if (done) break;
|
|
||||||
if (value && value.length > 0) {
|
|
||||||
totalDrained += value.length;
|
|
||||||
/* Got bytes; keep looping inside the same deadline window. */
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
/* Empty chunk: treat same as a quiet window. */
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
/* No bytes available for DRAIN_BYTE_TIMEOUT_MS -> channel is clean. */
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalDrained > 0) {
|
|
||||||
console.log(`[USB] drained ${totalDrained} stray bytes`);
|
|
||||||
}
|
}
|
||||||
|
if (drained > 0) console.log(`[USB] drained ${drained} stray bytes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── STK500v1 command implementations ─────────────────────────── */
|
/* ── STK500v1 command implementations ─────────────────────────── */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue