Skip to content

Commit 00ceb3b

Browse files
Attempt a workaround for the bad response issue
See microbit-foundation/python-editor-v3#89 Ultimately this needs a fix in DAPLink to clear the queue on reset but that won't have much impact on existing devices.
1 parent c01107d commit 00ceb3b

File tree

1 file changed

+78
-1
lines changed

1 file changed

+78
-1
lines changed

lib/usb-device-wrapper.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class DAPWrapper {
9696
this.initialConnectionComplete = true;
9797
}
9898

99-
await this.daplink.connect();
99+
await this.connectWithRetry();
100100
await this.cortexM.connect();
101101

102102
this.logging.event({
@@ -154,6 +154,83 @@ export class DAPWrapper {
154154
throw lastError;
155155
}
156156

157+
/**
158+
* Drain any stale responses from the USB buffer.
159+
* Sends a known command (DAP_INFO) and keeps reading until we get the
160+
* matching response. This recovers from the state where a previous session
161+
* was closed mid-serial-read, leaving stale responses in the device's
162+
* USB buffer that break subsequent connections.
163+
*
164+
* See: https://github.com/microbit-foundation/python-editor-v3/issues/89
165+
*/
166+
private async drainStaleResponses(): Promise<void> {
167+
// DAPLink's DAP_PACKET_COUNT is 5-8 for micro:bit variants.
168+
// In practice, only 1-2 stale responses are typical (from interrupted serial read).
169+
// Use a value slightly above the max buffer size as a safety margin.
170+
const maxAttempts = 10;
171+
172+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
173+
// DAP_INFO is a safe, read-only command that always succeeds
174+
const packet = [DapCmd.DAP_INFO, 0x01];
175+
await this.transport.write(Uint8Array.from(packet).buffer);
176+
177+
const response = await this.transport.read();
178+
const responseBytes = new Uint8Array(response.buffer);
179+
180+
if (responseBytes[0] === DapCmd.DAP_INFO) {
181+
this.logging.log("USB buffer drain: synchronized");
182+
return;
183+
}
184+
this.logging.log(
185+
`USB buffer drain: discarded stale response 0x${responseBytes[0].toString(16)}, expected 0x${DapCmd.DAP_INFO.toString(16)}`,
186+
);
187+
}
188+
189+
this.logging.log(
190+
"USB buffer drain: warning - could not fully synchronize after max attempts",
191+
);
192+
}
193+
194+
/**
195+
* Connect to daplink with retry logic for stale response recovery.
196+
* If a "Bad response" error occurs (indicating stale USB data from a
197+
* previous session), this method will drain the stale responses and retry.
198+
*/
199+
private async connectWithRetry(maxRetries = 3): Promise<void> {
200+
let lastError: Error | undefined;
201+
202+
for (let attempt = 0; attempt < maxRetries; attempt++) {
203+
try {
204+
if (attempt > 0) {
205+
this.logging.log(
206+
`Connection retry attempt ${attempt + 1}/${maxRetries}`,
207+
);
208+
await this.drainStaleResponses();
209+
}
210+
211+
await this.daplink.connect();
212+
return;
213+
} catch (e) {
214+
// https://github.com/ARMmbed/dapjs/blob/master/src/proxy/cmsis-dap.ts#L178
215+
if (e instanceof Error && /^Bad response for /.test(e.message)) {
216+
lastError = e;
217+
this.logging.log(`Bad response error during connect: ${e.message}`);
218+
219+
try {
220+
await this.transport.close();
221+
} catch {
222+
// Ignore close errors
223+
}
224+
await this.transport.open();
225+
continue;
226+
}
227+
throw e;
228+
}
229+
}
230+
231+
throw lastError || new Error("Connection failed after retries");
232+
}
233+
157234
async startSerial(listener: (data: string) => void): Promise<void> {
158235
const currentBaud = await this.daplink.getSerialBaudrate();
159236
if (currentBaud !== 115200) {

0 commit comments

Comments
 (0)